Попов Дмитрий
30 сент. 2021 г., 8:25

Мутации в @prisma-cms/nextjs-nexus

Николай, приветствую!

Сунулся разбираться с мутациями - забуксовал.
добавляю тип мутации создания поста.

export const PostExtendMutation = extendType({ type: 'Mutation', definition(t) { t.nonNull.field('createPost', { type: 'Post', resolve(_root, _args, ctx) { return ctx.prisma.post.create() }, }, })
На create() ожидаемо ругается - нужны данные для создания нового поста.
Нужна помощь: что делать и куда бечь...
Дима, привет!

Бежать можно было в соседние файлы :)

Смотри: для чтения в GraphQL используются Type и скаляры. А для входящих параметров используются InputType. В нексусе первые ты создаешь методом objectType, а вторые создаются методом inputObjectType.

Для примера рассмотрим мутацию сброса пароля resetPasswordProcessor. В ней на вход требуется аргумент where: ResetPasswordProcessorWhereInput!


Вот этот код:

export const ResetPasswordProcessorWhereInput = inputObjectType({ name: 'ResetPasswordProcessorWhereInput', definition(t) { t.nonNull.field('User', { type: 'UserWhereUniqueInput', }) t.nonNull.id('code') }, })
То есть здесь два параметра: User и code.


export const ResetPasswordExtendMutation = extendType({ type: 'Mutation', definition(t) { // ... t.nonNull.field('resetPasswordProcessor', { type: 'AuthPayload', args: { where: nonNull('ResetPasswordProcessorWhereInput'), }, resolve: resetPasswordProcessor, }) }, })
Все. Далее смотрим уже сам резолвер. Там у нас входящие параметры доступны в объекте args (что вторым атрибутом залетает). Только обрати внимание на этот блок:
const code = args.where.code const whereUser = args.where.User as Prisma.UserWhereUniqueInput
К сожалению, нексус-плагин не совсем корректно генерит типы и если просто передать args.where, он там ругается. Приходится вот так более явно задавать типы. В общем, попробуй, поиграйся и если что, выливай с --no-verify, я подправлю и напишу что и как.

Вопрос: тип AuthPayload какие функции выполняет? В описании есть, что это 'Объект ответа мутации пользователя'. Если мутация прошла ,то он возвращает данные пользователя?
Ты подобные типы чуть ли не везде используешь, надо понять, что это)
Дима, очень странный ты вопрос задаешь. AuthPayload не выполняет никаких функций, кроме как своих прямых - структура возвращаемых данных. Вот ровно как и у любого другого GraphQL-типа. Там прописаны поля и данные возвращаемые должны соответствовать. А вот что я на стороне фронта с этим буду делать - это уже совершенно другой вопрос. И все вот это мало относится к твоему изначальному вопросу. Ты спрашивал "как получить данные для создания записи". Я тебе сказал как. Ты полез в "чтение данных". Разберись сначала с ответом на твой вопрос, а потом в сторону полезешь.

Николай, посмотри, пожалуйста, коммит: https://github.com/linklib/gribok-prisma/commit/94458e23ae35e773b31e1e9ae4a03ea4eb59baa5

Расширил тип Mutation для создания поста. Но "забодай меня комар", если понимаю, что делаю(((
Да, ты определенно мало понимаешь, что делаешь :)
И главная проблема в том, что ты даже не сращиваешь типы данных. То есть тут уже как раз не вопрос в более высокоуровневых библиотеках, а в базовах вещах в принципе. Вот твой код

export const createpost: FieldResolver<'Mutation', 'createPost'> = async ( _, args, ctx ) => { const title: string = args.where.title || {} const text: string = args.where.text || {} const post = await ctx.prisma.post.create({ data: { title, text, }, }) return { data: post, } }
Давай его разберем.
Вот у тебя идет вызов призма-метода на создание поста с передачей в него данных в data
const post = await ctx.prisma.post.create({ data: { title, text, }, })
А какой тип у data?


/** * Post create */ export type PostCreateArgs = { /** * Select specific fields to fetch from the Post * **/ select?: PostSelect | null /** * Choose, which related nodes to fetch as well. * **/ include?: PostInclude | null /** * The data needed to create a Post. * **/ data: XOR<PostCreateInput, PostUncheckedCreateInput> }

export type PostCreateInput = { id?: string createdAt?: Date | string updatedAt?: Date | string title: string text?: string | null mashroom?: MashroomCreateNestedOneWithoutPostsInput CreatedBy: UserCreateNestedOneWithoutPostsInput Likes?: LikeCreateNestedManyWithoutPostLikeInput }
Что ты там пытаешься передать? title и text. Какие у них типы?
title: string text?: string | null
То есть титл - обязательно строка должна быть, а text - не обязательный (undefined) или строка или null.

А что ты делаешь?

const title: string = args.where.title || {} const text: string = args.where.text || {}
То есть у тебя переменная равно переменная или Объект? А с чего вдруг объект? У тебя что, title или text может быть объектом? Уж в крайнем случае тебе надо было сделать так:
const title: string = args.where.title || "" const text: string = args.where.text || ""
Но title у тебя на вход итак обязательно строка, зачем его проверять?

Я тебе более того скажу: если бы ты даже вот так сделал:
const post = await ctx.prisma.post.create({ data: args.where, })
то у тебя не было бы проблем здесь с титлом и текстом, у тебя там другая ошибка:
Property 'CreatedBy' is missing in type '{ text?: string | null | undefined; title: string; }' but required in type 'PostCreateInput'.ts(2322) index.d.ts(10047, 5): 'CreatedBy' is declared here.
Призма ругается на то, что у тебя не передан CreatedBy, что вполне логично, ведь у тебя же это поле в призма-схеме обязательное. https://github.com/linklib/gribok-prisma/blob/94458e23ae35e773b31e1e9ae4a03ea4eb59baa5/prisma/schema.prisma#L125-L126

Откуда его взять? Это же текущий пользователь и его просто надо взять из контекста, проверив, что он есть.
const { currentUser } = ctx if (!currentUser) { throw new Error('Не был получен пользователь') }
Но и это еще не все. У тебя сам резолвер ругается:
Type '{ data: Post; }' is missing the following properties from type '{ id: MaybePromise<string>; createdAt: MaybePromise<Date>; updatedAt: MaybePromise<Date>; mashroomId: MaybePromise<string | null>; title: MaybePromise<...>; text: MaybePromise<...>; createdById: MaybePromise<...>; }': id, createdAt, updatedAt, mashroomId, and 3 more.ts(2322)
То есть то, что ты пытаешься вернуть, это не то, что он ожидает. Что у тебя в схему мутации прописано?
t.nonNull.field('createPost', { type: 'Post', args: { where: nonNull('PostCreateInput'), }, resolve: createpost, })
Возвращаемый type - Post. А ты что пытаешься вернуть в мутации?
return { data: post, }
Так почему же { data: post, } (что есть суть Объект со свойством post, тип которому Post)? А не просто post. То есть ты возвращаешь {post: Post} вместо Post.

Знаешь чем все это объясняется? Большими перерывами в практике и малыми объемами этой самой практики. Типы и прочее - это все ты оттачивал на фронте. И очень странно, что здесь ты начал с типами плавать.

Ну и еще момент: если в ctx.prisma.post.create ты передаешь data, то почему ты в параметры мутации прописал where? Нельзя было переименовать в data? Не будь таким великим последователем копипасты.


Изучай и исправляйся.
Николай, привет!
Спасибо!

Так я знаю, что базовые понимания хромают, от этого и постоянные уходы в сторону и к теории(
Ну и мало практики, согласен.
Ты не в ту сторону и не в ту теорию уходишь. Внимательней изучай основы: типы данных (числа, строки, булевые, массивы, объекты, null, undefined). Ты должен четко видеть и понимать где что. Какие методы ты к ним применишь, как получишь - это уже другой вопрос. Но если ты в единицу времени не отличаешь строку от объекта, поверь - это крах и идти дальше некуда.

Вот почитай:
Прочитал: так-то всё понятно и знаю, но не в каждый момент времени. Исправлюсь.


Для закрепления делаю апдейт поста и все бы ничего, но тайпскрипт ругается на https://github.com/linklib/gribok-prisma/blob/14be6513cedfe64c5d0e8d6f38afe72c86b83874/server/nexus/types/Post/resolvers/updatepost.ts#L35-L36

Подозреваю, что прилететь может null, а в типах это не указано. Верно подозреваю?

И я перелизаю на фронт? Запрос постов, создание и редакция? Что взять за пример?
Да, ругается на null. Это давняя проблема и вроде в новой версии нексус-плагина ее решили, но это новый плагин без обратной совместимости, я пока на него не переходил. Так что ёжики плакали, кололись, но продолжали есть кактус :)

Можно сделать так:
return ctx.prisma.post.update({ data: { title: title === null ? undefined : title, text, }, where: post, })
То есть прописать title === null ? undefined : title. TypeScript здесь понимает, что если прилетел null, то будет undefined. Таким образом в титл не может попасть null.

И здесь важно понимать почему нельзя передавать так: title || undefined. Дело в том, что пустая строка "" тоже расценивается как отрицательный результат. Таким образом, если прелетит пустая строка (что технически возможно и допустимо), то вместо нее будет передано undefined и данное поле не будет обновлено.

И да, можешь на фронт перелезать. Для начала просто выведи списки (то есть создай страницу для вывода, запроси данные и выведи их). Можешь для примера посмотреть страницу вывода списка пользователей. Да и в репозитории самого freecode.academy можешь смотреть. Там примеров много и все по одному образу и подобию.

Николай, привет!
Опять за помощью.

Сунулся писать запросы в src/gql для Post и понял ,что не описал два поля в нексусе, вот они: https://github.com/linklib/gribok-prisma/blob/dd425ab13f0eb0338a829193d4a1a5a9ad656acf/server/nexus/types/Post/index.ts#L25-L43

И mashroomId - без ошибок, а вот CreatedById - пишет, что нет такого свойства в Post.

Можешь посмотреть, я вообще правильно описываю эти поля и если да, то почему проблема возникла?
Дима, привет!

Так ты внимательней будь. У тебя в призма-схеме что написано?
createdById String @db.VarChar(32) CreatedBy User @relation(fields: [createdById], references: [id])
createdById - с маленькой буквы. А ты что пишешь? CreatedById с большой буквы. Ты бы хотя бы Ctrl + Space нажал, тебе вскод подсказал бы.


В наименовании поля тоже перепутал, написал t.nonNull.field('CreatedById' (с большой буквы).

И здесь ты в целом не очень хорошо поступил. На примере того же "правильного" поля mashroomId: У тебя в БД mashroomId - это строка (в призма-схеме), а связанный объект - Mashroom. А ты у себя в нексусе эти строчные поля перебиваешь объектами. Это не хорошо. Получается, если тебе нужен будет всего-лишь id связанного объекта, тебе придется прям запрашивать этот объект. Представь, ты получаешь список сотни объектов и к ним надо будет id связанных объектов: на получение списка у тебя будет 1 запрос, а потом на каждый объект еще сотня запросов. А если ты и второй связанный объект захочешь, то еще сотня запросов. И все это без необходимости. Резюме: строчное поле должно возвращать строчное значение. Для объектов делай другое поле.

И еще, вот ты гриб сделал не обязательным полем, а кем создано - обязательным. Я тебе говорил: это не хорошая практика, она будет от тебя требовать в каждом запросе прописывать запрос и на связанный объект.

Переделывай, я не буду тебе сейчас отправлять ПР. Подсказок достаточно.
Спасибо!
То есть в нексусе и ресолверы никакие не нужны: просто строчные поля, как текст и титл?
Для скаляров да, не нужны.
Николай, с грибами ка-то не очень понятно:

Попытка описать t.string('mashroomId') не прошла
Expected 2 arguments, but got 1.ts(2554)
definitionBlocks.d.ts(477, 55): Arguments for the rest parameter 'config' were not provided.

Нужен второй аргумент: попробовал type:'Mashroom' - не прокатило.

У тебя нет подобного элемента на фрикомп.академии - подсмотреть?

Схема:

model Post { id String @id @default(cuid()) @db.VarChar(32) createdAt DateTime @default(now("0")) @db.DateTime(0) updatedAt DateTime @default(now("0")) @updatedAt @db.DateTime(0) mashroom Mashroom? @relation(fields: [mashroomId], references: [id]) mashroomId String? @db.VarChar(32) title String text String? createdById String @db.VarChar(32) createdBy User @relation(fields: [createdById], references: [id]) Likes Like[] @@index([createdById], name: "User") }

Во-первых, не выкатил коммит.

Во-вторых, ты просто сохрани и все. Нексус схему свою перегенерирует и будет ОК (не забудь yarn dev)
Николай, привет! Коммит не выкатил, так как особо нечего. И yarn dev не помогает. А вот перезагрузка образа докера с mysql - помогла.
Спасибо! Ковыряю дальше.
Дима, привет!

Не ищи взаимосвязи там, где ее не может быть :) Образ MySQL и TypeScript вообще никак и никаким образом не связаны. А скорее всего тут просто задержка в генерации нексусом и не сразу реагирование тайпскрипта, о чем я подробно писал совсем недавно здесь: https://freecode.academy/comments/topics/obzornoe-video-@prisma-cms/nextjs-nexus/dima,-u-tebya-tam-vse-ok.-a-oshibki-v-ide-v-takikh-sl
Но ты, видимо, опять не особо обратил внимание.
Итак, говорю еще раз:
1. yarn dev всегда надо запускать, чтобы нексус перегенерировал файлы при изменении.
2. Подождать секунд 10-15, чтобы исчезла ТС-ошибка.
3. Если ошибка не исчезла:
3.1. Запусти yarn types:server, это запустит проверку типов в серверной части. Здесь не дожидается актуализации тайпскрипта, потому что это не в режиме ожидания, а всегда с нуля и полностью.
3.2. Если types:server не выявил ошибок, значит рестартуй TS в IDE (значит он просто не увидел изменений).
3.3. Если все же ошибки были и при types:server, значит ошибки действительно есть и их надо исправлять.

Говорю точно, что у тебя как раз такой случай возник.
Понял, спасибо! Инструкция супер!!!
Проблема в том, что я пока не могу быть уверенным, что написал правильно и переложить ошибку на IDE:)))
1. А почему у тебя опять схема не up-to-date? Почему у тебя поля старые там?


Это потому что у тебя не запущен yarn dev. Давай договоримся?: ты не будешь работать с проектом без запущенного yarn dev. А иначе в чем смысл? Потом сам в ошибках как в лесной чаще, так и меня постоянно просишь гоняться за призраками.

2. Со схемой вроде ОК, но в posts ты не прописал никакие аргументы, то есть с фронта не получится ни фильтровать, ни сортировать, ни кол-во записей управлять.
Спасибо!
Так-то я работать с проектом начинаю с запуска dev, но хз - может здесь сбойнуло.

По схеме: то есть как минимум надо ордер добавить и лимит? И скип.
Николай, привет!
Можешь одним глазком посмотреть - https://github.com/linklib/gribok-prisma/commit/e35b04bf38b2c9c23bdb78e4c4d034194e1df8a5
Вроде все работает, но на всякий случай.
Николай, привет!


Можно как-то лучше сделать?

И есть 2 вопроса, которые в которых нужна помощь:
1. Как в случае удчного добавления поста правильно учести посетителя в перечень постов или чего-нибудь ему написать?
2. Я так погнял, зто здесь тесты проходят, но не понял как. Можешь объяснить, что происходит в коде?

Спасибо!
Дима, привет!


>> 1. Как в случае удчного добавления поста правильно учести посетителя в перечень постов или чего-нибудь ему написать?
2. Я так погнял, зто здесь тесты проходят, но не понял как. Можешь объяснить, что происходит в коде?

Это тебе надо получше освоить промисы (Promise). У тебя есть обработчик на успешное выполнение запроса. Убедись в ответе, что объект был получен (то есть создан). Далее уже решай что с этим делать. Для примера смотри создание технологий на текущем сайте. https://github.com/freecode-academy/freecode.academy/blob/83c82a066d10320733481fac06accc961c0b00d5/src/pages/Technologies/Create/index.tsx#L77-L87

mutation[0]({ variables: { data, }, }) .then(async (r) => { if (r.data?.createTechnology) { try { await mutation[1].client.resetStore() } catch (error) { console.error(error) } router.push(`/technologies/${r.data.createTechnology.id}`) } }) .catch((error) => { alert(error.message) })
Здесь в блоке then проверяю полученную технологию if (r.data?.createTechnology) и если все ОК, то
1. Сбрасываю кеш аполло-клиента (API-клиента) (одновременно со сбросом кеша выполняется перезапрос всех активных GraphQL-запросов на странице).
2. Выполняю переход на страницу технологии.


const router = useRouter() /** * При успешном выполнении перекидываем на созданную страницу */ const onSave = useCallback( (data: CreateTopicProcessorMutation) => { const uri = data.response.data?.uri if (uri) { router.push(uri) } }, [router] ) const [mutate] = useCreateTopicProcessorMutation({ onCompleted: onSave, })

Здесь примерно все тоже самое делается (только что кеш забыл сбросить), но обработчик на сохранение передан параметром в сам хук мутации (параметр onCompleted). Смысл тот же, но структура кода так вроде как понятней.

Николай, привет!
Спасибо! С промисами более-менее разобрался: первый код оказался более понятен.
Дима, привет!

>> первый код оказался более понятен.

Да, первый более понятен, потому что перед глазами полностью await then/catch. А вот второй получается чуть более запутанным, потому что не понятно что это вообще за onCompleted. Но на самом деле в случае с использованием аполло-клиент такой метод более правильный, потому что в рамках АПИ данной библиотеки. Смотри официальную документацию: https://www.apollographql.com/docs/react/data/mutations/#oncompleted

То есть в качестве параметра onCompleted передается функция-обработчик, которая будет вызвана в случае успешного АПИ-запроса. Плюс здесь в том числе и в том, что ты еще до вызовы непосредственно запроса прописываешь какую логику ты хочешь получить, когда запрос будет выполнен и будет выполнен успешно. Иногда такой подход более понятный с организационной точки зрения. Но, опять-таки, это кому как больше нравится и совсем не обязательно делать именно так. Но понимать такие приемы очень важно, потому что часто библиотеки пишутся так, что в них приходится передавать обработчики. Это, что называется, callback hell (Ад обратных вызовов). Есть вот даже отдельный сайт, посвященный этому явлению :)
Николай, добавляю картинку к посту.
1. Изменил схему призмы
2. Дописал тип поля в нексусе

и... не понимаю, а что дальше делать. Можешь дать направляющий пинок?
Дима, ну вот опять...
>> и... не понимаю, а что дальше делать.
А я откуда могу знать, что дальше? То есть я откуда могу знать, что ты хочешь сделать?
И вот ты пишешь "добавляю картинку к посту. ". То есть в единственном числе (то есть один-к-одному). А схему какую пишешь? https://github.com/linklib/gribok-prisma/blob/7601402a1362dc6172f26d712f765dc9a62f51d2/prisma/schema.prisma#L131
model Post { id String @id @default(cuid()) @db.VarChar(32) ... Files File[] }
А прописал множественную (то есть один-ко-многим). В твоем случае даже при множественном, твоя схема не очень удачная.
model File { id String @id @default(cuid()) @db.VarChar(32) ... postId String? @db.VarChar(32) Post Post? @relation(fields: [postId], references: [id]) }
Таким образом ты завязал файл на посте, а не пост на файле. Улавливай разницу. Здесь речь о первоисточниках. Если ты в дальнейшем будешь картинки добавлять к грибам, лайкам и т.п., ты для всех этих типов будешь прописывать ключи в объект картинки? Что в таком случае получится? Можно будет загрузить картинку, которая одновременно относится к посту, грибу, лайку и еще к чему-то? Ведь технически это будет возможно. (Хотя и в другом случае тоже можно будет, но не буду в дебри лезть).

В твоем случае, если бы ты хотел сделать одну картинку на одну публикацию, то надо было ссылку на картинку писать в самом посте, а не в картинке. А если ты хочешь много картинок для поста, то правильней завести связующий тип PostImage, у которого два ключа: ссылка на пост и ссылка на картинку. А схема такая:
type Post { PostImages: PostImage[] }


Спасибо, переделываю схему.
Николай, посмотришь по возможности коммит?
Там схема и нексус (описания запросов).
А вот как мутации прописывать и вообще, что делать дальше: туманно.
Написал там тебе несколько комментариев.

>> А вот как мутации прописывать и вообще, что делать дальше: туманно.

Ну ты хотя бы как-то сформулировать задачу должен. Как без этого? Тут видится несколько отдельных задач:
1. Написать мутацию для загрузки файла/файлов для топика (к слову, ты сам себе же задачу усложнил, потому что выбрал модель многие-ко-многим через PostImage, ну да ладно, зато может узнаешь больше).
2. На фронте написать механизм загрузки фото (то есть форму)
3. Вывод списка фото.

И т.п.

Задачи обязательно надо выделять отдельно, потому что они не всегда одни и те же. К примеру, то, что ты прописал картинки постам, не обязывает делать механизм загрузки этих фото со фронта. Может ты парсер на сервере пропишешь и будешь тянуть их со стороннего ресурса. Это для примера.
Плюс к этому, надо четко писать какую именно ты задачу выполняешь, потому что не ясно где ты что знаешь, а что нет.

Я тебе скажу так: тебе сейчас лучше развернуть локально у себя freecode.academy и поиграться с ним. Там ты найдешь примеры того, что тебе нужно. Загрузка фото есть в профиле пользователя. Работа с моделью многие-ко-многим есть в связке Пользователь-Технологии как минимум. Покопайся там, разбери что сможешь понять. Что не сможешь - спрашивай. Так будет эффективней. Когда нет мыслей для решения новых задач с нуля, посмотри как подобное делается у других.
Николай, привет! Разбираюсь с загрузкой фойла на примере https://github.com/linklib/gribok-prisma/blob/db59452183788330442feb723124b610b37439c5/src/pages/Users/User/index.tsx

Там как раз форма для загрузки аватара. Но не могу понять, почему отдает 404 страницу по /users/[id юзера]
Вороде как должно отдавать страницу, но нет. Можешь подсказать, в чем проблема?
Дима, привет!

Может у тебя база данных не работает? Я развернул твой проект, у меня работает все. По идее в логах терминала должно что-то выводиться. Из списка пользователей не получается перейти?

Не, не хочет. Но раз у тебя норм - буду искать причину.
В логах потоком идет
Watchpack Error (watcher): Error: ENOSPC: System limit for number of file watcher reachrd, watch 'home/dima/projects/prisma'
Что это может быть?
Блин, ну ты хотя бы в гугл-переводчик воткнул сообщение :)
Место на диске у тебя закончилось.
115 гб свободно) Не оно(
Аа, это уже я тупанул. Увидел код ENOSPC и дальше не прочитал. На самом деле говорит, что слишком много файлов отдновременно открыто и отслеживается. Вот ветка: https://stackoverflow.com/questions/53930305/nodemon-error-system-limit-for-number-of-file-watchers-reached

Выполни
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
Просто когда мы запускаем yarn dev, там выполняется отслеживание кучи файлов на диске (в проекте).

Спасибо! Пробую.
А это может быть причиной 404?
-----
Таки да: оно и мешало)
Да, оно с большой долей вероятности мешало. Данные же в базе данных хранятся, и для их чтения надо прочитать файлы базы данных, а на это не хватало лимитов на открытие файлов. Вот и ошибка.

Добавить комментарий