Всем привет!
Внедряться в итоге все это будет на сайт Клуба.
Напомню, что чаты здесь реализованы с помощью компонента @prisma-cms/society и модуля @prisma-cms/society-module, а проекты и задачи с помощью @prisma-cms/cooperation и @prisma-cms/cooperation-module.
Для простоты эти парные модули будем назваться Society (блого-социальный модуль) и Cooperation (проекты, задачи, команды и т.п.).
В этих двух отдельных модулях сейчас нас более всего интересуют ChatRoom (Чат-комнаты) из Society и Task (Задачи) из Cooperation. Эти сущности сейчас никак между собой не связаны, то есть Чат-комнаты ничего не знают о Задачах http://joxi.ru/MAjz7eNc4VoNOA, а Задачи ничего не знают о Чат-комнатах http://joxi.ru/EA4KnbqcwdMl12. То есть создавая чаты и общаясь в них по каким-то задачам, мы не сможем сделать выборку только тех чатов, которые имеют непосредственное отношение к каким-либо задачам. В свою очередь мы не можем зайти в задачу и получить чат, относящийся непосредственно к ней. А очень бы хотелось... Таким образом перед нами стоит задача установить связи между этими сущностями и получить API-методы для создания чатов для задач и получать чаты в задачах.
Шаг 1. Добавляем связь в схему.
Вообще связь создавать можно как в @prisma-cms/society-module, добавив в зависимости @prisma-cms/cooperation-module, так и наоборот (я уже писал ранее об этом, и это на мой взгляд одна из сильнейших сторон @prisma-cms). Но я стараюсь всегда добавлять связь от малого к большему. В нашем случае большее - это @prisma-cms/society-module (так как общение - это очень сложный и объемный модуль, с проверками прав и т.п.), а @prisma-cms/cooperation-module хоть и тоже довольно объемный и сложный модуль, но все-таки в данной задаче на мой взгляд это меньший модуль. Логика такого решения в том, что @prisma-cms/society-module используется много где в других модулях, а @prisma-cms/cooperation-module нет, и не круто будет подключая один модуль тянуть за ним еще кучу всего.
Итак, в @prisma-cms/cooperation-module в package.json я дописываю в dependencies "@prisma-cms/society-module": "latest" и выполняю установку зависимостей yarn --ignore-engines
Внимание! Важно делать именно так, то есть прописывать в package.json зависимость с указанием версии "latest", а не устанавливать через команду yarn add @prisma-cms/society-module@latest. Дело в том, что хотя установка выполнится корректно, в package.json будет записано типа "@prisma-cms/society-module": "^1.3.8", то есть с указанием конкретной версии, а не latest. Такое часто приводит к коллизиям при установке взаимоиспользуемых модулей ( когда занимаешься разработкой модуля и еще не вылил в сеть свежую версию, всегда в каком-либо модуле будет более старая версия). Хотя это в основном справедливо для этапа разработки модуля, а не при использовании его сторонними разработчиками, но все же считаю это правило упускать не надо, просто на будущее.
Все, установили зависимость. Теперь пропишем ее для использования в модуле. Для этого мы прописываем его в вызов метода this.mergeModules.
import SocietyModule from "@prisma-cms/society-module";
// ...
this.mergeModules([
// ...
SocietyModule,
// ...
]);
Сохраняем и выполняем deploy.
endpoint=http://localhost:4466/cooperation/dev yarn deploy
Если вы еще не выполняли деплой и выполняете его в первый раз, то у вас будут созданы не только сущности, прилетевшие с @prisma-cms/society-module, но и вообще все, что сейчас содержится в @prisma-cms/cooperation-module. А я уже выполнял деплой ранее, поэтому у меня вот только вновь прибывшие:
endpoint=http://localhost:4466/cooperation/dev yarn deploy
yarn run v1.13.0
$ NODE_ENV=test node --experimental-modules src/server/scripts/deploy/with-prisma
(node:11544) ExperimentalWarning: The ESM module loader is experimental.
✔
Changes:
Resource (Type)
+ Created type `Resource`
+ Created field `id` of type `GraphQLID!`
+ Created field `code` of type `GraphQLID`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `type` of type `Enum`
+ Created field `name` of type `String`
+ Created field `longtitle` of type `String`
+ Created field `content` of type `Json`
+ Created field `contentText` of type `String`
+ Created field `published` of type `Boolean!`
+ Created field `deleted` of type `Boolean!`
+ Created field `hidemenu` of type `Boolean!`
+ Created field `searchable` of type `Boolean!`
+ Created field `uri` of type `String`
+ Created field `isfolder` of type `Boolean!`
+ Created field `CreatedBy` of type `Relation!`
+ Created field `Parent` of type `Relation`
+ Created field `Childs` of type `[Relation!]!`
+ Created field `Image` of type `Relation`
+ Created field `rating` of type `Float`
+ Created field `positiveVotesCount` of type `Int`
+ Created field `negativeVotesCount` of type `Int`
+ Created field `neutralVotesCount` of type `Int`
+ Created field `CommentTarget` of type `Relation`
+ Created field `Comments` of type `[Relation!]!`
+ Created field `Votes` of type `[Relation!]!`
+ Created field `Tags` of type `[Relation!]!`
ChatMessage (Type)
+ Created type `ChatMessage`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `content` of type `Json`
+ Created field `contentText` of type `String`
+ Created field `CreatedBy` of type `Relation`
+ Created field `Room` of type `Relation!`
+ Created field `ReadedBy` of type `[Relation!]!`
ChatMessageReaded (Type)
+ Created type `ChatMessageReaded`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `Message` of type `Relation!`
+ Created field `User` of type `Relation!`
+ Created field `updatedAt` of type `DateTime!`
ChatRoom (Type)
+ Created type `ChatRoom`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `name` of type `String!`
+ Created field `description` of type `String`
+ Created field `image` of type `String`
+ Created field `code` of type `GraphQLID`
+ Created field `Members` of type `[Relation!]!`
+ Created field `CreatedBy` of type `Relation!`
+ Created field `Messages` of type `[Relation!]!`
+ Created field `isPublic` of type `Boolean`
+ Created field `Invitations` of type `[Relation!]!`
ChatRoomInvitation (Type)
+ Created type `ChatRoomInvitation`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `CreatedBy` of type `Relation!`
+ Created field `User` of type `Relation!`
+ Created field `ChatRoom` of type `Relation!`
+ Created field `Notice` of type `Relation`
Notice (Type)
+ Created type `Notice`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `type` of type `Enum!`
+ Created field `User` of type `Relation!`
+ Created field `CreatedBy` of type `Relation`
+ Created field `ChatMessage` of type `Relation`
+ Created field `ChatRoomInvitation` of type `Relation`
+ Created field `updatedAt` of type `DateTime!`
NotificationType (Type)
+ Created type `NotificationType`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `name` of type `String!`
+ Created field `code` of type `GraphQLID`
+ Created field `comment` of type `String`
+ Created field `Users` of type `[Relation!]!`
+ Created field `CreatedBy` of type `Relation!`
ResourceTag (Type)
+ Created type `ResourceTag`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `status` of type `Enum!`
+ Created field `Resource` of type `Relation!`
+ Created field `Tag` of type `Relation!`
+ Created field `CreatedBy` of type `Relation!`
Tag (Type)
+ Created type `Tag`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `name` of type `String!`
+ Created field `status` of type `Enum!`
+ Created field `Resources` of type `[Relation!]!`
+ Created field `CreatedBy` of type `Relation!`
Vote (Type)
+ Created type `Vote`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `Resource` of type `Relation!`
+ Created field `User` of type `Relation!`
+ Created field `value` of type `Float!`
File (Type)
+ Created field `ImageResource` of type `Relation`
User (Type)
+ Created field `Resources` of type `[Relation!]!`
+ Created field `Rooms` of type `[Relation!]!`
+ Created field `CreatedRooms` of type `[Relation!]!`
+ Created field `Messages` of type `[Relation!]!`
+ Created field `ReadedMessages` of type `[Relation!]!`
+ Created field `Notices` of type `[Relation!]!`
+ Created field `Votes` of type `[Relation!]!`
+ Created field `NotificationTypes` of type `[Relation!]!`
+ Created field `NotificationTypesCreated` of type `[Relation!]!`
+ Created field `Tags` of type `[Relation!]!`
+ Created field `ResourceTags` of type `[Relation!]!`
ResourceType (Enum)
+ Created enum ResourceType with values `Resource`, `Blog`, `Topic`, `Comment`
NoticeType (Enum)
+ Created enum NoticeType with values `ChatMessage`, `Call`, `CallRequest`, `ChatRoomInvitation`
TagStatus (Enum)
+ Created enum TagStatus with values `Active`, `Moderated`, `Blocked`
ChatRoomMessages (Relation)
+ Created relation between ChatMessage and ChatRoom
ResourceComments (Relation)
+ Created relation between Resource and Resource
UserNotificationTypesCreated (Relation)
+ Created relation between NotificationType and User
ResourceCreatedBy (Relation)
+ Created relation between Resource and User
NoticeUser (Relation)
+ Created relation between Notice and User
ChatRoomCreatedBy (Relation)
+ Created relation between ChatRoom and User
ResourceImage (Relation)
+ Created relation between File and Resource
UserResourceTag (Relation)
+ Created relation between ResourceTag and User
ResourceVotes (Relation)
+ Created relation between Resource and Vote
UserTags (Relation)
+ Created relation between Tag and User
ChatRoomToChatRoomInvitation (Relation)
+ Created relation between ChatRoom and ChatRoomInvitation
ResourceParent (Relation)
+ Created relation between Resource and Resource
ChatRoomInvitationInvited (Relation)
+ Created relation between ChatRoomInvitation and User
NoticeUserCreatedBy (Relation)
+ Created relation between Notice and User
UserVotes (Relation)
+ Created relation between User and Vote
ChatRoomsMembers (Relation)
+ Created relation between ChatRoom and User
ChatRoomInvitationNotice (Relation)
+ Created relation between ChatRoomInvitation and Notice
ChatMessageCreatedBy (Relation)
+ Created relation between ChatMessage and User
UserNotificationTypes (Relation)
+ Created relation between NotificationType and User
ResourcesTagsResource (Relation)
+ Created relation between Resource and ResourceTag
ResourcesTagsTag (Relation)
+ Created relation between ResourceTag and Tag
ChatRoomInvitationCreatedBy (Relation)
+ Created relation between ChatRoomInvitation and User
ChatMessageReadedByMessage (Relation)
+ Created relation between ChatMessage and ChatMessageReaded
ChatMessageToNotice (Relation)
+ Created relation between ChatMessage and Notice
ChatMessageReadedByUser (Relation)
+ Created relation between ChatMessageReaded and User
Your Prisma GraphQL database endpoint is live:
HTTP: http://localhost:4466/cooperation/dev
WS: ws://localhost:4466/cooperation/dev
⠋ Get schemaHandlerObject.flags { 'env-file': undefined }
Schema file was updated: src/schema/generated/prisma.graphql
⠋ Generating fragments for project app...src/schema/generated/api.graphql
✔ Fragments for project app written to src/schema/generated/api.fragments.js
Done in 16.05s.
Как видите, много всего прилетело с новым модулем, то есть добавили буквально две строчки, а сколько всего нового появилось... При чем это не просто программный код добавился, а выполнилось сразу несколько операций:
1. Сгенерировалась из всех модулей общая схема для деплоя в призма-сервер.
2. Выполнился деплой в паризму.
3. Посоздавались и обновились таблицы и взаимосвязи с ними в базу данных.
4. Скачалась новая API-схема.
5. Сгенерировались новые API-фрагменты для использования их в пользовательских API-запросах.
Теперь мы на выходе имеет не только обновленную и расширенную базу данных, но и API для работы с ней, обновленную графическую схему и обновленные фильтры. Вот так у нас схема выглядела: http://joxi.ru/Dr83W9lC4Vvg9A, а вот так она выглядит теперь: http://joxi.ru/YmEy1pOH0nq1Om. И хотя без масштабирования схемы теперь не видно названий сущностей, тем не менее очевидно, что их стало значительно больше. Но наша задача еще не решена, так как хотя в рамках одного модуля теперь существуют обе нужные нам сущности (ChatRoom и Task), и можно даже через API создавать/редактировать и те и другие, связей между ними по прежнему нет, это по прежнему обособленные сущности.
Что бы добавить связь между ними, делаем так:
type Task {
id: ID! @unique
"""
....
"""
ChatRoom: ChatRoom
}
Можно уже сейчас для наглядности опять выполнить деплой.
endpoint=http://localhost:4466/cooperation/dev yarn deploy
yarn run v1.13.0
$ NODE_ENV=test node --experimental-modules src/server/scripts/deploy/with-prisma
(node:12947) ExperimentalWarning: The ESM module loader is experimental.
✔
Changes:
Task (Type)
+ Created field `ChatRoom` of type `Relation`
ChatRoomToTask (Relation)
+ Created relation between ChatRoom and Task
Your Prisma GraphQL database endpoint is live:
HTTP: http://localhost:4466/cooperation/dev
WS: ws://localhost:4466/cooperation/dev
⠋ Get schemaHandlerObject.flags { 'env-file': undefined }
Schema file was updated: src/schema/generated/prisma.graphql
⠋ Generating fragments for project app...src/schema/generated/api.graphql
✔ Fragments for project app written to src/schema/generated/api.fragments.js
Done in 5.14s.
Обновляем схему и видим, что у нас появилась связь с ChatRoom: http://joxi.ru/MAjz7eNc4Vol4A
Теперь при создании или обновлении Задачи можно сразу указать создаваемую или подключаемую Чат-комнату, а при получении данных задачи можно и сразу ее Чат-комнату получить. Но пока еще нельзя создать Задачу из Чат-комнаты, потому что нет еще связи из нее на Задачу.
2. Создаем связь Чат-комната - Задача.
В папке src/modules/schema/database (можно создать подпапку и туда файл добавить) создаем файл chatRoom.graphql (название файла не важно, а вот расширение .graphql важно) и пишем в него:
type ChatRoom {
id: ID! @unique
Task: Task
}
Это вся запись. Хотя исходное описание сущности ChatRoom находится в подключаемом модуле @prisma-cms/society-module и мы не можем в нем ничего править, нам нет необходимости копировать ее хоть целиком, хоть полностью. Мы просто пишем свое дополнительное описание. При деплое все описания сущностей объединяются и деплоятся как единая схема. То есть можно даже в рамках одного модуля несколько раз описать одну и ту же сущность и задеплоить, и если конфликтов по полям не будет, то они объединятся. Рассмотрение конфликтов - это тема для отдельного топика, здесь мы ее не будем рассматривать, рассматриваемые нами здесь примеры конфликтов не имеют.
Итак, сохраняем и выполняем деплой.
endpoint=http://localhost:4466/cooperation/dev yarn deploy
yarn run v1.13.0
$ NODE_ENV=test node --experimental-modules src/server/scripts/deploy/with-prisma
(node:13417) ExperimentalWarning: The ESM module loader is experimental.
✔
Changes:
ChatRoom (Type)
+ Created field `Task` of type `Relation`
Your Prisma GraphQL database endpoint is live:
HTTP: http://localhost:4466/cooperation/dev
WS: ws://localhost:4466/cooperation/dev
⠋ Get schemaHandlerObject.flags { 'env-file': undefined }
Schema file was updated: src/schema/generated/prisma.graphql
⠋ Generating fragments for project app...src/schema/generated/api.graphql
✔ Fragments for project app written to src/schema/generated/api.fragments.js
Done in 5.25s.
Вот теперь у нас двусторонняя связь в обеих сущностях: http://joxi.ru/Vm6a53MtDBbl8r. Теперь можно написать запрос на создание Чат-комнаты с привязкой к конкретной задаче: http://joxi.ru/BA06nb7HJ0ZDlm.
Как вы могли заметить, в результате выполнения запроса на создание чат-комнаты в теле ответа мы прописали больше сущностей, получив в ответ сразу и ранее созданные таймеры по этой задаче. Это еще одна сильная сторона @prisma-cms (унаследованная от технологии GraphQL).
Кстати, обратите внимание, что мы запрос выполняли в рамках модуля Cooperation, хотя запрашиваемый метод createChatRoomProcessor прописан в Society. Дело в том, что при объединении модулей, мы получаем не только объединенную схему, но и обединенный набор методов (резолверов), и хотя их процесс объединения не такой мощный, как в случае со схемой (по сути просто замена одних другими в случае уже имеющихся), тем не менее это гораздо больше, чем ничего. И это еще одна сильная сторона @prisma-cms :)
Шаг 2. Дописываем интерфейсы.
Итак, схема у нас есть и методы уже работаю. Теперь нам осталось только дописать интерфейсы, чтобы на странице задачи появилась возможность создать чат и переписываться. Дописывать мы их будем уже в компоненте @prisma-cms/cooperation, потому что за интерфейсы отвечает именно он, а не модуль. В целом, это не особо сложно, сейчас все расскажу-покажу.
За основу можно взять то, как реализованы чаты на страницах пользователя на сайте Клуба. Если вы зайдете в профиль любого пользователя, то там выводят чаты с этим пользователем и есть возможность не сразу ему написать. Вот подключаемый класс ChatRoomsByUser, а вот здесь он вызывается.
1. Описанным выше способом устанавливаем в @prisma-cms/cooperation зависимость @prisma-cms/society@latest. С ним мы получим не только интерфейсы, но и уже готовые запросы для получения чатов, сообщений и т.п.
2. Так как в текущем варианте @prisma-cms/cooperation-module мы запускаем в отдельной директории самостоятельно (и крутится он у нас на порту 4000 по умолчанию), а @prisma-cms/cooperation мы запускаем отдельно в другой директории, то @prisma-cms/cooperation ничего не знает еще о том, что в @prisma-cms/cooperation-module у нас изменилась схема. Нам нужно подтянуть в @prisma-cms/cooperation новые API-фрагменты. Для этого выполняем:
yarn get-api-schema -e http://localhost:4000
yarn build-api-fragments
Вообще этот шаг требует оптимизации, но я пока не придумал как сделать так, чтобы этот момент полностью автоматизировать, так что его приходится выполнять на конечном проекте вручную. Компоненты, подобные @prisma-cms/society часто несут в себе два класса контекстных (ContextProvider и SubscriptionProvider). ContextProvider нужен для того, чтобы на всех уровнях ниже были доступны передаваемые в контекст переменные (включая заготовки запросов, некоторые UI-компоненты и т.п.). Пример использования описан здесь. SubscriptionProvider используется для того, чтобы автоматически подписаться на те или иные обновления с сервера (обновления данных пользователей, топиков и т.п.).
Вот при подключении подобных компонентов на конечных проектах надо подключать эти провайдеры, чтобы все работало корректно. К примеру, вот так выглядит подключение подобных провайдеров на сайте Клуба.
В нашем случае @prisma-cms/cooperation использует @prisma-cms/society, поэтому, чтобы используемые Society-компоненты работали корректно, надо в DevRenderer прописать эти провайдеры. Имейте ввиду, что DevRenderer вызывается только в режиме разработки конкретного компонента и не экспортируется автоматически на конечный проект, так что повторюсь, на конечном проекте его надо подключать дополнительно.
4. Дописываем вывод Чата на странице Задачи.
После того, как мы получили обновленные API-фрагменты и подключили контекст-провайдеры, можно пробовать выводить Чаты на страницах Задач. Для этого, как я и писал выше, мы позаимствуем пример выполнения с сайта Клуба. Я скопировал ChatRooms и немного переписал его, чтобы на вход он получал не пользователя, а задачу, и в условии запроса заменил
Members_some: {
id: userId,
},
на
Task: {
id: taskId,
},
Теперь он должен получать чаты не по признаку участия пользователя, а по привязке к задаче.
Блок нового сообщения в текущем виде я удалил, так как его суть отправка персонального сообщения указанному пользователю, а у нас задача писать в существующий Чат или создавать новый при его отсутствии.
После этого подключаю этот компонент на странице Задачи вот в таком виде:
<ChatRooms
task={object}
currentUser={currentUser}
/>
ОК, сохраняю и обновляю страницу Задачи. Ха, есть список Чатов и созданный нами ранее Чат:) http://joxi.ru/Y2LepD7h93qapA
Уже неплохо. Но нам надо еще докрутить это под намеченную логику, ведь у нас привязка не один-ко-многим, а один-к-одному, то есть в Задаче должен сразу выводиться Чат, соответствующий этой Задаче, или кнопка "Создать чат". Поэтому компонент ChatRooms мы полностью перепишем. Вот его код. Я им мало доволен, но это не страшно, главное - работает.
Ну, можно сказать и все. Остается только подключить на сайт Клуба. Подключение не потребовало особо усилий, в основном все свелось к тому, что бы убрать лишний код в кастомной странице и подключить внешний компонент. Вот коммит. Теперь здесь можно зайти в любую задачу и создать чат, чтобы обсудить детали. Еще одна приятная мелочь: фильтры в задачах сразу же подхватили новую схему и теперь можно найти все задачи, в которых есть чаты: https://modxclub.ru/tasks?filters=%7B%22ChatRoom%22%3A%7B%7D%7D
Конечно же есть еще какие-то баги. К примеру я сейчас создал чат в задаче и по задумке сразу же должен был назначиться участником в чат автор задачи, но этого не произошло. Я знаю почему (потому что у меня тут несколько измененные запросы для чатов, а не базовые, и соответственно надо чуть подправить API-запросы), но это поправится. Так же не хватает логики в плане вывода информации: в задаче мы видим чат, но из чата мы не видим задачу, то есть если сейчас зайти напрямую в чат, то не будет понятно, что он в задаче находится. Но это ничего, поправлю. Главное было сейчас в целом описать процесс интеграции отдельных призма-модулей. Надеюсь вам было интересно.