Николай Ланец
14 мар. 2019 г., 6:04

MODX-Клуб 2.14.4 Внедряем чаты в задачи

Всем привет!

Сегодня я пошагово опишу процесс внедрения чатов в задачах. Это довольно увлекательный процесс, демонстрирующий @prisma-cms и ее компоненты с лучшей стороны.

Внедряться в итоге все это будет на сайт Клуба.

Напомню, что чаты здесь реализованы с помощью компонента @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 создавать/редактировать и те и другие, связей между ними по прежнему нет, это по прежнему обособленные сущности.

Что бы добавить связь между ними, делаем так:
1. В схему сущности Task дописываем ChatRoom: ChatRoom. Получается вот так:
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
Более подробно об этом читайте в статье Разворачиваем у себя копию MODX-Клуба.

3. Подключаем контексты @prisma-cms/society.
Вообще этот шаг требует оптимизации, но я пока не придумал как сделать так, чтобы этот момент полностью автоматизировать, так что его приходится выполнять на конечном проекте вручную. Компоненты, подобные @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-запросы), но это поправится. Так же не хватает логики в плане вывода информации: в задаче мы видим чат, но из чата мы не видим задачу, то есть если сейчас зайти напрямую в чат, то не будет понятно, что он в задаче находится. Но это ничего, поправлю. Главное было сейчас в целом описать процесс интеграции отдельных призма-модулей. Надеюсь вам было интересно.

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