From 9640a329a1ad304704a09c15a1f08181fa306fdb Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 12:56:25 +0200 Subject: [PATCH 01/30] feat: initialize mentions module --- .../core/src/mention/events/handlers/index.ts | 0 .../events/handlers/mention.handler.ts | 0 packages/core/src/mention/events/index.ts | 0 .../core/src/mention/events/mention.event.ts | 0 .../core/src/mention/mention.controller.ts | 4 ++++ packages/core/src/mention/mention.module.ts | 21 +++++++++++++++++-- packages/core/src/mention/mention.service.ts | 16 ++++++++++++++ 7 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/mention/events/handlers/index.ts create mode 100644 packages/core/src/mention/events/handlers/mention.handler.ts create mode 100644 packages/core/src/mention/events/index.ts create mode 100644 packages/core/src/mention/events/mention.event.ts create mode 100644 packages/core/src/mention/mention.controller.ts create mode 100644 packages/core/src/mention/mention.service.ts diff --git a/packages/core/src/mention/events/handlers/index.ts b/packages/core/src/mention/events/handlers/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/mention/events/handlers/mention.handler.ts b/packages/core/src/mention/events/handlers/mention.handler.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/mention/events/index.ts b/packages/core/src/mention/events/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/mention/events/mention.event.ts b/packages/core/src/mention/events/mention.event.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/mention/mention.controller.ts b/packages/core/src/mention/mention.controller.ts new file mode 100644 index 00000000000..4bcaf34009f --- /dev/null +++ b/packages/core/src/mention/mention.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('/mention') +export class MentionController {} diff --git a/packages/core/src/mention/mention.module.ts b/packages/core/src/mention/mention.module.ts index 31b6c2c52d4..51065a619b4 100644 --- a/packages/core/src/mention/mention.module.ts +++ b/packages/core/src/mention/mention.module.ts @@ -1,4 +1,21 @@ import { Module } from '@nestjs/common'; - -@Module({}) +import { CqrsModule } from '@nestjs/cqrs'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { MentionService } from './mention.service'; +import { MentionController } from './mention.controller'; +import { Mention } from './mention.entity'; +import { TypeOrmMentionRepository } from './repository'; +@Module({ + imports: [ + TypeOrmModule.forFeature([Mention]), + MikroOrmModule.forFeature([Mention]), + CqrsModule, + RolePermissionModule + ], + providers: [MentionService, TypeOrmMentionRepository], + controllers: [MentionController], + exports: [TypeOrmModule, MikroOrmModule, MentionService, TypeOrmMentionRepository] +}) export class MentionModule {} diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts new file mode 100644 index 00000000000..31bbd461426 --- /dev/null +++ b/packages/core/src/mention/mention.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { EventBus } from '@nestjs/cqrs'; +import { TenantAwareCrudService } from './../core/crud'; +import { Mention } from './mention.entity'; +import { MikroOrmMentionRepository, TypeOrmMentionRepository } from './repository'; + +@Injectable() +export class MentionService extends TenantAwareCrudService { + constructor( + readonly typeOrmMentionRepository: TypeOrmMentionRepository, + readonly mikroOrmMentionRepository: MikroOrmMentionRepository, + private readonly _eventBus: EventBus + ) { + super(typeOrmMentionRepository, mikroOrmMentionRepository); + } +} From d9a1cb79eaa0d51fec7bcdc4ec7f2cf8bc761649 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 14:03:42 +0200 Subject: [PATCH 02/30] feat: add create mention service and and event --- .../core/src/mention/events/handlers/index.ts | 3 ++ .../events/handlers/mention.handler.ts | 22 ++++++++++ packages/core/src/mention/events/index.ts | 1 + .../core/src/mention/events/mention.event.ts | 6 +++ packages/core/src/mention/mention.module.ts | 5 ++- packages/core/src/mention/mention.service.ts | 40 ++++++++++++++++++- 6 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/core/src/mention/events/handlers/index.ts b/packages/core/src/mention/events/handlers/index.ts index e69de29bb2d..68fbdaae96a 100644 --- a/packages/core/src/mention/events/handlers/index.ts +++ b/packages/core/src/mention/events/handlers/index.ts @@ -0,0 +1,3 @@ +import { MentionEventHandler } from './mention.handler'; + +export const EventHandlers = [MentionEventHandler]; diff --git a/packages/core/src/mention/events/handlers/mention.handler.ts b/packages/core/src/mention/events/handlers/mention.handler.ts index e69de29bb2d..fb1ef44da32 100644 --- a/packages/core/src/mention/events/handlers/mention.handler.ts +++ b/packages/core/src/mention/events/handlers/mention.handler.ts @@ -0,0 +1,22 @@ +import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { IMention } from '@gauzy/contracts'; +import { MentionEvent } from '../mention.event'; +import { MentionService } from '../../mention.service'; + +@EventsHandler(MentionEvent) +export class MentionEventHandler implements IEventHandler { + constructor(readonly mentionService: MentionService) {} + + /** + * Handles the `MentionEvent` by creating a new mention using the provided input. + * + * @param {MentionEvent} event - The mention event containing the data required to create a mention. + * @returns {Promise} A promise that resolves to the newly created mention entry. + * + */ + async handle(event: MentionEvent): Promise { + // Extract the input from the event. + const { input } = event; + return await this.mentionService.create(input); + } +} diff --git a/packages/core/src/mention/events/index.ts b/packages/core/src/mention/events/index.ts index e69de29bb2d..8a8a0ac1a65 100644 --- a/packages/core/src/mention/events/index.ts +++ b/packages/core/src/mention/events/index.ts @@ -0,0 +1 @@ +export * from './mention.event'; diff --git a/packages/core/src/mention/events/mention.event.ts b/packages/core/src/mention/events/mention.event.ts index e69de29bb2d..07ab36acdff 100644 --- a/packages/core/src/mention/events/mention.event.ts +++ b/packages/core/src/mention/events/mention.event.ts @@ -0,0 +1,6 @@ +import { IEvent } from '@nestjs/cqrs'; +import { IMentionCreateInput } from '@gauzy/contracts'; + +export class MentionEvent implements IEvent { + constructor(public readonly input: IMentionCreateInput) {} +} diff --git a/packages/core/src/mention/mention.module.ts b/packages/core/src/mention/mention.module.ts index 51065a619b4..ebde5a69abb 100644 --- a/packages/core/src/mention/mention.module.ts +++ b/packages/core/src/mention/mention.module.ts @@ -6,7 +6,8 @@ import { RolePermissionModule } from '../role-permission/role-permission.module' import { MentionService } from './mention.service'; import { MentionController } from './mention.controller'; import { Mention } from './mention.entity'; -import { TypeOrmMentionRepository } from './repository'; +import { EventHandlers } from './events/handlers'; +import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; @Module({ imports: [ TypeOrmModule.forFeature([Mention]), @@ -14,7 +15,7 @@ import { TypeOrmMentionRepository } from './repository'; CqrsModule, RolePermissionModule ], - providers: [MentionService, TypeOrmMentionRepository], + providers: [MentionService, TypeOrmMentionRepository, ...EventHandlers], controllers: [MentionController], exports: [TypeOrmModule, MikroOrmModule, MentionService, TypeOrmMentionRepository] }) diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 31bbd461426..d79bf4b6c11 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -1,8 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; +import { IMention, IMentionCreateInput } from '@gauzy/contracts'; import { TenantAwareCrudService } from './../core/crud'; +import { RequestContext } from '../core/context'; import { Mention } from './mention.entity'; -import { MikroOrmMentionRepository, TypeOrmMentionRepository } from './repository'; +import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; +import { MikroOrmMentionRepository } from './repository/mikro-orm-mention.repository'; @Injectable() export class MentionService extends TenantAwareCrudService { @@ -13,4 +16,37 @@ export class MentionService extends TenantAwareCrudService { ) { super(typeOrmMentionRepository, mikroOrmMentionRepository); } + + /** + * Creates a new mention entity in the database. + * + * @param {IMentionCreateInput} entity - The data required to create a new mention entity. + * @returns {Promise} A promise that resolves to the newly created mention entity. + * @throws {BadRequestException} If an error occurs during the creation process, + * a `BadRequestException` is thrown with a descriptive message and the original error. + */ + async create(entity: IMentionCreateInput): Promise { + try { + // Retrieve the ID of the currently logged-in user + const mentionById = RequestContext.currentUserId(); + + // Get the tenant ID from the current request context or use the one from the entity + const tenantId = RequestContext.currentTenantId() || entity.tenantId; + + // Create the mention entry using the provided input along with the tenantId and mentionById. + const mention = await super.create({ ...entity, tenantId, mentionById }); + + /** + * TODO + * 1. Optional create an user subscibption for provided entity + * 2. Send email motifications and trigger internal system notifications for both mention and optional subscription + */ + + // Return the created mention. + return mention; + } catch (error) { + console.log('Error while creating mention:', error); + throw new BadRequestException('Error while creating mention', error); + } + } } From 65bb8f7486442d39e654c18adb1873cac989970c Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 14:40:43 +0200 Subject: [PATCH 03/30] feat: publish mention event --- packages/core/src/mention/mention.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index d79bf4b6c11..105c53ae2f5 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -6,6 +6,7 @@ import { RequestContext } from '../core/context'; import { Mention } from './mention.entity'; import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; import { MikroOrmMentionRepository } from './repository/mikro-orm-mention.repository'; +import { MentionEvent } from './events'; @Injectable() export class MentionService extends TenantAwareCrudService { @@ -49,4 +50,14 @@ export class MentionService extends TenantAwareCrudService { throw new BadRequestException('Error while creating mention', error); } } + + /** + * Publishes a `MentionEvent` to the event bus. + * + * @param {IMentionCreateInput} input - The input data required to create a new mention. + * + */ + publishMention(input: IMentionCreateInput) { + this._eventBus.publish(new MentionEvent(input)); + } } From 51242987d2199a2c2150b42c7e3e5a96a9664484 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 14:55:54 +0200 Subject: [PATCH 04/30] feat: mention controller --- packages/core/src/mention/mention.controller.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/core/src/mention/mention.controller.ts b/packages/core/src/mention/mention.controller.ts index 4bcaf34009f..0df3b985add 100644 --- a/packages/core/src/mention/mention.controller.ts +++ b/packages/core/src/mention/mention.controller.ts @@ -1,4 +1,13 @@ -import { Controller } from '@nestjs/common'; +import { Controller, UseGuards } from '@nestjs/common'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; +import { CrudController } from './../core/crud'; +import { Mention } from './mention.entity'; +import { MentionService } from './mention.service'; +@UseGuards(TenantPermissionGuard, PermissionGuard) @Controller('/mention') -export class MentionController {} +export class MentionController extends CrudController { + constructor(readonly mentionService: MentionService) { + super(mentionService); + } +} From 4c9559fdff38a3a08725d563f23f05cd623c927d Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 14:59:35 +0200 Subject: [PATCH 05/30] fix(typos): cspell build error --- packages/core/src/mention/mention.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 105c53ae2f5..5cfb820ca1a 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -39,8 +39,8 @@ export class MentionService extends TenantAwareCrudService { /** * TODO - * 1. Optional create an user subscibption for provided entity - * 2. Send email motifications and trigger internal system notifications for both mention and optional subscription + * 1. Optional create an user subscription for provided entity + * 2. Send email notifications and trigger internal system notifications for both mention and optional subscription */ // Return the created mention. From 9e38d8f3da97c928559eb8f61c376635f68cfb82 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 15:22:59 +0200 Subject: [PATCH 06/30] fix: improve code by coderabbit --- packages/core/src/mention/events/handlers/mention.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/mention/events/handlers/mention.handler.ts b/packages/core/src/mention/events/handlers/mention.handler.ts index fb1ef44da32..37eddc3d6f6 100644 --- a/packages/core/src/mention/events/handlers/mention.handler.ts +++ b/packages/core/src/mention/events/handlers/mention.handler.ts @@ -5,7 +5,7 @@ import { MentionService } from '../../mention.service'; @EventsHandler(MentionEvent) export class MentionEventHandler implements IEventHandler { - constructor(readonly mentionService: MentionService) {} + constructor(private readonly mentionService: MentionService) {} /** * Handles the `MentionEvent` by creating a new mention using the provided input. From aaabf42676f9438ca01a6a67e55298c45ceb8f4e Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 26 Nov 2024 15:33:57 +0200 Subject: [PATCH 07/30] fix: improve mention event naming --- packages/core/src/mention/events/handlers/index.ts | 4 ++-- .../src/mention/events/handlers/mention.handler.ts | 10 +++++----- packages/core/src/mention/events/mention.event.ts | 2 +- packages/core/src/mention/mention.service.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/mention/events/handlers/index.ts b/packages/core/src/mention/events/handlers/index.ts index 68fbdaae96a..757a8817fc4 100644 --- a/packages/core/src/mention/events/handlers/index.ts +++ b/packages/core/src/mention/events/handlers/index.ts @@ -1,3 +1,3 @@ -import { MentionEventHandler } from './mention.handler'; +import { CreateMentionEventHandler } from './mention.handler'; -export const EventHandlers = [MentionEventHandler]; +export const EventHandlers = [CreateMentionEventHandler]; diff --git a/packages/core/src/mention/events/handlers/mention.handler.ts b/packages/core/src/mention/events/handlers/mention.handler.ts index 37eddc3d6f6..1e080fbe9b1 100644 --- a/packages/core/src/mention/events/handlers/mention.handler.ts +++ b/packages/core/src/mention/events/handlers/mention.handler.ts @@ -1,20 +1,20 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; import { IMention } from '@gauzy/contracts'; -import { MentionEvent } from '../mention.event'; +import { CreateMentionEvent } from '../mention.event'; import { MentionService } from '../../mention.service'; -@EventsHandler(MentionEvent) -export class MentionEventHandler implements IEventHandler { +@EventsHandler(CreateMentionEvent) +export class CreateMentionEventHandler implements IEventHandler { constructor(private readonly mentionService: MentionService) {} /** * Handles the `MentionEvent` by creating a new mention using the provided input. * - * @param {MentionEvent} event - The mention event containing the data required to create a mention. + * @param {CreateMentionEvent} event - The mention event containing the data required to create a mention. * @returns {Promise} A promise that resolves to the newly created mention entry. * */ - async handle(event: MentionEvent): Promise { + async handle(event: CreateMentionEvent): Promise { // Extract the input from the event. const { input } = event; return await this.mentionService.create(input); diff --git a/packages/core/src/mention/events/mention.event.ts b/packages/core/src/mention/events/mention.event.ts index 07ab36acdff..84cbd38f479 100644 --- a/packages/core/src/mention/events/mention.event.ts +++ b/packages/core/src/mention/events/mention.event.ts @@ -1,6 +1,6 @@ import { IEvent } from '@nestjs/cqrs'; import { IMentionCreateInput } from '@gauzy/contracts'; -export class MentionEvent implements IEvent { +export class CreateMentionEvent implements IEvent { constructor(public readonly input: IMentionCreateInput) {} } diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 5cfb820ca1a..5815ae4b863 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -6,7 +6,7 @@ import { RequestContext } from '../core/context'; import { Mention } from './mention.entity'; import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; import { MikroOrmMentionRepository } from './repository/mikro-orm-mention.repository'; -import { MentionEvent } from './events'; +import { CreateMentionEvent } from './events'; @Injectable() export class MentionService extends TenantAwareCrudService { @@ -58,6 +58,6 @@ export class MentionService extends TenantAwareCrudService { * */ publishMention(input: IMentionCreateInput) { - this._eventBus.publish(new MentionEvent(input)); + this._eventBus.publish(new CreateMentionEvent(input)); } } From 82763ff579ba54bcd7bb9b6a8d8c6baae417cd27 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 06:20:58 +0200 Subject: [PATCH 08/30] fix: improve entity type based model --- packages/contracts/src/activity-log.model.ts | 6 ++---- packages/contracts/src/base-entity.model.ts | 6 ++++++ packages/contracts/src/comment.model.ts | 10 +++------- packages/contracts/src/favorite.model.ts | 7 ++----- packages/contracts/src/index.ts | 4 +++- packages/contracts/src/mention.model.ts | 6 ++---- packages/contracts/src/resource-link.model.ts | 6 ++---- packages/contracts/src/subscription.model.ts | 3 +++ 8 files changed, 23 insertions(+), 25 deletions(-) create mode 100644 packages/contracts/src/subscription.model.ts diff --git a/packages/contracts/src/activity-log.model.ts b/packages/contracts/src/activity-log.model.ts index 008d456a979..8a0cf01b125 100644 --- a/packages/contracts/src/activity-log.model.ts +++ b/packages/contracts/src/activity-log.model.ts @@ -1,6 +1,6 @@ import { ActorTypeEnum, - BaseEntityEnum, + IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID, JsonData @@ -10,9 +10,7 @@ import { IUser } from './user.model'; /** * Interface representing an activity log entry. */ -export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel { - entity: BaseEntityEnum; // Entity / Table name concerned by activity log - entityId: ID; // The ID of the element we are interacting with (a task, an organization, an employee, ...) +export interface IActivityLog extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { action: ActionTypeEnum; actorType?: ActorTypeEnum; description?: string; // A short sentence describing the action performed. (E.g John Doe created this on 22.09.2024) diff --git a/packages/contracts/src/base-entity.model.ts b/packages/contracts/src/base-entity.model.ts index 86f41428ca2..184e1417cd6 100644 --- a/packages/contracts/src/base-entity.model.ts +++ b/packages/contracts/src/base-entity.model.ts @@ -59,6 +59,12 @@ export interface IBasePerTenantAndOrganizationEntityMutationInput extends Partia organization?: Partial; // Allow additional fields from IOrganization } +// Represents a base structure for generic entities, linking their unique ID with their type. +export interface IBasePerEntityType { + entityId: ID; // Unique ID of the entity + entity: BaseEntityEnum; // The type of the entity, defined in BaseEntityEnum enumeration. +} + // Actor type defines if it's User or system performed some action export enum ActorTypeEnum { System = 'System', // System performed the action diff --git a/packages/contracts/src/comment.model.ts b/packages/contracts/src/comment.model.ts index 63d42b4b0d3..0c7616e25cf 100644 --- a/packages/contracts/src/comment.model.ts +++ b/packages/contracts/src/comment.model.ts @@ -1,11 +1,9 @@ -import { ActorTypeEnum, BaseEntityEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { ActorTypeEnum, IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; import { IEmployee } from './employee.model'; import { IOrganizationTeam } from './organization-team.model'; -export interface IComment extends IBasePerTenantAndOrganizationEntityModel { - entity: BaseEntityEnum; - entityId: ID; // Indicate the ID of entity record comment related to +export interface IComment extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { comment: string; creator?: IUser; creatorId?: ID; // The comment's user author ID @@ -22,10 +20,8 @@ export interface IComment extends IBasePerTenantAndOrganizationEntityModel { replies?: IComment[]; } -export interface ICommentCreateInput { +export interface ICommentCreateInput extends IBasePerEntityType { comment: string; - entity: BaseEntityEnum; - entityId: ID; parentId?: ID; members?: IEmployee[]; teams?: IOrganizationTeam[]; diff --git a/packages/contracts/src/favorite.model.ts b/packages/contracts/src/favorite.model.ts index fc4264f6527..f2c3f6d6952 100644 --- a/packages/contracts/src/favorite.model.ts +++ b/packages/contracts/src/favorite.model.ts @@ -1,9 +1,6 @@ -import { BaseEntityEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; import { IEmployeeEntityInput } from './employee.model'; -export interface IFavorite extends IBasePerTenantAndOrganizationEntityModel, IEmployeeEntityInput { - entity: BaseEntityEnum; - entityId: ID; // Indicate the ID of entity record marked as favorite -} +export interface IFavorite extends IBasePerTenantAndOrganizationEntityModel, IEmployeeEntityInput, IBasePerEntityType {} export interface IFavoriteCreateInput extends IFavorite {} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 135d643acbd..1fa9dce1e91 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -115,6 +115,7 @@ export * from './shared-types'; export * from './skill-entity.model'; export * from './sms.model'; export * from './social-account.model'; +export * from './subscription.model'; export * from './tag.model'; export * from './task-estimation.model'; export * from './task-linked-issue.model'; @@ -147,7 +148,8 @@ export { IBaseRelationsEntityModel, ActorTypeEnum, JsonData, - BaseEntityEnum + BaseEntityEnum, + IBasePerEntityType } from './base-entity.model'; export * from './proxy.model'; diff --git a/packages/contracts/src/mention.model.ts b/packages/contracts/src/mention.model.ts index 262036bc3dd..c7345211a74 100644 --- a/packages/contracts/src/mention.model.ts +++ b/packages/contracts/src/mention.model.ts @@ -1,9 +1,7 @@ -import { BaseEntityEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; -export interface IMention extends IBasePerTenantAndOrganizationEntityModel { - entityId: ID; - entity: BaseEntityEnum; +export interface IMention extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { mentionedUserId: ID; mentionedUser?: IUser; mentionById: ID; diff --git a/packages/contracts/src/resource-link.model.ts b/packages/contracts/src/resource-link.model.ts index 1fed06631f9..92525b4d001 100644 --- a/packages/contracts/src/resource-link.model.ts +++ b/packages/contracts/src/resource-link.model.ts @@ -1,10 +1,8 @@ import { IURLMetaData } from './timesheet.model'; -import { BaseEntityEnum, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; -export interface IResourceLink extends IBasePerTenantAndOrganizationEntityModel { - entity: BaseEntityEnum; - entityId: ID; +export interface IResourceLink extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { title: string; url: string; creator?: IUser; diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts new file mode 100644 index 00000000000..84e97720eae --- /dev/null +++ b/packages/contracts/src/subscription.model.ts @@ -0,0 +1,3 @@ +import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; + +export interface ISubscription extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType {} From 354bdf4087cea33221586df22ece0fcdc08fa095 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 06:37:31 +0200 Subject: [PATCH 09/30] feat: add subscription model --- packages/contracts/src/subscription.model.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts index 84e97720eae..bed4c37f4c0 100644 --- a/packages/contracts/src/subscription.model.ts +++ b/packages/contracts/src/subscription.model.ts @@ -1,3 +1,17 @@ -import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; +import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { IUser } from './user.model'; -export interface ISubscription extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType {} +export interface ISubscription extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { + subscriptionType: SubscriptionTypeEnum; + userId: ID; + user?: IUser; +} + +export enum SubscriptionTypeEnum { + MANUAL = 'manual', + MENTION = 'mention', + ASSIGNMENT = 'assignment', + COMMENT = 'comment' +} + +export interface ISubscriptionCreateInput extends Omit {} From 02cc68af5ae5ecccf2cd4078e39e6a455b8bf43f Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 06:48:54 +0200 Subject: [PATCH 10/30] feat: add subscription repositories --- packages/core/src/app.module.ts | 4 +++- packages/core/src/subscription/repositoty/index.ts | 2 ++ .../repositoty/mikro-orm-subscription.repository.ts | 4 ++++ .../repositoty/type-orm-subscription.repository.ts | 11 +++++++++++ packages/core/src/subscription/subscription.entity.ts | 0 packages/core/src/subscription/subscription.module.ts | 4 ++++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/subscription/repositoty/index.ts create mode 100644 packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts create mode 100644 packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts create mode 100644 packages/core/src/subscription/subscription.entity.ts create mode 100644 packages/core/src/subscription/subscription.module.ts diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 96a0a66f2f6..ac6c3d45d38 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -152,6 +152,7 @@ import { ApiCallLogModule } from './api-call-log/api-call-log.module'; // Global import { TaskViewModule } from './tasks/views/view.module'; import { ResourceLinkModule } from './resource-link/resource-link.module'; import { MentionModule } from './mention/mention.module'; +import { SubscriptionModule } from './subscription/subscription.module'; const { unleashConfig } = environment; @@ -453,7 +454,8 @@ if (environment.THROTTLE_ENABLED) { ApiCallLogModule, TaskViewModule, ResourceLinkModule, - MentionModule // Task views Module + MentionModule, + SubscriptionModule // Task views Module ], controllers: [AppController], providers: [ diff --git a/packages/core/src/subscription/repositoty/index.ts b/packages/core/src/subscription/repositoty/index.ts new file mode 100644 index 00000000000..94f6b855a3e --- /dev/null +++ b/packages/core/src/subscription/repositoty/index.ts @@ -0,0 +1,2 @@ +export * from './mikro-orm-subscription.repository'; +export * from './type-orm-subscription.repository'; diff --git a/packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts b/packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts new file mode 100644 index 00000000000..3d7bba88cee --- /dev/null +++ b/packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts @@ -0,0 +1,4 @@ +import { MikroOrmBaseEntityRepository } from '../../core/repository/mikro-orm-base-entity.repository'; +import { Subscription } from '../subscription.entity'; + +export class MikroOrmSubscriptionRepository extends MikroOrmBaseEntityRepository {} diff --git a/packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts b/packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts new file mode 100644 index 00000000000..b0d32e0ebe2 --- /dev/null +++ b/packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Subscription } from '../subscription.entity'; + +@Injectable() +export class TypeOrmSubscriptionRepository extends Repository { + constructor(@InjectRepository(Subscription) readonly repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } +} diff --git a/packages/core/src/subscription/subscription.entity.ts b/packages/core/src/subscription/subscription.entity.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/subscription/subscription.module.ts b/packages/core/src/subscription/subscription.module.ts new file mode 100644 index 00000000000..e43ac92dc18 --- /dev/null +++ b/packages/core/src/subscription/subscription.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class SubscriptionModule {} From f2d92a1b2662e94683ef5efb15b1a968772b0772 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 07:20:42 +0200 Subject: [PATCH 11/30] feat: subscroption entity --- packages/core/src/core/entities/index.ts | 2 + packages/core/src/core/entities/internal.ts | 1 + .../src/subscription/subscription.entity.ts | 61 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/packages/core/src/core/entities/index.ts b/packages/core/src/core/entities/index.ts index 56e37016fea..c04a09ff356 100644 --- a/packages/core/src/core/entities/index.ts +++ b/packages/core/src/core/entities/index.ts @@ -124,6 +124,7 @@ import { Screenshot, Skill, SocialAccount, + Subscription, Tag, Task, TaskEstimation, @@ -275,6 +276,7 @@ export const coreEntities = [ Screenshot, Skill, SocialAccount, + Subscription, Tag, Task, TaskEstimation, diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 7bf7a6dcf92..0fb53b09960 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -126,6 +126,7 @@ export * from '../../resource-link/resource-link.entity'; export * from '../../role-permission/role-permission.entity'; export * from '../../role/role.entity'; export * from '../../skills/skill.entity'; +export * from '../../subscription/subscription.entity'; export * from '../../tags/tag.entity'; export * from '../../tasks/daily-plan/daily-plan.entity'; export * from '../../tasks/estimation/task-estimation.entity'; diff --git a/packages/core/src/subscription/subscription.entity.ts b/packages/core/src/subscription/subscription.entity.ts index e69de29bb2d..c84ea381292 100644 --- a/packages/core/src/subscription/subscription.entity.ts +++ b/packages/core/src/subscription/subscription.entity.ts @@ -0,0 +1,61 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { EntityRepositoryType } from '@mikro-orm/core'; +import { JoinColumn, RelationId } from 'typeorm'; +import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; +import { BaseEntityEnum, ID, ISubscription, IUser, SubscriptionTypeEnum } from '@gauzy/contracts'; +import { TenantOrganizationBaseEntity, User } from '../core/entities/internal'; +import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '../core/decorators/entity'; +import { MikroOrmSubscriptionRepository } from './repositoty/mikro-orm-subscription.repository'; + +@MultiORMEntity('subscription', { mikroOrmRepository: () => MikroOrmSubscriptionRepository }) +export class Subscription extends TenantOrganizationBaseEntity implements ISubscription { + [EntityRepositoryType]?: MikroOrmSubscriptionRepository; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsUUID() + @ColumnIndex() + @MultiORMColumn() + entityId: ID; + + @ApiProperty({ type: () => String, enum: BaseEntityEnum }) + @IsNotEmpty() + @IsEnum(BaseEntityEnum) + @ColumnIndex() + @MultiORMColumn() + entity: BaseEntityEnum; + + @ApiProperty({ type: () => String, enum: SubscriptionTypeEnum }) + @IsNotEmpty() + @IsEnum(SubscriptionTypeEnum) + @ColumnIndex() + @MultiORMColumn() + subscriptionType: SubscriptionTypeEnum; + + /* + |-------------------------------------------------------------------------- + | @ManyToOne + |-------------------------------------------------------------------------- + */ + /** + * Subscribed User + */ + @ApiPropertyOptional({ type: () => Object }) + @MultiORMManyToOne(() => User, { + /** Indicates if relation column value can be nullable or not. */ + nullable: true, + + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) + @JoinColumn() + user?: IUser; + + @ApiProperty({ type: () => String }) + @IsNotEmpty() + @IsUUID() + @RelationId((it: Subscription) => it.user) + @ColumnIndex() + @MultiORMColumn({ relationId: true }) + userId: ID; +} From d2b73dd9d1031b0a6ee2724fbfeb1b5b6d6d4fe6 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 07:36:07 +0200 Subject: [PATCH 12/30] feat: add subscription PostgreSQL migration --- .../1732685369636-CreateSubscriptionTable.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts diff --git a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts new file mode 100644 index 00000000000..9bd1666881e --- /dev/null +++ b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts @@ -0,0 +1,132 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateSubscriptionTable1732685369636 implements MigrationInterface { + name = 'CreateSubscriptionTable1732685369636'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entityId" character varying NOT NULL, "entity" character varying NOT NULL, "subscriptionType" character varying NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_8c3e00ebd02103caa1174cd5d9d" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query( + `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0"`); + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_8ccdfc22892c16950b568145d53"`); + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "public"."IDX_59d935a1de99d140f45550f344"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "public"."IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP TABLE "subscription"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} +} From c1b36005eaa158807f3c137946966c08d2e043a9 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 08:31:59 +0200 Subject: [PATCH 13/30] feat: subscription MySQL and SQLite migrations --- .../1732685369636-CreateSubscriptionTable.ts | 110 +++++++++++++++++- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts index 9bd1666881e..f8ceb200458 100644 --- a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts +++ b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts @@ -107,26 +107,128 @@ export class CreateSubscriptionTable1732685369636 implements MigrationInterface * * @param queryRunner */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL)` + ); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query( + `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query( + `CREATE TABLE "temporary_subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL, CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId" FROM "subscription"` + ); + await queryRunner.query(`DROP TABLE "subscription"`); + await queryRunner.query(`ALTER TABLE "temporary_subscription" RENAME TO "subscription"`); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query( + `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + } /** * SqliteDB and BetterSQlite3DB Down Migration * * @param queryRunner */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`ALTER TABLE "subscription" RENAME TO "temporary_subscription"`); + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL)` + ); + await queryRunner.query( + `INSERT INTO "subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId" FROM "temporary_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_subscription"`); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query( + `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP TABLE "subscription"`); + } /** * MySQL Up Migration * * @param queryRunner */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`subscription\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entityId\` varchar(255) NOT NULL, \`entity\` varchar(255) NOT NULL, \`subscriptionType\` varchar(255) NOT NULL, \`userId\` varchar(255) NOT NULL, INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` (\`isActive\`), INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` (\`isArchived\`), INDEX \`IDX_c86077795cb9a3ce80d19d670a\` (\`tenantId\`), INDEX \`IDX_8ccdfc22892c16950b568145d5\` (\`organizationId\`), INDEX \`IDX_404bc7ad0e4734744372d656fe\` (\`entityId\`), INDEX \`IDX_cb98de07e0868a9951e4d9b353\` (\`entity\`), INDEX \`IDX_59d935a1de99d140f45550f344\` (\`subscriptionType\`), INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` (\`userId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_c86077795cb9a3ce80d19d670a5\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_8ccdfc22892c16950b568145d53\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_cc906b4bc892b048f1b654d2aa0\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } /** * MySQL Down Migration * * @param queryRunner */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_cc906b4bc892b048f1b654d2aa0\``); + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_8ccdfc22892c16950b568145d53\``); + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_c86077795cb9a3ce80d19d670a5\``); + await queryRunner.query(`DROP INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_59d935a1de99d140f45550f344\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_cb98de07e0868a9951e4d9b353\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_404bc7ad0e4734744372d656fe\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_8ccdfc22892c16950b568145d5\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_c86077795cb9a3ce80d19d670a\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` ON \`subscription\``); + await queryRunner.query(`DROP TABLE \`subscription\``); + } } From e9f84d9dab25616a05eedb217d9722c4b39769ad Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 08:38:38 +0200 Subject: [PATCH 14/30] fix: wrong folder naming --- .../core/src/subscription/{repositoty => repository}/index.ts | 0 .../mikro-orm-subscription.repository.ts | 0 .../type-orm-subscription.repository.ts | 0 packages/core/src/subscription/subscription.entity.ts | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/src/subscription/{repositoty => repository}/index.ts (100%) rename packages/core/src/subscription/{repositoty => repository}/mikro-orm-subscription.repository.ts (100%) rename packages/core/src/subscription/{repositoty => repository}/type-orm-subscription.repository.ts (100%) diff --git a/packages/core/src/subscription/repositoty/index.ts b/packages/core/src/subscription/repository/index.ts similarity index 100% rename from packages/core/src/subscription/repositoty/index.ts rename to packages/core/src/subscription/repository/index.ts diff --git a/packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts b/packages/core/src/subscription/repository/mikro-orm-subscription.repository.ts similarity index 100% rename from packages/core/src/subscription/repositoty/mikro-orm-subscription.repository.ts rename to packages/core/src/subscription/repository/mikro-orm-subscription.repository.ts diff --git a/packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts b/packages/core/src/subscription/repository/type-orm-subscription.repository.ts similarity index 100% rename from packages/core/src/subscription/repositoty/type-orm-subscription.repository.ts rename to packages/core/src/subscription/repository/type-orm-subscription.repository.ts diff --git a/packages/core/src/subscription/subscription.entity.ts b/packages/core/src/subscription/subscription.entity.ts index c84ea381292..ceca6daa172 100644 --- a/packages/core/src/subscription/subscription.entity.ts +++ b/packages/core/src/subscription/subscription.entity.ts @@ -5,7 +5,7 @@ import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; import { BaseEntityEnum, ID, ISubscription, IUser, SubscriptionTypeEnum } from '@gauzy/contracts'; import { TenantOrganizationBaseEntity, User } from '../core/entities/internal'; import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from '../core/decorators/entity'; -import { MikroOrmSubscriptionRepository } from './repositoty/mikro-orm-subscription.repository'; +import { MikroOrmSubscriptionRepository } from './repository/mikro-orm-subscription.repository'; @MultiORMEntity('subscription', { mikroOrmRepository: () => MikroOrmSubscriptionRepository }) export class Subscription extends TenantOrganizationBaseEntity implements ISubscription { From c4ab665be5e2d824bd5424f47049b3b4374cfa1b Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 10:24:34 +0200 Subject: [PATCH 15/30] feat: create subscription service, command and event --- packages/contracts/src/subscription.model.ts | 2 +- .../events/handlers/mention.handler.ts | 2 +- .../subscription/commands/handlers/index.ts | 3 ++ .../handlers/subscription.create.handler.ts | 15 ++++++ .../core/src/subscription/commands/index.ts | 1 + .../commands/subscription.create.command.ts | 8 +++ .../src/subscription/events/handlers/index.ts | 0 .../handlers/subscription.create.handler.ts | 21 ++++++++ .../core/src/subscription/events/index.ts | 1 + .../events/subscription.create.event.ts | 6 +++ .../subscription/subscription.controller.ts | 4 ++ .../src/subscription/subscription.module.ts | 7 ++- .../src/subscription/subscription.service.ts | 51 +++++++++++++++++++ 13 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/subscription/commands/handlers/index.ts create mode 100644 packages/core/src/subscription/commands/handlers/subscription.create.handler.ts create mode 100644 packages/core/src/subscription/commands/index.ts create mode 100644 packages/core/src/subscription/commands/subscription.create.command.ts create mode 100644 packages/core/src/subscription/events/handlers/index.ts create mode 100644 packages/core/src/subscription/events/handlers/subscription.create.handler.ts create mode 100644 packages/core/src/subscription/events/index.ts create mode 100644 packages/core/src/subscription/events/subscription.create.event.ts create mode 100644 packages/core/src/subscription/subscription.controller.ts create mode 100644 packages/core/src/subscription/subscription.service.ts diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts index bed4c37f4c0..baf55181d74 100644 --- a/packages/contracts/src/subscription.model.ts +++ b/packages/contracts/src/subscription.model.ts @@ -14,4 +14,4 @@ export enum SubscriptionTypeEnum { COMMENT = 'comment' } -export interface ISubscriptionCreateInput extends Omit {} +export interface ISubscriptionCreateInput extends Omit {} diff --git a/packages/core/src/mention/events/handlers/mention.handler.ts b/packages/core/src/mention/events/handlers/mention.handler.ts index 1e080fbe9b1..16c4e3d61ca 100644 --- a/packages/core/src/mention/events/handlers/mention.handler.ts +++ b/packages/core/src/mention/events/handlers/mention.handler.ts @@ -8,7 +8,7 @@ export class CreateMentionEventHandler implements IEventHandler} A promise that resolves to the newly created mention entry. diff --git a/packages/core/src/subscription/commands/handlers/index.ts b/packages/core/src/subscription/commands/handlers/index.ts new file mode 100644 index 00000000000..1dcc76527e8 --- /dev/null +++ b/packages/core/src/subscription/commands/handlers/index.ts @@ -0,0 +1,3 @@ +import { SubscriptionCreateHandler } from './subscription.create.handler'; + +export const CommandHandlers = [SubscriptionCreateHandler]; diff --git a/packages/core/src/subscription/commands/handlers/subscription.create.handler.ts b/packages/core/src/subscription/commands/handlers/subscription.create.handler.ts new file mode 100644 index 00000000000..103bd62bb39 --- /dev/null +++ b/packages/core/src/subscription/commands/handlers/subscription.create.handler.ts @@ -0,0 +1,15 @@ +import { ISubscription } from '@gauzy/contracts'; +import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { SubscriptionService } from '../../subscription.service'; +import { SubscriptionCreateCommand } from '../subscription.create.command'; + +@CommandHandler(SubscriptionCreateCommand) +export class SubscriptionCreateHandler implements ICommandHandler { + constructor(private readonly subscriptionService: SubscriptionService) {} + + public async execute(command: SubscriptionCreateCommand): Promise { + const { input } = command; + + return await this.subscriptionService.create(input); + } +} diff --git a/packages/core/src/subscription/commands/index.ts b/packages/core/src/subscription/commands/index.ts new file mode 100644 index 00000000000..3411fd63e91 --- /dev/null +++ b/packages/core/src/subscription/commands/index.ts @@ -0,0 +1 @@ +export * from './subscription.create.command'; diff --git a/packages/core/src/subscription/commands/subscription.create.command.ts b/packages/core/src/subscription/commands/subscription.create.command.ts new file mode 100644 index 00000000000..31e9cbcf180 --- /dev/null +++ b/packages/core/src/subscription/commands/subscription.create.command.ts @@ -0,0 +1,8 @@ +import { ISubscriptionCreateInput } from '@gauzy/contracts'; +import { ICommand } from '@nestjs/cqrs'; + +export class SubscriptionCreateCommand implements ICommand { + static readonly type = '[Subscription] Create'; + + constructor(public readonly input: ISubscriptionCreateInput) {} +} diff --git a/packages/core/src/subscription/events/handlers/index.ts b/packages/core/src/subscription/events/handlers/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/src/subscription/events/handlers/subscription.create.handler.ts b/packages/core/src/subscription/events/handlers/subscription.create.handler.ts new file mode 100644 index 00000000000..89842fcb6ac --- /dev/null +++ b/packages/core/src/subscription/events/handlers/subscription.create.handler.ts @@ -0,0 +1,21 @@ +import { CommandBus, EventsHandler, IEventHandler } from '@nestjs/cqrs'; +import { ISubscription } from '@gauzy/contracts'; +import { CreateSubscriptionEvent } from '../subscription.create.event'; +import { SubscriptionCreateCommand } from '../../commands'; + +@EventsHandler(CreateSubscriptionEvent) +export class CreateSubscriptionHandler implements IEventHandler { + constructor(private readonly commandBus: CommandBus) {} + + /** + * Handles the `CreateSubscriptionEvent` by delegating the subscription creation process to the appropriate command. + * + * @param {CreateSubscriptionEvent} event - The event object containing the subscription input data. + * @returns {Promise} A promise that resolves to the created subscription. + * + */ + async handle(event: CreateSubscriptionEvent): Promise { + const { input } = event; + return await this.commandBus.execute(new SubscriptionCreateCommand(input)); + } +} diff --git a/packages/core/src/subscription/events/index.ts b/packages/core/src/subscription/events/index.ts new file mode 100644 index 00000000000..907ceb1aaad --- /dev/null +++ b/packages/core/src/subscription/events/index.ts @@ -0,0 +1 @@ +export * from './subscription.create.event'; diff --git a/packages/core/src/subscription/events/subscription.create.event.ts b/packages/core/src/subscription/events/subscription.create.event.ts new file mode 100644 index 00000000000..7a661d53be3 --- /dev/null +++ b/packages/core/src/subscription/events/subscription.create.event.ts @@ -0,0 +1,6 @@ +import { IEvent } from '@nestjs/cqrs'; +import { ISubscriptionCreateInput } from '@gauzy/contracts'; + +export class CreateSubscriptionEvent implements IEvent { + constructor(public readonly input: ISubscriptionCreateInput) {} +} diff --git a/packages/core/src/subscription/subscription.controller.ts b/packages/core/src/subscription/subscription.controller.ts new file mode 100644 index 00000000000..21ea75d85f5 --- /dev/null +++ b/packages/core/src/subscription/subscription.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('subscription') +export class SubscriptionController {} diff --git a/packages/core/src/subscription/subscription.module.ts b/packages/core/src/subscription/subscription.module.ts index e43ac92dc18..5a19283f10f 100644 --- a/packages/core/src/subscription/subscription.module.ts +++ b/packages/core/src/subscription/subscription.module.ts @@ -1,4 +1,9 @@ import { Module } from '@nestjs/common'; +import { SubscriptionService } from './subscription.service'; +import { SubscriptionController } from './subscription.controller'; -@Module({}) +@Module({ + providers: [SubscriptionService], + controllers: [SubscriptionController] +}) export class SubscriptionModule {} diff --git a/packages/core/src/subscription/subscription.service.ts b/packages/core/src/subscription/subscription.service.ts new file mode 100644 index 00000000000..4b9db8440fe --- /dev/null +++ b/packages/core/src/subscription/subscription.service.ts @@ -0,0 +1,51 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { ISubscription, ISubscriptionCreateInput } from '@gauzy/contracts'; +import { TenantAwareCrudService } from './../core/crud'; +import { RequestContext } from '../core/context'; +import { Subscription } from './subscription.entity'; +import { MikroOrmSubscriptionRepository } from './repository/mikro-orm-subscription.repository'; +import { TypeOrmSubscriptionRepository } from './repository/type-orm-subscription.repository'; + +@Injectable() +export class SubscriptionService extends TenantAwareCrudService { + constructor( + readonly typeOrmSubscriptionRepository: TypeOrmSubscriptionRepository, + readonly mikroOrmSubscriptionRepository: MikroOrmSubscriptionRepository + ) { + super(typeOrmSubscriptionRepository, mikroOrmSubscriptionRepository); + } + + /** + * Creates a new subscription for the specified entity and user. + * + * @param {ISubscriptionCreateInput} input - The input object containing subscription details, including the entity type, entity ID, and optional tenant ID. + * @returns {Promise} A promise resolving to the created subscription, or the existing subscription if it already exists. + * @throws {BadRequestException} Throws a BadRequestException if the subscription creation fails due to an error. + */ + async create(input: ISubscriptionCreateInput): Promise { + try { + const userId = RequestContext.currentUserId(); + const tenantId = RequestContext.currentTenantId() || input.tenantId; + + const { entity, entityId } = input; + + // Check if the subscription already exists + const existingSubscription = await this.findOneByOptions({ where: { userId, entity, entityId } }); + if (existingSubscription) { + return existingSubscription; + } + + // Create a new subscription if none exists + const subscription = await super.create({ ...input, tenantId, userId }); + + /** + * TODO : Optional subscription notification if needed + */ + + return subscription; + } catch (error) { + console.log('Error creating subscription:', error); + throw new BadRequestException('Failed to create subscription', error); + } + } +} From e95a214cb988adf639bc4dd536aab95694d578be Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 12:13:36 +0200 Subject: [PATCH 16/30] feat: add subscription controller --- packages/contracts/src/subscription.model.ts | 6 +- packages/core/src/mention/mention.service.ts | 26 ++++-- .../dto/create-subscription.dto.ts | 11 +++ packages/core/src/subscription/dto/index.ts | 2 + .../dto/subscription-find-input.dto.ts | 5 + .../src/subscription/events/handlers/index.ts | 3 + .../subscription/subscription.controller.ts | 92 ++++++++++++++++++- .../src/subscription/subscription.module.ts | 18 +++- .../src/subscription/subscription.service.ts | 26 +++++- 9 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 packages/core/src/subscription/dto/create-subscription.dto.ts create mode 100644 packages/core/src/subscription/dto/index.ts create mode 100644 packages/core/src/subscription/dto/subscription-find-input.dto.ts diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts index baf55181d74..debf40f7b93 100644 --- a/packages/contracts/src/subscription.model.ts +++ b/packages/contracts/src/subscription.model.ts @@ -14,4 +14,8 @@ export enum SubscriptionTypeEnum { COMMENT = 'comment' } -export interface ISubscriptionCreateInput extends Omit {} +export interface ISubscriptionCreateInput + extends Omit, + Partial> {} + +export interface ISubscriptionFindInput extends Partial {} diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 5815ae4b863..3ee6668d372 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -1,12 +1,13 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; -import { IMention, IMentionCreateInput } from '@gauzy/contracts'; +import { IMention, IMentionCreateInput, SubscriptionTypeEnum } from '@gauzy/contracts'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; import { Mention } from './mention.entity'; import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; import { MikroOrmMentionRepository } from './repository/mikro-orm-mention.repository'; import { CreateMentionEvent } from './events'; +import { CreateSubscriptionEvent } from '../subscription/events'; @Injectable() export class MentionService extends TenantAwareCrudService { @@ -26,21 +27,34 @@ export class MentionService extends TenantAwareCrudService { * @throws {BadRequestException} If an error occurs during the creation process, * a `BadRequestException` is thrown with a descriptive message and the original error. */ - async create(entity: IMentionCreateInput): Promise { + async create(input: IMentionCreateInput): Promise { try { + const { entity, entityId, mentionedUserId, organizationId } = input; + // Retrieve the ID of the currently logged-in user const mentionById = RequestContext.currentUserId(); // Get the tenant ID from the current request context or use the one from the entity - const tenantId = RequestContext.currentTenantId() || entity.tenantId; + const tenantId = RequestContext.currentTenantId() || input.tenantId; // Create the mention entry using the provided input along with the tenantId and mentionById. - const mention = await super.create({ ...entity, tenantId, mentionById }); + const mention = await super.create({ ...input, tenantId, mentionById }); + + // Create an user subscription for provided entity + this._eventBus.publish( + new CreateSubscriptionEvent({ + entity, + entityId, + userId: mentionedUserId, + subscriptionType: SubscriptionTypeEnum.MENTION, + organizationId, + tenantId + }) + ); /** * TODO - * 1. Optional create an user subscription for provided entity - * 2. Send email notifications and trigger internal system notifications for both mention and optional subscription + * 1. Send email notifications and trigger internal system notifications for both mention and optional subscription */ // Return the created mention. diff --git a/packages/core/src/subscription/dto/create-subscription.dto.ts b/packages/core/src/subscription/dto/create-subscription.dto.ts new file mode 100644 index 00000000000..ae7f95440d5 --- /dev/null +++ b/packages/core/src/subscription/dto/create-subscription.dto.ts @@ -0,0 +1,11 @@ +import { IntersectionType, OmitType } from '@nestjs/swagger'; +import { TenantOrganizationBaseDTO } from './../../core/dto'; +import { Subscription } from '../subscription.entity'; +import { ISubscriptionCreateInput } from '@gauzy/contracts'; + +/** + * Create subscription data validation request DTO + */ +export class CreateSubscriptionDTO + extends IntersectionType(TenantOrganizationBaseDTO, OmitType(Subscription, ['userId', 'user'])) + implements ISubscriptionCreateInput {} diff --git a/packages/core/src/subscription/dto/index.ts b/packages/core/src/subscription/dto/index.ts new file mode 100644 index 00000000000..40bfa5b56d1 --- /dev/null +++ b/packages/core/src/subscription/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-subscription.dto'; +export * from './subscription-find-input.dto'; diff --git a/packages/core/src/subscription/dto/subscription-find-input.dto.ts b/packages/core/src/subscription/dto/subscription-find-input.dto.ts new file mode 100644 index 00000000000..df7ad6bb506 --- /dev/null +++ b/packages/core/src/subscription/dto/subscription-find-input.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; +import { ISubscriptionFindInput } from '@gauzy/contracts'; +import { Subscription } from '../subscription.entity'; + +export class SubscriptionFindInputDTO extends PartialType(Subscription) implements ISubscriptionFindInput {} diff --git a/packages/core/src/subscription/events/handlers/index.ts b/packages/core/src/subscription/events/handlers/index.ts index e69de29bb2d..169013bd38a 100644 --- a/packages/core/src/subscription/events/handlers/index.ts +++ b/packages/core/src/subscription/events/handlers/index.ts @@ -0,0 +1,3 @@ +import { CreateSubscriptionHandler } from './subscription.create.handler'; + +export const EventHandlers = [CreateSubscriptionHandler]; diff --git a/packages/core/src/subscription/subscription.controller.ts b/packages/core/src/subscription/subscription.controller.ts index 21ea75d85f5..5da80038fcb 100644 --- a/packages/core/src/subscription/subscription.controller.ts +++ b/packages/core/src/subscription/subscription.controller.ts @@ -1,4 +1,90 @@ -import { Controller } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { DeleteResult } from 'typeorm'; +import { ID, IPagination, ISubscription } from '@gauzy/contracts'; +import { UseValidationPipe, UUIDValidationPipe } from './../shared/pipes'; +import { PermissionGuard, TenantPermissionGuard } from '../shared/guards'; +import { CrudController, OptionParams, PaginationParams } from './../core/crud'; +import { Subscription } from './subscription.entity'; +import { SubscriptionService } from './subscription.service'; +import { SubscriptionCreateCommand } from './commands'; +import { CreateSubscriptionDTO, SubscriptionFindInputDTO } from './dto'; -@Controller('subscription') -export class SubscriptionController {} +@ApiTags('Subscriptions') +@UseGuards(TenantPermissionGuard, PermissionGuard) +@Controller('/subscription') +export class SubscriptionController extends CrudController { + constructor(private readonly subscriptionService: SubscriptionService, private readonly commandBus: CommandBus) { + super(subscriptionService); + } + + @ApiOperation({ + summary: 'Find all subscriptions filtered by type.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Found subscriptions', + type: Subscription + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Record not found' + }) + @Get() + @UseValidationPipe() + async findAll(@Query() params: PaginationParams): Promise> { + return await this.subscriptionService.findAll(params); + } + + @ApiOperation({ summary: 'Find by id' }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Found one record' /*, type: T*/ + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Record not found' + }) + @Get(':id') + async findById( + @Param('id', UUIDValidationPipe) id: ID, + @Query() params: OptionParams + ): Promise { + return this.subscriptionService.findOneByIdString(id, params); + } + + @ApiOperation({ summary: 'Subscribe to an entity' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'The record has been successfully created.' + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid input, The response body may contain clues as to what went wrong' + }) + @HttpCode(HttpStatus.ACCEPTED) + @Post() + @UseValidationPipe({ whitelist: true }) + async create(@Body() entity: CreateSubscriptionDTO): Promise { + return await this.commandBus.execute(new SubscriptionCreateCommand(entity)); + } + + @ApiOperation({ summary: 'Unsubscribe from entity' }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'Subscription was deleted successfully' + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Record not found' + }) + @HttpCode(HttpStatus.ACCEPTED) + @Delete('/:id') + async delete( + @Param('id', UUIDValidationPipe) id: ID, + @Query() options: SubscriptionFindInputDTO + ): Promise { + return await this.subscriptionService.unsubscribe(id, options); + } +} diff --git a/packages/core/src/subscription/subscription.module.ts b/packages/core/src/subscription/subscription.module.ts index 5a19283f10f..f8b6262ded5 100644 --- a/packages/core/src/subscription/subscription.module.ts +++ b/packages/core/src/subscription/subscription.module.ts @@ -1,9 +1,23 @@ +import { CqrsModule } from '@nestjs/cqrs'; import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { RolePermissionModule } from '../role-permission/role-permission.module'; +import { CommandHandlers } from './commands/handlers'; +import { EventHandlers } from './events/handlers'; import { SubscriptionService } from './subscription.service'; import { SubscriptionController } from './subscription.controller'; +import { Subscription } from './subscription.entity'; +import { TypeOrmSubscriptionRepository } from './repository/type-orm-subscription.repository'; @Module({ - providers: [SubscriptionService], - controllers: [SubscriptionController] + imports: [ + TypeOrmModule.forFeature([Subscription]), + MikroOrmModule.forFeature([Subscription]), + RolePermissionModule, + CqrsModule + ], + providers: [SubscriptionService, TypeOrmSubscriptionRepository, ...CommandHandlers, ...EventHandlers], + controllers: [SubscriptionController] }) export class SubscriptionModule {} diff --git a/packages/core/src/subscription/subscription.service.ts b/packages/core/src/subscription/subscription.service.ts index 4b9db8440fe..d09657aa865 100644 --- a/packages/core/src/subscription/subscription.service.ts +++ b/packages/core/src/subscription/subscription.service.ts @@ -1,10 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { ISubscription, ISubscriptionCreateInput } from '@gauzy/contracts'; +import { ID, ISubscription, ISubscriptionCreateInput, ISubscriptionFindInput } from '@gauzy/contracts'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; import { Subscription } from './subscription.entity'; import { MikroOrmSubscriptionRepository } from './repository/mikro-orm-subscription.repository'; import { TypeOrmSubscriptionRepository } from './repository/type-orm-subscription.repository'; +import { DeleteResult } from 'typeorm'; @Injectable() export class SubscriptionService extends TenantAwareCrudService { @@ -24,7 +25,7 @@ export class SubscriptionService extends TenantAwareCrudService { */ async create(input: ISubscriptionCreateInput): Promise { try { - const userId = RequestContext.currentUserId(); + const userId = input.userId || RequestContext.currentUserId(); const tenantId = RequestContext.currentTenantId() || input.tenantId; const { entity, entityId } = input; @@ -48,4 +49,25 @@ export class SubscriptionService extends TenantAwareCrudService { throw new BadRequestException('Failed to create subscription', error); } } + + /** + * Unsubscribes a user from a specific entity by deleting the corresponding subscription. + * + * @param {ID} id - The unique identifier of the subscription to delete. + * @param {ISubscriptionFindInput} options - Additional options to refine the deletion query. + * - `entity`: The type of entity the subscription is associated with (e.g., "project"). + * - `entityId`: The unique identifier of the associated entity. + * @returns {Promise} A promise that resolves to the result of the delete operation. + * + * @throws {BadRequestException} Throws an exception if an error occurs during the unsubscribe process. + */ + async unsubscribe(id: ID, options?: ISubscriptionFindInput): Promise { + try { + const { entity, entityId } = options; + const userId = RequestContext.currentUserId(); + return await super.delete({ id, userId, entity, entityId }); + } catch (error) { + throw new BadRequestException(error); + } + } } From c1385c218a027c0581213013d7f4ecc755b01451 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 27 Nov 2024 12:35:06 +0200 Subject: [PATCH 17/30] fix: improve by coderabbit --- packages/core/src/subscription/subscription.controller.ts | 4 +++- packages/core/src/subscription/subscription.service.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/subscription/subscription.controller.ts b/packages/core/src/subscription/subscription.controller.ts index 5da80038fcb..27f342ded05 100644 --- a/packages/core/src/subscription/subscription.controller.ts +++ b/packages/core/src/subscription/subscription.controller.ts @@ -47,6 +47,7 @@ export class SubscriptionController extends CrudController { description: 'Record not found' }) @Get(':id') + @UseValidationPipe() async findById( @Param('id', UUIDValidationPipe) id: ID, @Query() params: OptionParams @@ -63,7 +64,7 @@ export class SubscriptionController extends CrudController { status: HttpStatus.BAD_REQUEST, description: 'Invalid input, The response body may contain clues as to what went wrong' }) - @HttpCode(HttpStatus.ACCEPTED) + @HttpCode(HttpStatus.CREATED) @Post() @UseValidationPipe({ whitelist: true }) async create(@Body() entity: CreateSubscriptionDTO): Promise { @@ -81,6 +82,7 @@ export class SubscriptionController extends CrudController { }) @HttpCode(HttpStatus.ACCEPTED) @Delete('/:id') + @UseValidationPipe({ whitelist: true }) async delete( @Param('id', UUIDValidationPipe) id: ID, @Query() options: SubscriptionFindInputDTO diff --git a/packages/core/src/subscription/subscription.service.ts b/packages/core/src/subscription/subscription.service.ts index d09657aa865..80457dd91f8 100644 --- a/packages/core/src/subscription/subscription.service.ts +++ b/packages/core/src/subscription/subscription.service.ts @@ -63,11 +63,11 @@ export class SubscriptionService extends TenantAwareCrudService { */ async unsubscribe(id: ID, options?: ISubscriptionFindInput): Promise { try { - const { entity, entityId } = options; + const { entity, entityId } = options || {}; const userId = RequestContext.currentUserId(); return await super.delete({ id, userId, entity, entityId }); } catch (error) { - throw new BadRequestException(error); + throw new BadRequestException('Failed to unsubscribe from entity', error); } } } From f6c4923867633add3386a39d69ee6decd99405b7 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 08:24:17 +0200 Subject: [PATCH 18/30] fix: improve mention entity add parent entity fields --- packages/contracts/src/mention.model.ts | 4 +++- packages/core/src/mention/mention.entity.ts | 21 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/contracts/src/mention.model.ts b/packages/contracts/src/mention.model.ts index c7345211a74..714bccb4fef 100644 --- a/packages/contracts/src/mention.model.ts +++ b/packages/contracts/src/mention.model.ts @@ -1,4 +1,4 @@ -import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; +import { BaseEntityEnum, IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from './base-entity.model'; import { IUser } from './user.model'; export interface IMention extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { @@ -6,6 +6,8 @@ export interface IMention extends IBasePerTenantAndOrganizationEntityModel, IBas mentionedUser?: IUser; mentionById: ID; mentionBy?: IUser; + parentEntityId?: ID; // E.g : If the mention is in a comment, we need this for subscription and notifications purpose (It could be the task ID concerned by comment, then the user will be subscribed to that task instead of to a comment itself) + parentEntityType?: BaseEntityEnum; } export interface IMentionCreateInput extends Omit {} diff --git a/packages/core/src/mention/mention.entity.ts b/packages/core/src/mention/mention.entity.ts index 79eb78b2c23..6fae01a0885 100644 --- a/packages/core/src/mention/mention.entity.ts +++ b/packages/core/src/mention/mention.entity.ts @@ -16,7 +16,7 @@ export class Mention extends TenantOrganizationBaseEntity implements IMention { @IsUUID() @ColumnIndex() @MultiORMColumn() - entityId: string; + entityId: ID; @ApiProperty({ type: () => String, enum: BaseEntityEnum }) @IsNotEmpty() @@ -25,6 +25,25 @@ export class Mention extends TenantOrganizationBaseEntity implements IMention { @MultiORMColumn() entity: BaseEntityEnum; + /** + * The parent entity ID + * + * E.g : If the user was mentioned is in a comment, we need this for subscription and notifications purpose (It could be the `task ID` concerned by comment, then the user will be subscribed to that task instead of to a comment itself because in this case, `entityId` will store the comment ID) + */ + @ApiProperty({ type: () => String }) + @IsOptional() + @IsUUID() + @ColumnIndex() + @MultiORMColumn() + parentEntityId?: ID; + + @ApiProperty({ type: () => String, enum: BaseEntityEnum }) + @IsOptional() + @IsEnum(BaseEntityEnum) + @ColumnIndex() + @MultiORMColumn() + parentEntityType?: BaseEntityEnum; + /* |-------------------------------------------------------------------------- | @ManyToOne From 2a68786c44bd3639ffb507def367ac3ab9ab2285 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 08:35:55 +0200 Subject: [PATCH 19/30] feat: generate Postgre migration --- ...-AtlerMentionTableAddParentEntityFields.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts diff --git a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts new file mode 100644 index 00000000000..2a2510d459f --- /dev/null +++ b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts @@ -0,0 +1,106 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AtlerMentionTableAddParentEntityFields1732775571004 implements MigrationInterface { + name = 'AtlerMentionTableAddParentEntityFields1732775571004'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "mention" ADD "parentEntityId" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "mention" ADD "parentEntityType" character varying NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "public"."IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`ALTER TABLE "mention" DROP COLUMN "parentEntityType"`); + await queryRunner.query(`ALTER TABLE "mention" DROP COLUMN "parentEntityId"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} +} From d53e238a42a032a8e339e67108fb2686bdd3a14b Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 08:48:20 +0200 Subject: [PATCH 20/30] feat: generate MySQL migration --- ...004-AtlerMentionTableAddParentEntityFields.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts index 2a2510d459f..9758fba6f92 100644 --- a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts +++ b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts @@ -95,12 +95,24 @@ export class AtlerMentionTableAddParentEntityFields1732775571004 implements Migr * * @param queryRunner */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`mention\` ADD \`parentEntityId\` varchar(255) NOT NULL`); + await queryRunner.query(`ALTER TABLE \`mention\` ADD \`parentEntityType\` varchar(255) NOT NULL`); + await queryRunner.query(`CREATE INDEX \`IDX_5b95805861f9de5cf7760a964a\` ON \`mention\` (\`parentEntityId\`)`); + await queryRunner.query( + `CREATE INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` ON \`mention\` (\`parentEntityType\`)` + ); + } /** * MySQL Down Migration * * @param queryRunner */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_5b95805861f9de5cf7760a964a\` ON \`mention\``); + await queryRunner.query(`ALTER TABLE \`mention\` DROP COLUMN \`parentEntityType\``); + await queryRunner.query(`ALTER TABLE \`mention\` DROP COLUMN \`parentEntityId\``); + } } From 58a1ba3af2da536f535e7f0d4653350cc71429e1 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 09:03:38 +0200 Subject: [PATCH 21/30] feat: generate SQLite migration --- ...-AtlerMentionTableAddParentEntityFields.ts | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts index 9758fba6f92..11242f74740 100644 --- a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts +++ b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts @@ -81,14 +81,68 @@ export class AtlerMentionTableAddParentEntityFields1732775571004 implements Migr * * @param queryRunner */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query( + `CREATE TABLE "temporary_mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, "parentEntityId" varchar NOT NULL, "parentEntityType" varchar NOT NULL, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "mention"` + ); + await queryRunner.query(`DROP TABLE "mention"`); + await queryRunner.query(`ALTER TABLE "temporary_mention" RENAME TO "mention"`); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + } /** * SqliteDB and BetterSQlite3DB Down Migration * * @param queryRunner */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise {} + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query(`ALTER TABLE "mention" RENAME TO "temporary_mention"`); + await queryRunner.query( + `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "temporary_mention"` + ); + await queryRunner.query(`DROP TABLE "temporary_mention"`); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + } /** * MySQL Up Migration From 80ccb20537b03ac23d31945ae54a77a9c1488e35 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 09:46:20 +0200 Subject: [PATCH 22/30] fix: mention subscription event --- packages/core/src/mention/mention.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 3ee6668d372..b399178ff3d 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -29,7 +29,7 @@ export class MentionService extends TenantAwareCrudService { */ async create(input: IMentionCreateInput): Promise { try { - const { entity, entityId, mentionedUserId, organizationId } = input; + const { entity, entityId, parentEntityId, parentEntityType, mentionedUserId, organizationId } = input; // Retrieve the ID of the currently logged-in user const mentionById = RequestContext.currentUserId(); @@ -43,8 +43,8 @@ export class MentionService extends TenantAwareCrudService { // Create an user subscription for provided entity this._eventBus.publish( new CreateSubscriptionEvent({ - entity, - entityId, + entity: parentEntityType ?? entity, + entityId: parentEntityId ?? entityId, userId: mentionedUserId, subscriptionType: SubscriptionTypeEnum.MENTION, organizationId, From 1e1ad3d8f2fb4065617d9c5ce222c9038336f161 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Thu, 28 Nov 2024 16:56:08 +0200 Subject: [PATCH 23/30] feat: subscription handlers --- packages/contracts/src/base-entity.model.ts | 3 +- packages/contracts/src/comment.model.ts | 5 +- packages/core/src/comment/comment.service.ts | 84 ++++++++++++++++--- .../src/comment/dto/create-comment.dto.ts | 13 ++- .../src/comment/dto/update-comment.dto.ts | 2 +- packages/core/src/mention/mention.module.ts | 8 +- packages/core/src/mention/mention.service.ts | 2 +- .../src/subscription/subscription.service.ts | 17 +++- 8 files changed, 111 insertions(+), 23 deletions(-) diff --git a/packages/contracts/src/base-entity.model.ts b/packages/contracts/src/base-entity.model.ts index 184e1417cd6..ee028edaf2b 100644 --- a/packages/contracts/src/base-entity.model.ts +++ b/packages/contracts/src/base-entity.model.ts @@ -94,5 +94,6 @@ export enum BaseEntityEnum { Task = 'Task', TaskView = 'TaskView', TaskLinkedIssue = 'TaskLinkedIssue', - User = 'User' + User = 'User', + Comment = 'Comment' } diff --git a/packages/contracts/src/comment.model.ts b/packages/contracts/src/comment.model.ts index 0c7616e25cf..b6ae5bb1d9d 100644 --- a/packages/contracts/src/comment.model.ts +++ b/packages/contracts/src/comment.model.ts @@ -25,8 +25,11 @@ export interface ICommentCreateInput extends IBasePerEntityType { parentId?: ID; members?: IEmployee[]; teams?: IOrganizationTeam[]; + mentionIds?: ID[]; } -export interface ICommentUpdateInput extends Partial> {} +export interface ICommentUpdateInput extends Partial> { + mentionIds?: ID[]; +} export interface ICommentFindInput extends Pick {} diff --git a/packages/core/src/comment/comment.service.ts b/packages/core/src/comment/comment.service.ts index d26522c6ce5..04caf8a4355 100644 --- a/packages/core/src/comment/comment.service.ts +++ b/packages/core/src/comment/comment.service.ts @@ -1,9 +1,11 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { UpdateResult } from 'typeorm'; +import { FindOptionsSelect, FindOptionsWhere, In, UpdateResult } from 'typeorm'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; -import { IComment, ICommentCreateInput, ICommentUpdateInput, ID } from '@gauzy/contracts'; +import { BaseEntityEnum, IComment, ICommentCreateInput, ICommentUpdateInput, ID } from '@gauzy/contracts'; import { UserService } from '../user/user.service'; +import { MentionService } from '../mention/mention.service'; +import { Mention } from '../mention/mention.entity'; import { Comment } from './comment.entity'; import { TypeOrmCommentRepository } from './repository/type-orm.comment.repository'; import { MikroOrmCommentRepository } from './repository/mikro-orm-comment.repository'; @@ -13,7 +15,8 @@ export class CommentService extends TenantAwareCrudService { constructor( readonly typeOrmCommentRepository: TypeOrmCommentRepository, readonly mikroOrmCommentRepository: MikroOrmCommentRepository, - private readonly userService: UserService + private readonly userService: UserService, + private readonly mentionService: MentionService ) { super(typeOrmCommentRepository, mikroOrmCommentRepository); } @@ -28,7 +31,7 @@ export class CommentService extends TenantAwareCrudService { try { const userId = RequestContext.currentUserId(); const tenantId = RequestContext.currentTenantId(); - const { ...entity } = input; + const { mentionIds = [], ...data } = input; // Employee existence validation const user = await this.userService.findOneByIdString(userId); @@ -36,12 +39,29 @@ export class CommentService extends TenantAwareCrudService { throw new NotFoundException('User not found'); } - // return created comment - return await super.create({ - ...entity, + // create comment + const comment = await super.create({ + ...data, tenantId, creatorId: user.id }); + + // Apply mentions if needed + await Promise.all( + mentionIds.map((mentionedUserId) => + this.mentionService.publishMention({ + entity: BaseEntityEnum.Comment, + entityId: comment.id, + mentionedUserId, + mentionById: user.id, + parentEntityId: comment.entityId, + parentEntityType: comment.entity + }) + ) + ); + + // Return created Comment + return comment; } catch (error) { console.log(error); // Debug Logging throw new BadRequestException('Comment post failed', error); @@ -50,12 +70,15 @@ export class CommentService extends TenantAwareCrudService { /** * @description Update comment - Note - * @param {ICommentUpdateInput} input - Data to update comment - * @returns A promise that resolves to the updated comment OR Update result + * @param id - The comment ID to be updated. + * @param {ICommentUpdateInput} input - Data to update comment. + * @returns A promise that resolves to the updated comment OR Update result. * @memberof CommentService */ async update(id: ID, input: ICommentUpdateInput): Promise { try { + const { mentionIds } = input; + const userId = RequestContext.currentUserId(); const comment = await this.findOneByOptions({ where: { @@ -68,10 +91,51 @@ export class CommentService extends TenantAwareCrudService { throw new BadRequestException('Comment not found'); } - return await super.create({ + const updatedComment = await super.create({ ...input, id }); + + // Where condition for searching mentions + const where: FindOptionsWhere = { + entity: BaseEntityEnum.Comment, + entityId: id + }; + + // Select option for selecting mentions' fields + const select: FindOptionsSelect = { + mentionedUserId: true + }; + + const commentMentions = await this.mentionService.find({ where, select }); + + // Extract existing mentioned users in comment + const existingMentionUserIds = new Set(commentMentions.map((mention) => mention.mentionedUserId)); + + const mentionsToAdd = mentionIds.filter((id) => !existingMentionUserIds.has(id)); + const mentionsToRemove = [...existingMentionUserIds].filter((id) => !mentionIds.includes(id)); + + // Add mentions + if (mentionsToAdd.length > 0) { + await Promise.all( + mentionsToAdd.map((mentionedUserId) => + this.mentionService.publishMention({ + entity: BaseEntityEnum.Comment, + entityId: updatedComment.id, + mentionedUserId, + mentionById: userId, + parentEntityId: updatedComment.entityId, + parentEntityType: updatedComment.entity + }) + ) + ); + } + // Delete unused mentions + if (mentionsToRemove.length > 0) { + await this.mentionService.delete({ mentionedUserId: In(mentionsToRemove), ...where }); + } + + return updatedComment; } catch (error) { console.log(error); // Debug Logging throw new BadRequestException('Comment update failed', error); diff --git a/packages/core/src/comment/dto/create-comment.dto.ts b/packages/core/src/comment/dto/create-comment.dto.ts index 42344bbe2fe..a8144f0a5f8 100644 --- a/packages/core/src/comment/dto/create-comment.dto.ts +++ b/packages/core/src/comment/dto/create-comment.dto.ts @@ -1,6 +1,7 @@ -import { IntersectionType, OmitType } from '@nestjs/swagger'; +import { ApiPropertyOptional, IntersectionType, OmitType } from '@nestjs/swagger'; +import { IsArray, IsOptional } from 'class-validator'; +import { ICommentCreateInput, ID } from '@gauzy/contracts'; import { TenantOrganizationBaseDTO } from './../../core/dto'; -import { ICommentCreateInput } from '@gauzy/contracts'; import { Comment } from '../comment.entity'; /** @@ -8,4 +9,10 @@ import { Comment } from '../comment.entity'; */ export class CreateCommentDTO extends IntersectionType(TenantOrganizationBaseDTO, OmitType(Comment, ['creatorId', 'creator'])) - implements ICommentCreateInput {} + implements ICommentCreateInput +{ + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + mentionIds?: ID[]; +} diff --git a/packages/core/src/comment/dto/update-comment.dto.ts b/packages/core/src/comment/dto/update-comment.dto.ts index 11fe265730a..f2732a328ce 100644 --- a/packages/core/src/comment/dto/update-comment.dto.ts +++ b/packages/core/src/comment/dto/update-comment.dto.ts @@ -3,7 +3,7 @@ import { ICommentUpdateInput } from '@gauzy/contracts'; import { CreateCommentDTO } from './create-comment.dto'; /** - * Create Comment data validation request DTO + * Update Comment data validation request DTO */ export class UpdateCommentDTO extends PartialType(OmitType(CreateCommentDTO, ['entity', 'entityId'])) diff --git a/packages/core/src/mention/mention.module.ts b/packages/core/src/mention/mention.module.ts index ebde5a69abb..7bcb6196955 100644 --- a/packages/core/src/mention/mention.module.ts +++ b/packages/core/src/mention/mention.module.ts @@ -1,19 +1,23 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RolePermissionModule } from '../role-permission/role-permission.module'; import { MentionService } from './mention.service'; +import { SubscriptionModule } from '../subscription/subscription.module'; import { MentionController } from './mention.controller'; import { Mention } from './mention.entity'; import { EventHandlers } from './events/handlers'; import { TypeOrmMentionRepository } from './repository/type-orm-mention.repository'; + +@Global() @Module({ imports: [ TypeOrmModule.forFeature([Mention]), MikroOrmModule.forFeature([Mention]), CqrsModule, - RolePermissionModule + RolePermissionModule, + SubscriptionModule ], providers: [MentionService, TypeOrmMentionRepository, ...EventHandlers], controllers: [MentionController], diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index b399178ff3d..8bedb6cca54 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -22,7 +22,7 @@ export class MentionService extends TenantAwareCrudService { /** * Creates a new mention entity in the database. * - * @param {IMentionCreateInput} entity - The data required to create a new mention entity. + * @param {IMentionCreateInput} input - The data required to create a new mention entity. * @returns {Promise} A promise that resolves to the newly created mention entity. * @throws {BadRequestException} If an error occurs during the creation process, * a `BadRequestException` is thrown with a descriptive message and the original error. diff --git a/packages/core/src/subscription/subscription.service.ts b/packages/core/src/subscription/subscription.service.ts index 80457dd91f8..e3af9ad9bb6 100644 --- a/packages/core/src/subscription/subscription.service.ts +++ b/packages/core/src/subscription/subscription.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { ID, ISubscription, ISubscriptionCreateInput, ISubscriptionFindInput } from '@gauzy/contracts'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; @@ -31,9 +31,18 @@ export class SubscriptionService extends TenantAwareCrudService { const { entity, entityId } = input; // Check if the subscription already exists - const existingSubscription = await this.findOneByOptions({ where: { userId, entity, entityId } }); - if (existingSubscription) { - return existingSubscription; + try { + const existingSubscription = await this.findOneByOptions({ + where: { userId, entity, entityId, tenantId } + }); + if (existingSubscription) { + return existingSubscription; + } + } catch (e) { + // If NotFoundException, continue with subscription creation + if (!(e instanceof NotFoundException)) { + throw e; + } } // Create a new subscription if none exists From c362bd879ab076534238efaf734e44ff10c2fc60 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Fri, 29 Nov 2024 08:34:35 +0200 Subject: [PATCH 24/30] feat: mention service improve update operation --- packages/contracts/src/comment.model.ts | 10 +-- packages/contracts/src/mention.model.ts | 4 ++ packages/contracts/src/task.model.ts | 3 +- packages/core/src/comment/comment.service.ts | 51 +++----------- .../src/comment/dto/create-comment.dto.ts | 19 +++-- packages/core/src/mention/dto/index.ts | 1 + .../src/mention/dto/mentioned-user-ids.dto.ts | 10 +++ packages/core/src/mention/mention.service.ts | 70 ++++++++++++++++++- .../core/src/tasks/dto/create-task.dto.ts | 22 +++--- 9 files changed, 120 insertions(+), 70 deletions(-) create mode 100644 packages/core/src/mention/dto/index.ts create mode 100644 packages/core/src/mention/dto/mentioned-user-ids.dto.ts diff --git a/packages/contracts/src/comment.model.ts b/packages/contracts/src/comment.model.ts index b6ae5bb1d9d..2c94b499c2e 100644 --- a/packages/contracts/src/comment.model.ts +++ b/packages/contracts/src/comment.model.ts @@ -2,6 +2,7 @@ import { ActorTypeEnum, IBasePerEntityType, IBasePerTenantAndOrganizationEntityM import { IUser } from './user.model'; import { IEmployee } from './employee.model'; import { IOrganizationTeam } from './organization-team.model'; +import { IMentionedUserIds } from './mention.model'; export interface IComment extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { comment: string; @@ -20,16 +21,15 @@ export interface IComment extends IBasePerTenantAndOrganizationEntityModel, IBas replies?: IComment[]; } -export interface ICommentCreateInput extends IBasePerEntityType { +export interface ICommentCreateInput extends IBasePerEntityType, IMentionedUserIds { comment: string; parentId?: ID; members?: IEmployee[]; teams?: IOrganizationTeam[]; - mentionIds?: ID[]; } -export interface ICommentUpdateInput extends Partial> { - mentionIds?: ID[]; -} +export interface ICommentUpdateInput + extends IMentionedUserIds, + Partial> {} export interface ICommentFindInput extends Pick {} diff --git a/packages/contracts/src/mention.model.ts b/packages/contracts/src/mention.model.ts index 714bccb4fef..5f3fb6d176f 100644 --- a/packages/contracts/src/mention.model.ts +++ b/packages/contracts/src/mention.model.ts @@ -11,3 +11,7 @@ export interface IMention extends IBasePerTenantAndOrganizationEntityModel, IBas } export interface IMentionCreateInput extends Omit {} + +export interface IMentionedUserIds { + mentionIds?: ID[]; +} diff --git a/packages/contracts/src/task.model.ts b/packages/contracts/src/task.model.ts index cd66590a0e4..75471c0a505 100644 --- a/packages/contracts/src/task.model.ts +++ b/packages/contracts/src/task.model.ts @@ -15,6 +15,7 @@ import { ITaskPriority, TaskPriorityEnum } from './task-priority.model'; import { ITaskSize, TaskSizeEnum } from './task-size.model'; import { IOrganizationProjectModule } from './organization-project-module.model'; import { TaskTypeEnum } from './issue-type.model'; +import { IMentionedUserIds } from './mention.model'; export interface ITask extends IBasePerTenantAndOrganizationEntityModel, @@ -75,7 +76,7 @@ export enum TaskParticipantEnum { TEAMS = 'teams' } -export type ITaskCreateInput = ITask; +export interface ITaskCreateInput extends ITask, IMentionedUserIds {} export interface ITaskUpdateInput extends ITaskCreateInput { id?: string; diff --git a/packages/core/src/comment/comment.service.ts b/packages/core/src/comment/comment.service.ts index 04caf8a4355..9560979fd9a 100644 --- a/packages/core/src/comment/comment.service.ts +++ b/packages/core/src/comment/comment.service.ts @@ -1,11 +1,10 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { FindOptionsSelect, FindOptionsWhere, In, UpdateResult } from 'typeorm'; +import { UpdateResult } from 'typeorm'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; import { BaseEntityEnum, IComment, ICommentCreateInput, ICommentUpdateInput, ID } from '@gauzy/contracts'; import { UserService } from '../user/user.service'; import { MentionService } from '../mention/mention.service'; -import { Mention } from '../mention/mention.entity'; import { Comment } from './comment.entity'; import { TypeOrmCommentRepository } from './repository/type-orm.comment.repository'; import { MikroOrmCommentRepository } from './repository/mikro-orm-comment.repository'; @@ -77,7 +76,7 @@ export class CommentService extends TenantAwareCrudService { */ async update(id: ID, input: ICommentUpdateInput): Promise { try { - const { mentionIds } = input; + const { mentionIds = [] } = input; const userId = RequestContext.currentUserId(); const comment = await this.findOneByOptions({ @@ -96,44 +95,14 @@ export class CommentService extends TenantAwareCrudService { id }); - // Where condition for searching mentions - const where: FindOptionsWhere = { - entity: BaseEntityEnum.Comment, - entityId: id - }; - - // Select option for selecting mentions' fields - const select: FindOptionsSelect = { - mentionedUserId: true - }; - - const commentMentions = await this.mentionService.find({ where, select }); - - // Extract existing mentioned users in comment - const existingMentionUserIds = new Set(commentMentions.map((mention) => mention.mentionedUserId)); - - const mentionsToAdd = mentionIds.filter((id) => !existingMentionUserIds.has(id)); - const mentionsToRemove = [...existingMentionUserIds].filter((id) => !mentionIds.includes(id)); - - // Add mentions - if (mentionsToAdd.length > 0) { - await Promise.all( - mentionsToAdd.map((mentionedUserId) => - this.mentionService.publishMention({ - entity: BaseEntityEnum.Comment, - entityId: updatedComment.id, - mentionedUserId, - mentionById: userId, - parentEntityId: updatedComment.entityId, - parentEntityType: updatedComment.entity - }) - ) - ); - } - // Delete unused mentions - if (mentionsToRemove.length > 0) { - await this.mentionService.delete({ mentionedUserId: In(mentionsToRemove), ...where }); - } + // Synchronize mentions + await this.mentionService.updateEntityMentions( + BaseEntityEnum.Comment, + id, + mentionIds, + updatedComment.entityId, + updatedComment.entity + ); return updatedComment; } catch (error) { diff --git a/packages/core/src/comment/dto/create-comment.dto.ts b/packages/core/src/comment/dto/create-comment.dto.ts index a8144f0a5f8..615ae6c5220 100644 --- a/packages/core/src/comment/dto/create-comment.dto.ts +++ b/packages/core/src/comment/dto/create-comment.dto.ts @@ -1,18 +1,15 @@ -import { ApiPropertyOptional, IntersectionType, OmitType } from '@nestjs/swagger'; -import { IsArray, IsOptional } from 'class-validator'; -import { ICommentCreateInput, ID } from '@gauzy/contracts'; +import { IntersectionType, OmitType } from '@nestjs/swagger'; +import { ICommentCreateInput } from '@gauzy/contracts'; import { TenantOrganizationBaseDTO } from './../../core/dto'; +import { MentionedUserIdsDTO } from '../../mention/dto'; import { Comment } from '../comment.entity'; /** * Create Comment data validation request DTO */ export class CreateCommentDTO - extends IntersectionType(TenantOrganizationBaseDTO, OmitType(Comment, ['creatorId', 'creator'])) - implements ICommentCreateInput -{ - @ApiPropertyOptional({ type: () => Array }) - @IsOptional() - @IsArray() - mentionIds?: ID[]; -} + extends IntersectionType( + TenantOrganizationBaseDTO, + IntersectionType(OmitType(Comment, ['creatorId', 'creator']), MentionedUserIdsDTO) + ) + implements ICommentCreateInput {} diff --git a/packages/core/src/mention/dto/index.ts b/packages/core/src/mention/dto/index.ts new file mode 100644 index 00000000000..40c18c3cc21 --- /dev/null +++ b/packages/core/src/mention/dto/index.ts @@ -0,0 +1 @@ +export * from './mentioned-user-ids.dto'; diff --git a/packages/core/src/mention/dto/mentioned-user-ids.dto.ts b/packages/core/src/mention/dto/mentioned-user-ids.dto.ts new file mode 100644 index 00000000000..1000bbd63a4 --- /dev/null +++ b/packages/core/src/mention/dto/mentioned-user-ids.dto.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsOptional } from 'class-validator'; +import { ID, IMentionedUserIds } from '@gauzy/contracts'; + +export class MentionedUserIdsDTO implements IMentionedUserIds { + @ApiPropertyOptional({ type: () => Array }) + @IsOptional() + @IsArray() + mentionIds?: ID[]; +} diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index 8bedb6cca54..d5b84d53bb4 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; -import { IMention, IMentionCreateInput, SubscriptionTypeEnum } from '@gauzy/contracts'; +import { In } from 'typeorm'; +import { BaseEntityEnum, ID, IMention, IMentionCreateInput, SubscriptionTypeEnum } from '@gauzy/contracts'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; import { Mention } from './mention.entity'; @@ -74,4 +75,71 @@ export class MentionService extends TenantAwareCrudService { publishMention(input: IMentionCreateInput) { this._eventBus.publish(new CreateMentionEvent(input)); } + + /** + * Synchronize mentions for a given entity. + * + * This method handles adding new mentions and removing outdated mentions + * for an entity (e.g., comments, tasks, or projects). It ensures that only + * the specified user mentions (`newMentionIds`) are associated with the entity. + * + * @param entity - The type of entity being updated (e.g., Comment, Task, etc.). + * @param entityId - The ID of the entity being updated. + * @param mentionIds - Array of user IDs to be mentioned in this entity. + * @param parentEntityId - (Optional) The ID of the parent entity, if applicable. + * @param parentEntityType - (Optional) The type of the parent entity, if applicable. + */ + async updateEntityMentions( + entity: BaseEntityEnum, + entityId: ID, + mentionsIds: ID[], + parentEntityId?: ID, + parentEntityType?: BaseEntityEnum + ): Promise { + try { + const actorId = RequestContext.currentUserId(); + + // Retrieve existing mentions for the entity + const existingMentions = await this.find({ + where: { entity, entityId }, + select: ['mentionedUserId'] + }); + + // Extract the IDs of currently mentioned users + const existingMentionUserIds = new Set(existingMentions.map((mention) => mention.mentionedUserId)); + + // Determine mentions to add (not present in existing mentions) + const mentionsToAdd = mentionsIds.filter((id) => !existingMentionUserIds.has(id)); + + // Determine mentions to remove (present in existing mentions but not in mentionsIds) + const mentionsToRemove = [...existingMentionUserIds].filter((id) => !mentionsIds.includes(id)); + + // Add new mentions + if (mentionsToAdd.length > 0) { + await Promise.all( + mentionsToAdd.map((mentionedUserId) => + this.publishMention({ + entity: entity, + entityId, + mentionedUserId, + mentionById: actorId, + parentEntityId, + parentEntityType + }) + ) + ); + } + + // Remove outdated mentions + if (mentionsToRemove.length > 0) { + await this.delete({ + mentionedUserId: In(mentionsToRemove), + entity, + entityId + }); + } + } catch (error) { + console.log(error); + } + } } diff --git a/packages/core/src/tasks/dto/create-task.dto.ts b/packages/core/src/tasks/dto/create-task.dto.ts index e4c23ca5592..a3738bb9686 100644 --- a/packages/core/src/tasks/dto/create-task.dto.ts +++ b/packages/core/src/tasks/dto/create-task.dto.ts @@ -1,15 +1,15 @@ -import { ITaskCreateInput } from "@gauzy/contracts"; -import { IntersectionType, OmitType } from "@nestjs/swagger"; -import { TenantOrganizationBaseDTO } from "./../../core/dto"; -import { Task } from "./../task.entity"; +import { ITaskCreateInput } from '@gauzy/contracts'; +import { IntersectionType, OmitType } from '@nestjs/swagger'; +import { TenantOrganizationBaseDTO } from './../../core/dto'; +import { MentionedUserIdsDTO } from '../../mention/dto'; +import { Task } from './../task.entity'; /** * Create task validation request DTO */ -export class CreateTaskDTO extends IntersectionType( - TenantOrganizationBaseDTO, - OmitType( - Task, - ['organizationId', 'organization'] - ) -) implements ITaskCreateInput { } +export class CreateTaskDTO + extends IntersectionType( + TenantOrganizationBaseDTO, + IntersectionType(OmitType(Task, ['organizationId', 'organization']), MentionedUserIdsDTO) + ) + implements ITaskCreateInput {} From 79abcddf08df372a252bff5a5c4bf1973e2177ad Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Fri, 29 Nov 2024 09:06:48 +0200 Subject: [PATCH 25/30] feat: task mentions and subscription --- packages/contracts/src/subscription.model.ts | 3 +- .../commands/handlers/task-create.handler.ts | 48 +++++++++++++++---- packages/core/src/tasks/task.service.ts | 20 +++++--- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts index debf40f7b93..5a9b32dc1fc 100644 --- a/packages/contracts/src/subscription.model.ts +++ b/packages/contracts/src/subscription.model.ts @@ -11,7 +11,8 @@ export enum SubscriptionTypeEnum { MANUAL = 'manual', MENTION = 'mention', ASSIGNMENT = 'assignment', - COMMENT = 'comment' + COMMENT = 'comment', + CREATED_ENTITY = 'created-entity' } export interface ISubscriptionCreateInput diff --git a/packages/core/src/tasks/commands/handlers/task-create.handler.ts b/packages/core/src/tasks/commands/handlers/task-create.handler.ts index 494f14a5d11..0ae87e2f501 100644 --- a/packages/core/src/tasks/commands/handlers/task-create.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-create.handler.ts @@ -1,14 +1,16 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; +import { CommandHandler, ICommandHandler, EventBus as CqrsEventBus } from '@nestjs/cqrs'; import { HttpException, HttpStatus, Logger } from '@nestjs/common'; -import { BaseEntityEnum, ActorTypeEnum, ITask, ActionTypeEnum } from '@gauzy/contracts'; +import { BaseEntityEnum, ActorTypeEnum, ITask, ActionTypeEnum, SubscriptionTypeEnum } from '@gauzy/contracts'; import { EventBus } from '../../../event-bus'; import { TaskEvent } from '../../../event-bus/events'; import { BaseEntityEventTypeEnum } from '../../../event-bus/base-entity-event'; import { RequestContext } from './../../../core/context'; import { OrganizationProjectService } from './../../../organization-project/organization-project.service'; +import { CreateSubscriptionEvent } from '../../../subscription/events'; import { TaskCreateCommand } from './../task-create.command'; import { TaskService } from '../../task.service'; import { Task } from './../../task.entity'; +import { MentionService } from '../../../mention/mention.service'; import { ActivityLogService } from '../../../activity-log/activity-log.service'; @CommandHandler(TaskCreateCommand) @@ -17,8 +19,10 @@ export class TaskCreateHandler implements ICommandHandler { constructor( private readonly _eventBus: EventBus, + private readonly _cqrsEventBus: CqrsEventBus, private readonly _taskService: TaskService, private readonly _organizationProjectService: OrganizationProjectService, + private readonly mentionService: MentionService, private readonly activityLogService: ActivityLogService ) {} @@ -32,16 +36,16 @@ export class TaskCreateHandler implements ICommandHandler { try { // Destructure input and triggered event flag from the command const { input, triggeredEvent } = command; - const { organizationId } = input; + const { organizationId, mentionIds = [], ...data } = input; // Retrieve current tenant ID from request context or use input tenant ID - const tenantId = RequestContext.currentTenantId() ?? input.tenantId; + const tenantId = RequestContext.currentTenantId() ?? data.tenantId; // Check if projectId is provided, if not use the provided project object from the input. // If neither is provided, set project to null. - const project = input.projectId - ? await this._organizationProjectService.findOneByIdString(input.projectId) - : input.project || null; + const project = data.projectId + ? await this._organizationProjectService.findOneByIdString(data.projectId) + : data.project || null; // Check if project exists and extract the project prefix (first 3 characters of the project name) const projectPrefix = project?.name?.substring(0, 3) ?? null; @@ -60,7 +64,7 @@ export class TaskCreateHandler implements ICommandHandler { // Create the task with incremented number, project prefix, and other task details const task = await this._taskService.create({ - ...input, // Spread the input properties + ...data, // Spread the input properties number: maxNumber + 1, // Increment the task number prefix: projectPrefix, // Use the project prefix, or null if no project tenantId, // Pass the tenant ID @@ -70,9 +74,35 @@ export class TaskCreateHandler implements ICommandHandler { // Publish a task created event if triggeredEvent flag is set if (triggeredEvent) { const ctx = RequestContext.currentRequestContext(); // Get current request context; - this._eventBus.publish(new TaskEvent(ctx, task, BaseEntityEventTypeEnum.CREATED, input)); // Publish the event using EventBus + this._eventBus.publish(new TaskEvent(ctx, task, BaseEntityEventTypeEnum.CREATED, data)); // Publish the event using EventBus } + // Apply mentions if needed + if (mentionIds.length > 0) { + await Promise.all( + mentionIds.map((mentionedUserId) => + this.mentionService.publishMention({ + entity: BaseEntityEnum.Task, + entityId: task.id, + mentionedUserId, + mentionById: task.creatorId + }) + ) + ); + } + + // Subscribe creator to the task + this._cqrsEventBus.publish( + new CreateSubscriptionEvent({ + entity: BaseEntityEnum.Task, + entityId: task.id, + userId: task.creatorId, + subscriptionType: SubscriptionTypeEnum.CREATED_ENTITY, + organizationId, + tenantId + }) + ); + // Generate the activity log this.activityLogService.logActivity( BaseEntityEnum.Task, diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index e34de4467b8..f4fa2772652 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -31,6 +31,7 @@ import { PaginationParams, TenantAwareCrudService } from './../core/crud'; import { addBetween } from './../core/util'; import { RequestContext } from '../core/context'; import { TaskViewService } from './views/view.service'; +import { MentionService } from '../mention/mention.service'; import { ActivityLogService } from '../activity-log/activity-log.service'; import { Task } from './task.entity'; import { TypeOrmOrganizationSprintTaskHistoryRepository } from './../organization-sprint/repository/type-orm-organization-sprint-task-history.repository'; @@ -46,6 +47,7 @@ export class TaskService extends TenantAwareCrudService { readonly mikroOrmTaskRepository: MikroOrmTaskRepository, readonly typeOrmOrganizationSprintTaskHistoryRepository: TypeOrmOrganizationSprintTaskHistoryRepository, private readonly taskViewService: TaskViewService, + private readonly mentionService: MentionService, private readonly activityLogService: ActivityLogService ) { super(typeOrmTaskRepository, mikroOrmTaskRepository); @@ -62,14 +64,14 @@ export class TaskService extends TenantAwareCrudService { try { const tenantId = RequestContext.currentTenantId() || input.tenantId; const userId = RequestContext.currentUserId(); - const { organizationSprintId } = input; + const { mentionIds, ...data } = input; // Find task relations const relations = this.typeOrmTaskRepository.metadata.relations.map((relation) => relation.propertyName); const task = await this.findOneByIdString(id, { relations }); - if (input.projectId && input.projectId !== task.projectId) { + if (data.projectId && data.projectId !== task.projectId) { const { organizationId, projectId } = task; // Get the maximum task number for the project @@ -88,23 +90,29 @@ export class TaskService extends TenantAwareCrudService { // Update the task with the provided data const updatedTask = await super.create({ - ...input, + ...data, id }); // Register Task Sprint moving history + const { organizationSprintId } = data; if (organizationSprintId && organizationSprintId !== task.organizationSprintId) { await this.typeOrmOrganizationSprintTaskHistoryRepository.save({ fromSprintId: task.organizationSprintId || organizationSprintId, // Use incoming sprint ID if the task's organizationSprintId was previously null or undefined toSprintId: organizationSprintId, taskId: updatedTask.id, movedById: userId, - reason: input.taskSprintMoveReason, - organizationId: input.organizationId, + reason: data.taskSprintMoveReason, + organizationId: data.organizationId, tenantId }); } + // Synchronize mentions + if (data.description) { + await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionIds); + } + // Generate the activity log const { organizationId } = updatedTask; this.activityLogService.logActivity( @@ -117,7 +125,7 @@ export class TaskService extends TenantAwareCrudService { organizationId, tenantId, task, - input + data ); // Return the updated Task From b7203db9846b49b69e0e85b3eafe7869c5fd538f Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Fri, 29 Nov 2024 09:14:22 +0200 Subject: [PATCH 26/30] fix: hint for future TODO improve --- .../core/src/tasks/commands/handlers/task-create.handler.ts | 2 ++ packages/core/src/tasks/task.service.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/core/src/tasks/commands/handlers/task-create.handler.ts b/packages/core/src/tasks/commands/handlers/task-create.handler.ts index 0ae87e2f501..9d54ae09b49 100644 --- a/packages/core/src/tasks/commands/handlers/task-create.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-create.handler.ts @@ -103,6 +103,8 @@ export class TaskCreateHandler implements ICommandHandler { }) ); + // TODO : Subscribe assignees + // Generate the activity log this.activityLogService.logActivity( BaseEntityEnum.Task, diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index f4fa2772652..4bb34d62198 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -113,6 +113,8 @@ export class TaskService extends TenantAwareCrudService { await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionIds); } + // TODO : Subscribe assignees + // Generate the activity log const { organizationId } = updatedTask; this.activityLogService.logActivity( @@ -624,6 +626,8 @@ export class TaskService extends TenantAwareCrudService { } }); + // TODO : Unsubscribe employee from task + // Save updated entities to DB await this.typeOrmRepository.save(tasks); } catch (error) { From 04e448c7cad11b98d879313db3c18e4c20645d9e Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Sat, 30 Nov 2024 11:37:54 +0200 Subject: [PATCH 27/30] fix: subscribe users after comment --- packages/core/src/comment/comment.service.ts | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/core/src/comment/comment.service.ts b/packages/core/src/comment/comment.service.ts index 9560979fd9a..3ca1d046b56 100644 --- a/packages/core/src/comment/comment.service.ts +++ b/packages/core/src/comment/comment.service.ts @@ -1,8 +1,17 @@ +import { EventBus } from '@nestjs/cqrs'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { UpdateResult } from 'typeorm'; import { TenantAwareCrudService } from './../core/crud'; import { RequestContext } from '../core/context'; -import { BaseEntityEnum, IComment, ICommentCreateInput, ICommentUpdateInput, ID } from '@gauzy/contracts'; +import { + BaseEntityEnum, + IComment, + ICommentCreateInput, + ICommentUpdateInput, + ID, + SubscriptionTypeEnum +} from '@gauzy/contracts'; +import { CreateSubscriptionEvent } from '../subscription/events'; import { UserService } from '../user/user.service'; import { MentionService } from '../mention/mention.service'; import { Comment } from './comment.entity'; @@ -14,6 +23,7 @@ export class CommentService extends TenantAwareCrudService { constructor( readonly typeOrmCommentRepository: TypeOrmCommentRepository, readonly mikroOrmCommentRepository: MikroOrmCommentRepository, + private readonly _eventBus: EventBus, private readonly userService: UserService, private readonly mentionService: MentionService ) { @@ -59,6 +69,18 @@ export class CommentService extends TenantAwareCrudService { ) ); + // Subscribe creator to the entity + this._eventBus.publish( + new CreateSubscriptionEvent({ + entity: input.entity, + entityId: input.entityId, + userId: user.id, + subscriptionType: SubscriptionTypeEnum.COMMENT, + organizationId: comment.organizationId, + tenantId + }) + ); + // Return created Comment return comment; } catch (error) { From 8bfc819b1450c439b586dcb9fdd9505fb03f6f45 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 3 Dec 2024 12:24:49 +0200 Subject: [PATCH 28/30] fix: enhance code and naming --- packages/contracts/src/comment.model.ts | 6 +- packages/contracts/src/mention.model.ts | 4 +- packages/contracts/src/subscription.model.ts | 2 +- packages/contracts/src/task.model.ts | 4 +- packages/core/src/comment/comment.service.ts | 10 +- .../src/comment/dto/create-comment.dto.ts | 4 +- .../1732537145609-CreateMentionTable.ts | 234 ------------------ .../1732685369636-CreateSubscriptionTable.ts | 234 ------------------ ...-AtlerMentionTableAddParentEntityFields.ts | 172 ------------- .../src/mention/dto/mentioned-user-ids.dto.ts | 6 +- .../core/src/mention/mention.controller.ts | 2 +- packages/core/src/mention/mention.entity.ts | 4 +- packages/core/src/mention/mention.service.ts | 12 +- .../src/subscription/subscription.entity.ts | 2 +- .../commands/handlers/task-create.handler.ts | 8 +- .../core/src/tasks/dto/create-task.dto.ts | 4 +- packages/core/src/tasks/task.service.ts | 4 +- 17 files changed, 36 insertions(+), 676 deletions(-) delete mode 100644 packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts delete mode 100644 packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts delete mode 100644 packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts diff --git a/packages/contracts/src/comment.model.ts b/packages/contracts/src/comment.model.ts index 2c94b499c2e..460db074954 100644 --- a/packages/contracts/src/comment.model.ts +++ b/packages/contracts/src/comment.model.ts @@ -2,7 +2,7 @@ import { ActorTypeEnum, IBasePerEntityType, IBasePerTenantAndOrganizationEntityM import { IUser } from './user.model'; import { IEmployee } from './employee.model'; import { IOrganizationTeam } from './organization-team.model'; -import { IMentionedUserIds } from './mention.model'; +import { IMentionUserIds } from './mention.model'; export interface IComment extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { comment: string; @@ -21,7 +21,7 @@ export interface IComment extends IBasePerTenantAndOrganizationEntityModel, IBas replies?: IComment[]; } -export interface ICommentCreateInput extends IBasePerEntityType, IMentionedUserIds { +export interface ICommentCreateInput extends IBasePerEntityType, IMentionUserIds { comment: string; parentId?: ID; members?: IEmployee[]; @@ -29,7 +29,7 @@ export interface ICommentCreateInput extends IBasePerEntityType, IMentionedUserI } export interface ICommentUpdateInput - extends IMentionedUserIds, + extends IMentionUserIds, Partial> {} export interface ICommentFindInput extends Pick {} diff --git a/packages/contracts/src/mention.model.ts b/packages/contracts/src/mention.model.ts index 5f3fb6d176f..a50bf067f08 100644 --- a/packages/contracts/src/mention.model.ts +++ b/packages/contracts/src/mention.model.ts @@ -12,6 +12,6 @@ export interface IMention extends IBasePerTenantAndOrganizationEntityModel, IBas export interface IMentionCreateInput extends Omit {} -export interface IMentionedUserIds { - mentionIds?: ID[]; +export interface IMentionUserIds { + mentionUserIds?: ID[]; } diff --git a/packages/contracts/src/subscription.model.ts b/packages/contracts/src/subscription.model.ts index 5a9b32dc1fc..7e84c9d7ab2 100644 --- a/packages/contracts/src/subscription.model.ts +++ b/packages/contracts/src/subscription.model.ts @@ -2,7 +2,7 @@ import { IBasePerEntityType, IBasePerTenantAndOrganizationEntityModel, ID } from import { IUser } from './user.model'; export interface ISubscription extends IBasePerTenantAndOrganizationEntityModel, IBasePerEntityType { - subscriptionType: SubscriptionTypeEnum; + type: SubscriptionTypeEnum; userId: ID; user?: IUser; } diff --git a/packages/contracts/src/task.model.ts b/packages/contracts/src/task.model.ts index 75471c0a505..7c9d20d88c1 100644 --- a/packages/contracts/src/task.model.ts +++ b/packages/contracts/src/task.model.ts @@ -15,7 +15,7 @@ import { ITaskPriority, TaskPriorityEnum } from './task-priority.model'; import { ITaskSize, TaskSizeEnum } from './task-size.model'; import { IOrganizationProjectModule } from './organization-project-module.model'; import { TaskTypeEnum } from './issue-type.model'; -import { IMentionedUserIds } from './mention.model'; +import { IMentionUserIds } from './mention.model'; export interface ITask extends IBasePerTenantAndOrganizationEntityModel, @@ -76,7 +76,7 @@ export enum TaskParticipantEnum { TEAMS = 'teams' } -export interface ITaskCreateInput extends ITask, IMentionedUserIds {} +export interface ITaskCreateInput extends ITask, IMentionUserIds {} export interface ITaskUpdateInput extends ITaskCreateInput { id?: string; diff --git a/packages/core/src/comment/comment.service.ts b/packages/core/src/comment/comment.service.ts index 3ca1d046b56..d80dc07739b 100644 --- a/packages/core/src/comment/comment.service.ts +++ b/packages/core/src/comment/comment.service.ts @@ -40,7 +40,7 @@ export class CommentService extends TenantAwareCrudService { try { const userId = RequestContext.currentUserId(); const tenantId = RequestContext.currentTenantId(); - const { mentionIds = [], ...data } = input; + const { mentionUserIds = [], ...data } = input; // Employee existence validation const user = await this.userService.findOneByIdString(userId); @@ -57,7 +57,7 @@ export class CommentService extends TenantAwareCrudService { // Apply mentions if needed await Promise.all( - mentionIds.map((mentionedUserId) => + mentionUserIds.map((mentionedUserId) => this.mentionService.publishMention({ entity: BaseEntityEnum.Comment, entityId: comment.id, @@ -75,7 +75,7 @@ export class CommentService extends TenantAwareCrudService { entity: input.entity, entityId: input.entityId, userId: user.id, - subscriptionType: SubscriptionTypeEnum.COMMENT, + type: SubscriptionTypeEnum.COMMENT, organizationId: comment.organizationId, tenantId }) @@ -98,7 +98,7 @@ export class CommentService extends TenantAwareCrudService { */ async update(id: ID, input: ICommentUpdateInput): Promise { try { - const { mentionIds = [] } = input; + const { mentionUserIds = [] } = input; const userId = RequestContext.currentUserId(); const comment = await this.findOneByOptions({ @@ -121,7 +121,7 @@ export class CommentService extends TenantAwareCrudService { await this.mentionService.updateEntityMentions( BaseEntityEnum.Comment, id, - mentionIds, + mentionUserIds, updatedComment.entityId, updatedComment.entity ); diff --git a/packages/core/src/comment/dto/create-comment.dto.ts b/packages/core/src/comment/dto/create-comment.dto.ts index 615ae6c5220..66780ad4ec5 100644 --- a/packages/core/src/comment/dto/create-comment.dto.ts +++ b/packages/core/src/comment/dto/create-comment.dto.ts @@ -1,7 +1,7 @@ import { IntersectionType, OmitType } from '@nestjs/swagger'; import { ICommentCreateInput } from '@gauzy/contracts'; import { TenantOrganizationBaseDTO } from './../../core/dto'; -import { MentionedUserIdsDTO } from '../../mention/dto'; +import { MentionUserIdsDTO } from '../../mention/dto'; import { Comment } from '../comment.entity'; /** @@ -10,6 +10,6 @@ import { Comment } from '../comment.entity'; export class CreateCommentDTO extends IntersectionType( TenantOrganizationBaseDTO, - IntersectionType(OmitType(Comment, ['creatorId', 'creator']), MentionedUserIdsDTO) + IntersectionType(OmitType(Comment, ['creatorId', 'creator']), MentionUserIdsDTO) ) implements ICommentCreateInput {} diff --git a/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts b/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts deleted file mode 100644 index 61ca56a2f20..00000000000 --- a/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { yellow } from 'chalk'; -import { DatabaseTypeEnum } from '@gauzy/config'; - -export class CreateMentionTable1732537145609 implements MigrationInterface { - name = 'CreateMentionTable1732537145609'; - - /** - * Up Migration - * - * @param queryRunner - */ - public async up(queryRunner: QueryRunner): Promise { - Logger.debug(yellow(this.name + ' start running!'), 'Migration'); - - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlUpQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * Down Migration - * - * @param queryRunner - */ - public async down(queryRunner: QueryRunner): Promise { - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlDownQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * PostgresDB Up Migration - * - * @param queryRunner - */ - public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "mention" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entityId" character varying NOT NULL, "entity" character varying NOT NULL, "mentionedUserId" uuid NOT NULL, "mentionById" uuid NOT NULL, CONSTRAINT "PK_9b02b76c4b65e3c35c1a545bf57" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - await queryRunner.query( - `ALTER TABLE "mention" ADD CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE "mention" ADD CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` - ); - await queryRunner.query( - `ALTER TABLE "mention" ADD CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE "mention" ADD CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` - ); - } - - /** - * PostgresDB Down Migration - * - * @param queryRunner - */ - public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_34b0087a30379c86b470a4298ca"`); - await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_16a2deee0d7ea361950eed1b944"`); - await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11"`); - await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_580d84e23219b07f520131f9271"`); - await queryRunner.query(`DROP INDEX "public"."IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "public"."IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "public"."IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "public"."IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query(`DROP TABLE "mention"`); - } - - /** - * SqliteDB and BetterSQlite3DB Up Migration - * - * @param queryRunner - */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL)` - ); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query( - `CREATE TABLE "temporary_mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` - ); - await queryRunner.query( - `INSERT INTO "temporary_mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "mention"` - ); - await queryRunner.query(`DROP TABLE "mention"`); - await queryRunner.query(`ALTER TABLE "temporary_mention" RENAME TO "mention"`); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - } - - /** - * SqliteDB and BetterSQlite3DB Down Migration - * - * @param queryRunner - */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query(`ALTER TABLE "mention" RENAME TO "temporary_mention"`); - await queryRunner.query( - `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL)` - ); - await queryRunner.query( - `INSERT INTO "mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "temporary_mention"` - ); - await queryRunner.query(`DROP TABLE "temporary_mention"`); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query(`DROP TABLE "mention"`); - } - - /** - * MySQL Up Migration - * - * @param queryRunner - */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE \`mention\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entityId\` varchar(255) NOT NULL, \`entity\` varchar(255) NOT NULL, \`mentionedUserId\` varchar(255) NOT NULL, \`mentionById\` varchar(255) NOT NULL, INDEX \`IDX_2c71b2f53b9162a94e1f02e40b\` (\`isActive\`), INDEX \`IDX_9597d3f3afbf40e6ffd1b0ebc9\` (\`isArchived\`), INDEX \`IDX_580d84e23219b07f520131f927\` (\`tenantId\`), INDEX \`IDX_4f018d32b6d2e2c907833d0db1\` (\`organizationId\`), INDEX \`IDX_d01675da9ddf57bef5692fca8b\` (\`entityId\`), INDEX \`IDX_3d6a8e3430779c21f04513cc5a\` (\`entity\`), INDEX \`IDX_16a2deee0d7ea361950eed1b94\` (\`mentionedUserId\`), INDEX \`IDX_34b0087a30379c86b470a4298c\` (\`mentionById\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` - ); - await queryRunner.query( - `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_580d84e23219b07f520131f9271\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_4f018d32b6d2e2c907833d0db11\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` - ); - await queryRunner.query( - `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_16a2deee0d7ea361950eed1b944\` FOREIGN KEY (\`mentionedUserId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_34b0087a30379c86b470a4298ca\` FOREIGN KEY (\`mentionById\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` - ); - } - - /** - * MySQL Down Migration - * - * @param queryRunner - */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_34b0087a30379c86b470a4298ca\``); - await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_16a2deee0d7ea361950eed1b944\``); - await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_4f018d32b6d2e2c907833d0db11\``); - await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_580d84e23219b07f520131f9271\``); - await queryRunner.query(`DROP INDEX \`IDX_34b0087a30379c86b470a4298c\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_16a2deee0d7ea361950eed1b94\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_3d6a8e3430779c21f04513cc5a\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_d01675da9ddf57bef5692fca8b\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_4f018d32b6d2e2c907833d0db1\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_580d84e23219b07f520131f927\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_9597d3f3afbf40e6ffd1b0ebc9\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_2c71b2f53b9162a94e1f02e40b\` ON \`mention\``); - await queryRunner.query(`DROP TABLE \`mention\``); - } -} diff --git a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts deleted file mode 100644 index f8ceb200458..00000000000 --- a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { yellow } from 'chalk'; -import { DatabaseTypeEnum } from '@gauzy/config'; - -export class CreateSubscriptionTable1732685369636 implements MigrationInterface { - name = 'CreateSubscriptionTable1732685369636'; - - /** - * Up Migration - * - * @param queryRunner - */ - public async up(queryRunner: QueryRunner): Promise { - Logger.debug(yellow(this.name + ' start running!'), 'Migration'); - - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlUpQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * Down Migration - * - * @param queryRunner - */ - public async down(queryRunner: QueryRunner): Promise { - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlDownQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * PostgresDB Up Migration - * - * @param queryRunner - */ - public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "subscription" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entityId" character varying NOT NULL, "entity" character varying NOT NULL, "subscriptionType" character varying NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_8c3e00ebd02103caa1174cd5d9d" PRIMARY KEY ("id"))` - ); - await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); - await queryRunner.query( - `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` - ); - await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); - await queryRunner.query( - `ALTER TABLE "subscription" ADD CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE "subscription" ADD CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` - ); - await queryRunner.query( - `ALTER TABLE "subscription" ADD CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` - ); - } - - /** - * PostgresDB Down Migration - * - * @param queryRunner - */ - public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0"`); - await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_8ccdfc22892c16950b568145d53"`); - await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cc906b4bc892b048f1b654d2aa"`); - await queryRunner.query(`DROP INDEX "public"."IDX_59d935a1de99d140f45550f344"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cb98de07e0868a9951e4d9b353"`); - await queryRunner.query(`DROP INDEX "public"."IDX_404bc7ad0e4734744372d656fe"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8ccdfc22892c16950b568145d5"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c86077795cb9a3ce80d19d670a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_6eafe9ba53fdd744cd1cffede8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a0ce0007cfcc8e6ee405d0272f"`); - await queryRunner.query(`DROP TABLE "subscription"`); - } - - /** - * SqliteDB and BetterSQlite3DB Up Migration - * - * @param queryRunner - */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL)` - ); - await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); - await queryRunner.query( - `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` - ); - await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); - await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); - await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); - await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); - await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); - await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); - await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); - await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); - await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); - await queryRunner.query( - `CREATE TABLE "temporary_subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL, CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` - ); - await queryRunner.query( - `INSERT INTO "temporary_subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId" FROM "subscription"` - ); - await queryRunner.query(`DROP TABLE "subscription"`); - await queryRunner.query(`ALTER TABLE "temporary_subscription" RENAME TO "subscription"`); - await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); - await queryRunner.query( - `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` - ); - await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); - } - - /** - * SqliteDB and BetterSQlite3DB Down Migration - * - * @param queryRunner - */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); - await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); - await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); - await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); - await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); - await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); - await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); - await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); - await queryRunner.query(`ALTER TABLE "subscription" RENAME TO "temporary_subscription"`); - await queryRunner.query( - `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "subscriptionType" varchar NOT NULL, "userId" varchar NOT NULL)` - ); - await queryRunner.query( - `INSERT INTO "subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "subscriptionType", "userId" FROM "temporary_subscription"` - ); - await queryRunner.query(`DROP TABLE "temporary_subscription"`); - await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); - await queryRunner.query( - `CREATE INDEX "IDX_59d935a1de99d140f45550f344" ON "subscription" ("subscriptionType") ` - ); - await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); - await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); - await queryRunner.query(`DROP INDEX "IDX_59d935a1de99d140f45550f344"`); - await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); - await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); - await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); - await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); - await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); - await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); - await queryRunner.query(`DROP TABLE "subscription"`); - } - - /** - * MySQL Up Migration - * - * @param queryRunner - */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE \`subscription\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entityId\` varchar(255) NOT NULL, \`entity\` varchar(255) NOT NULL, \`subscriptionType\` varchar(255) NOT NULL, \`userId\` varchar(255) NOT NULL, INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` (\`isActive\`), INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` (\`isArchived\`), INDEX \`IDX_c86077795cb9a3ce80d19d670a\` (\`tenantId\`), INDEX \`IDX_8ccdfc22892c16950b568145d5\` (\`organizationId\`), INDEX \`IDX_404bc7ad0e4734744372d656fe\` (\`entityId\`), INDEX \`IDX_cb98de07e0868a9951e4d9b353\` (\`entity\`), INDEX \`IDX_59d935a1de99d140f45550f344\` (\`subscriptionType\`), INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` (\`userId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` - ); - await queryRunner.query( - `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_c86077795cb9a3ce80d19d670a5\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` - ); - await queryRunner.query( - `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_8ccdfc22892c16950b568145d53\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` - ); - await queryRunner.query( - `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_cc906b4bc892b048f1b654d2aa0\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` - ); - } - - /** - * MySQL Down Migration - * - * @param queryRunner - */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_cc906b4bc892b048f1b654d2aa0\``); - await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_8ccdfc22892c16950b568145d53\``); - await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_c86077795cb9a3ce80d19d670a5\``); - await queryRunner.query(`DROP INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_59d935a1de99d140f45550f344\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_cb98de07e0868a9951e4d9b353\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_404bc7ad0e4734744372d656fe\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_8ccdfc22892c16950b568145d5\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_c86077795cb9a3ce80d19d670a\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` ON \`subscription\``); - await queryRunner.query(`DROP INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` ON \`subscription\``); - await queryRunner.query(`DROP TABLE \`subscription\``); - } -} diff --git a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts b/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts deleted file mode 100644 index 11242f74740..00000000000 --- a/packages/core/src/database/migrations/1732775571004-AtlerMentionTableAddParentEntityFields.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { MigrationInterface, QueryRunner } from 'typeorm'; -import { yellow } from 'chalk'; -import { DatabaseTypeEnum } from '@gauzy/config'; - -export class AtlerMentionTableAddParentEntityFields1732775571004 implements MigrationInterface { - name = 'AtlerMentionTableAddParentEntityFields1732775571004'; - - /** - * Up Migration - * - * @param queryRunner - */ - public async up(queryRunner: QueryRunner): Promise { - Logger.debug(yellow(this.name + ' start running!'), 'Migration'); - - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresUpQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlUpQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * Down Migration - * - * @param queryRunner - */ - public async down(queryRunner: QueryRunner): Promise { - switch (queryRunner.connection.options.type) { - case DatabaseTypeEnum.sqlite: - case DatabaseTypeEnum.betterSqlite3: - await this.sqliteDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.postgres: - await this.postgresDownQueryRunner(queryRunner); - break; - case DatabaseTypeEnum.mysql: - await this.mysqlDownQueryRunner(queryRunner); - break; - default: - throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); - } - } - - /** - * PostgresDB Up Migration - * - * @param queryRunner - */ - public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "mention" ADD "parentEntityId" character varying NOT NULL`); - await queryRunner.query(`ALTER TABLE "mention" ADD "parentEntityType" character varying NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); - } - - /** - * PostgresDB Down Migration - * - * @param queryRunner - */ - public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_4f9397b277ec0791c5f9e2fd62"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5b95805861f9de5cf7760a964a"`); - await queryRunner.query(`ALTER TABLE "mention" DROP COLUMN "parentEntityType"`); - await queryRunner.query(`ALTER TABLE "mention" DROP COLUMN "parentEntityId"`); - } - - /** - * SqliteDB and BetterSQlite3DB Up Migration - * - * @param queryRunner - */ - public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query( - `CREATE TABLE "temporary_mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, "parentEntityId" varchar NOT NULL, "parentEntityType" varchar NOT NULL, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` - ); - await queryRunner.query( - `INSERT INTO "temporary_mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "mention"` - ); - await queryRunner.query(`DROP TABLE "mention"`); - await queryRunner.query(`ALTER TABLE "temporary_mention" RENAME TO "mention"`); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); - } - - /** - * SqliteDB and BetterSQlite3DB Down Migration - * - * @param queryRunner - */ - public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "IDX_4f9397b277ec0791c5f9e2fd62"`); - await queryRunner.query(`DROP INDEX "IDX_5b95805861f9de5cf7760a964a"`); - await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); - await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); - await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); - await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); - await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); - await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); - await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); - await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); - await queryRunner.query(`ALTER TABLE "mention" RENAME TO "temporary_mention"`); - await queryRunner.query( - `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` - ); - await queryRunner.query( - `INSERT INTO "mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "mentionedUserId", "mentionById" FROM "temporary_mention"` - ); - await queryRunner.query(`DROP TABLE "temporary_mention"`); - await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); - await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); - await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); - await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); - await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); - await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); - } - - /** - * MySQL Up Migration - * - * @param queryRunner - */ - public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE \`mention\` ADD \`parentEntityId\` varchar(255) NOT NULL`); - await queryRunner.query(`ALTER TABLE \`mention\` ADD \`parentEntityType\` varchar(255) NOT NULL`); - await queryRunner.query(`CREATE INDEX \`IDX_5b95805861f9de5cf7760a964a\` ON \`mention\` (\`parentEntityId\`)`); - await queryRunner.query( - `CREATE INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` ON \`mention\` (\`parentEntityType\`)` - ); - } - - /** - * MySQL Down Migration - * - * @param queryRunner - */ - public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` ON \`mention\``); - await queryRunner.query(`DROP INDEX \`IDX_5b95805861f9de5cf7760a964a\` ON \`mention\``); - await queryRunner.query(`ALTER TABLE \`mention\` DROP COLUMN \`parentEntityType\``); - await queryRunner.query(`ALTER TABLE \`mention\` DROP COLUMN \`parentEntityId\``); - } -} diff --git a/packages/core/src/mention/dto/mentioned-user-ids.dto.ts b/packages/core/src/mention/dto/mentioned-user-ids.dto.ts index 1000bbd63a4..6c9bced5505 100644 --- a/packages/core/src/mention/dto/mentioned-user-ids.dto.ts +++ b/packages/core/src/mention/dto/mentioned-user-ids.dto.ts @@ -1,10 +1,10 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsArray, IsOptional } from 'class-validator'; -import { ID, IMentionedUserIds } from '@gauzy/contracts'; +import { ID, IMentionUserIds } from '@gauzy/contracts'; -export class MentionedUserIdsDTO implements IMentionedUserIds { +export class MentionUserIdsDTO implements IMentionUserIds { @ApiPropertyOptional({ type: () => Array }) @IsOptional() @IsArray() - mentionIds?: ID[]; + mentionUserIds?: ID[]; } diff --git a/packages/core/src/mention/mention.controller.ts b/packages/core/src/mention/mention.controller.ts index 0df3b985add..92fe6e22c84 100644 --- a/packages/core/src/mention/mention.controller.ts +++ b/packages/core/src/mention/mention.controller.ts @@ -5,7 +5,7 @@ import { Mention } from './mention.entity'; import { MentionService } from './mention.service'; @UseGuards(TenantPermissionGuard, PermissionGuard) -@Controller('/mention') +@Controller('/mentions') export class MentionController extends CrudController { constructor(readonly mentionService: MentionService) { super(mentionService); diff --git a/packages/core/src/mention/mention.entity.ts b/packages/core/src/mention/mention.entity.ts index 6fae01a0885..3914cf33838 100644 --- a/packages/core/src/mention/mention.entity.ts +++ b/packages/core/src/mention/mention.entity.ts @@ -34,14 +34,14 @@ export class Mention extends TenantOrganizationBaseEntity implements IMention { @IsOptional() @IsUUID() @ColumnIndex() - @MultiORMColumn() + @MultiORMColumn({ nullable: true }) parentEntityId?: ID; @ApiProperty({ type: () => String, enum: BaseEntityEnum }) @IsOptional() @IsEnum(BaseEntityEnum) @ColumnIndex() - @MultiORMColumn() + @MultiORMColumn({ nullable: true }) parentEntityType?: BaseEntityEnum; /* diff --git a/packages/core/src/mention/mention.service.ts b/packages/core/src/mention/mention.service.ts index d5b84d53bb4..4dfda23ee9b 100644 --- a/packages/core/src/mention/mention.service.ts +++ b/packages/core/src/mention/mention.service.ts @@ -47,7 +47,7 @@ export class MentionService extends TenantAwareCrudService { entity: parentEntityType ?? entity, entityId: parentEntityId ?? entityId, userId: mentionedUserId, - subscriptionType: SubscriptionTypeEnum.MENTION, + type: SubscriptionTypeEnum.MENTION, organizationId, tenantId }) @@ -81,18 +81,18 @@ export class MentionService extends TenantAwareCrudService { * * This method handles adding new mentions and removing outdated mentions * for an entity (e.g., comments, tasks, or projects). It ensures that only - * the specified user mentions (`newMentionIds`) are associated with the entity. + * the specified user mentions (`newMentionUserIds`) are associated with the entity. * * @param entity - The type of entity being updated (e.g., Comment, Task, etc.). * @param entityId - The ID of the entity being updated. - * @param mentionIds - Array of user IDs to be mentioned in this entity. + * @param mentionUserIds - Array of user IDs to be mentioned in this entity. * @param parentEntityId - (Optional) The ID of the parent entity, if applicable. * @param parentEntityType - (Optional) The type of the parent entity, if applicable. */ async updateEntityMentions( entity: BaseEntityEnum, entityId: ID, - mentionsIds: ID[], + mentionUserIds: ID[], parentEntityId?: ID, parentEntityType?: BaseEntityEnum ): Promise { @@ -109,10 +109,10 @@ export class MentionService extends TenantAwareCrudService { const existingMentionUserIds = new Set(existingMentions.map((mention) => mention.mentionedUserId)); // Determine mentions to add (not present in existing mentions) - const mentionsToAdd = mentionsIds.filter((id) => !existingMentionUserIds.has(id)); + const mentionsToAdd = mentionUserIds.filter((id) => !existingMentionUserIds.has(id)); // Determine mentions to remove (present in existing mentions but not in mentionsIds) - const mentionsToRemove = [...existingMentionUserIds].filter((id) => !mentionsIds.includes(id)); + const mentionsToRemove = [...existingMentionUserIds].filter((id) => !mentionUserIds.includes(id)); // Add new mentions if (mentionsToAdd.length > 0) { diff --git a/packages/core/src/subscription/subscription.entity.ts b/packages/core/src/subscription/subscription.entity.ts index ceca6daa172..16003ea78c5 100644 --- a/packages/core/src/subscription/subscription.entity.ts +++ b/packages/core/src/subscription/subscription.entity.ts @@ -30,7 +30,7 @@ export class Subscription extends TenantOrganizationBaseEntity implements ISubsc @IsEnum(SubscriptionTypeEnum) @ColumnIndex() @MultiORMColumn() - subscriptionType: SubscriptionTypeEnum; + type: SubscriptionTypeEnum; /* |-------------------------------------------------------------------------- diff --git a/packages/core/src/tasks/commands/handlers/task-create.handler.ts b/packages/core/src/tasks/commands/handlers/task-create.handler.ts index 9d54ae09b49..24cae283979 100644 --- a/packages/core/src/tasks/commands/handlers/task-create.handler.ts +++ b/packages/core/src/tasks/commands/handlers/task-create.handler.ts @@ -36,7 +36,7 @@ export class TaskCreateHandler implements ICommandHandler { try { // Destructure input and triggered event flag from the command const { input, triggeredEvent } = command; - const { organizationId, mentionIds = [], ...data } = input; + const { organizationId, mentionUserIds = [], ...data } = input; // Retrieve current tenant ID from request context or use input tenant ID const tenantId = RequestContext.currentTenantId() ?? data.tenantId; @@ -78,9 +78,9 @@ export class TaskCreateHandler implements ICommandHandler { } // Apply mentions if needed - if (mentionIds.length > 0) { + if (mentionUserIds.length > 0) { await Promise.all( - mentionIds.map((mentionedUserId) => + mentionUserIds.map((mentionedUserId) => this.mentionService.publishMention({ entity: BaseEntityEnum.Task, entityId: task.id, @@ -97,7 +97,7 @@ export class TaskCreateHandler implements ICommandHandler { entity: BaseEntityEnum.Task, entityId: task.id, userId: task.creatorId, - subscriptionType: SubscriptionTypeEnum.CREATED_ENTITY, + type: SubscriptionTypeEnum.CREATED_ENTITY, organizationId, tenantId }) diff --git a/packages/core/src/tasks/dto/create-task.dto.ts b/packages/core/src/tasks/dto/create-task.dto.ts index a3738bb9686..2f60e916096 100644 --- a/packages/core/src/tasks/dto/create-task.dto.ts +++ b/packages/core/src/tasks/dto/create-task.dto.ts @@ -1,7 +1,7 @@ import { ITaskCreateInput } from '@gauzy/contracts'; import { IntersectionType, OmitType } from '@nestjs/swagger'; import { TenantOrganizationBaseDTO } from './../../core/dto'; -import { MentionedUserIdsDTO } from '../../mention/dto'; +import { MentionUserIdsDTO } from '../../mention/dto'; import { Task } from './../task.entity'; /** @@ -10,6 +10,6 @@ import { Task } from './../task.entity'; export class CreateTaskDTO extends IntersectionType( TenantOrganizationBaseDTO, - IntersectionType(OmitType(Task, ['organizationId', 'organization']), MentionedUserIdsDTO) + IntersectionType(OmitType(Task, ['organizationId', 'organization']), MentionUserIdsDTO) ) implements ITaskCreateInput {} diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index 4bb34d62198..ccd9ba83509 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -64,7 +64,7 @@ export class TaskService extends TenantAwareCrudService { try { const tenantId = RequestContext.currentTenantId() || input.tenantId; const userId = RequestContext.currentUserId(); - const { mentionIds, ...data } = input; + const { mentionUserIds, ...data } = input; // Find task relations const relations = this.typeOrmTaskRepository.metadata.relations.map((relation) => relation.propertyName); @@ -110,7 +110,7 @@ export class TaskService extends TenantAwareCrudService { // Synchronize mentions if (data.description) { - await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionIds); + await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionUserIds); } // TODO : Subscribe assignees From ade2a5a4a2bfb5030511452ec0048f6a20fc3ced Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 3 Dec 2024 17:15:51 +0200 Subject: [PATCH 29/30] fix: mention and subscription migration --- .../1732537145609-CreateMentionTable.ts | 252 ++++++++++++++++++ .../1732685369636-CreateSubscriptionTable.ts | 228 ++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts create mode 100644 packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts diff --git a/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts b/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts new file mode 100644 index 00000000000..df68208f081 --- /dev/null +++ b/packages/core/src/database/migrations/1732537145609-CreateMentionTable.ts @@ -0,0 +1,252 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateMentionTable1732537145609 implements MigrationInterface { + name = 'CreateMentionTable1732537145609'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "mention" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entityId" character varying NOT NULL, "entity" character varying NOT NULL, "parentEntityId" character varying, "parentEntityType" character varying, "mentionedUserId" uuid NOT NULL, "mentionById" uuid NOT NULL, CONSTRAINT "PK_9b02b76c4b65e3c35c1a545bf57" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + await queryRunner.query( + `ALTER TABLE "mention" ADD CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "mention" ADD CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "mention" ADD CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "mention" ADD CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_34b0087a30379c86b470a4298ca"`); + await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_16a2deee0d7ea361950eed1b944"`); + await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11"`); + await queryRunner.query(`ALTER TABLE "mention" DROP CONSTRAINT "FK_580d84e23219b07f520131f9271"`); + await queryRunner.query(`DROP INDEX "public"."IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "public"."IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "public"."IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "public"."IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query(`DROP TABLE "mention"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "parentEntityId" varchar, "parentEntityType" varchar, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL)` + ); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`DROP INDEX "IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query( + `CREATE TABLE "temporary_mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "parentEntityId" varchar, "parentEntityType" varchar, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL, CONSTRAINT "FK_580d84e23219b07f520131f9271" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_4f018d32b6d2e2c907833d0db11" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_16a2deee0d7ea361950eed1b944" FOREIGN KEY ("mentionedUserId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_34b0087a30379c86b470a4298ca" FOREIGN KEY ("mentionById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "parentEntityId", "parentEntityType", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "parentEntityId", "parentEntityType", "mentionedUserId", "mentionById" FROM "mention"` + ); + await queryRunner.query(`DROP TABLE "mention"`); + await queryRunner.query(`ALTER TABLE "temporary_mention" RENAME TO "mention"`); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query(`ALTER TABLE "mention" RENAME TO "temporary_mention"`); + await queryRunner.query( + `CREATE TABLE "mention" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "parentEntityId" varchar, "parentEntityType" varchar, "mentionedUserId" varchar NOT NULL, "mentionById" varchar NOT NULL)` + ); + await queryRunner.query( + `INSERT INTO "mention"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "parentEntityId", "parentEntityType", "mentionedUserId", "mentionById") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "parentEntityId", "parentEntityType", "mentionedUserId", "mentionById" FROM "temporary_mention"` + ); + await queryRunner.query(`DROP TABLE "temporary_mention"`); + await queryRunner.query(`CREATE INDEX "IDX_34b0087a30379c86b470a4298c" ON "mention" ("mentionById") `); + await queryRunner.query(`CREATE INDEX "IDX_16a2deee0d7ea361950eed1b94" ON "mention" ("mentionedUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f9397b277ec0791c5f9e2fd62" ON "mention" ("parentEntityType") `); + await queryRunner.query(`CREATE INDEX "IDX_5b95805861f9de5cf7760a964a" ON "mention" ("parentEntityId") `); + await queryRunner.query(`CREATE INDEX "IDX_3d6a8e3430779c21f04513cc5a" ON "mention" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_d01675da9ddf57bef5692fca8b" ON "mention" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_4f018d32b6d2e2c907833d0db1" ON "mention" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_580d84e23219b07f520131f927" ON "mention" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9" ON "mention" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_2c71b2f53b9162a94e1f02e40b" ON "mention" ("isActive") `); + await queryRunner.query(`DROP INDEX "IDX_34b0087a30379c86b470a4298c"`); + await queryRunner.query(`DROP INDEX "IDX_16a2deee0d7ea361950eed1b94"`); + await queryRunner.query(`DROP INDEX "IDX_4f9397b277ec0791c5f9e2fd62"`); + await queryRunner.query(`DROP INDEX "IDX_5b95805861f9de5cf7760a964a"`); + await queryRunner.query(`DROP INDEX "IDX_3d6a8e3430779c21f04513cc5a"`); + await queryRunner.query(`DROP INDEX "IDX_d01675da9ddf57bef5692fca8b"`); + await queryRunner.query(`DROP INDEX "IDX_4f018d32b6d2e2c907833d0db1"`); + await queryRunner.query(`DROP INDEX "IDX_580d84e23219b07f520131f927"`); + await queryRunner.query(`DROP INDEX "IDX_9597d3f3afbf40e6ffd1b0ebc9"`); + await queryRunner.query(`DROP INDEX "IDX_2c71b2f53b9162a94e1f02e40b"`); + await queryRunner.query(`DROP TABLE "mention"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`mention\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entityId\` varchar(255) NOT NULL, \`entity\` varchar(255) NOT NULL, \`parentEntityId\` varchar(255) NULL, \`parentEntityType\` varchar(255) NULL, \`mentionedUserId\` varchar(255) NOT NULL, \`mentionById\` varchar(255) NOT NULL, INDEX \`IDX_2c71b2f53b9162a94e1f02e40b\` (\`isActive\`), INDEX \`IDX_9597d3f3afbf40e6ffd1b0ebc9\` (\`isArchived\`), INDEX \`IDX_580d84e23219b07f520131f927\` (\`tenantId\`), INDEX \`IDX_4f018d32b6d2e2c907833d0db1\` (\`organizationId\`), INDEX \`IDX_d01675da9ddf57bef5692fca8b\` (\`entityId\`), INDEX \`IDX_3d6a8e3430779c21f04513cc5a\` (\`entity\`), INDEX \`IDX_5b95805861f9de5cf7760a964a\` (\`parentEntityId\`), INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` (\`parentEntityType\`), INDEX \`IDX_16a2deee0d7ea361950eed1b94\` (\`mentionedUserId\`), INDEX \`IDX_34b0087a30379c86b470a4298c\` (\`mentionById\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_580d84e23219b07f520131f9271\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_4f018d32b6d2e2c907833d0db11\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_16a2deee0d7ea361950eed1b944\` FOREIGN KEY (\`mentionedUserId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`mention\` ADD CONSTRAINT \`FK_34b0087a30379c86b470a4298ca\` FOREIGN KEY (\`mentionById\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_34b0087a30379c86b470a4298ca\``); + await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_16a2deee0d7ea361950eed1b944\``); + await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_4f018d32b6d2e2c907833d0db11\``); + await queryRunner.query(`ALTER TABLE \`mention\` DROP FOREIGN KEY \`FK_580d84e23219b07f520131f9271\``); + await queryRunner.query(`DROP INDEX \`IDX_34b0087a30379c86b470a4298c\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_16a2deee0d7ea361950eed1b94\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_4f9397b277ec0791c5f9e2fd62\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_5b95805861f9de5cf7760a964a\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_3d6a8e3430779c21f04513cc5a\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_d01675da9ddf57bef5692fca8b\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_4f018d32b6d2e2c907833d0db1\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_580d84e23219b07f520131f927\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_9597d3f3afbf40e6ffd1b0ebc9\` ON \`mention\``); + await queryRunner.query(`DROP INDEX \`IDX_2c71b2f53b9162a94e1f02e40b\` ON \`mention\``); + await queryRunner.query(`DROP TABLE \`mention\``); + } +} diff --git a/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts new file mode 100644 index 00000000000..aa01866e617 --- /dev/null +++ b/packages/core/src/database/migrations/1732685369636-CreateSubscriptionTable.ts @@ -0,0 +1,228 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class CreateSubscriptionTable1732685369636 implements MigrationInterface { + name = 'CreateSubscriptionTable1732685369636'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" TIMESTAMP, "id" uuid NOT NULL DEFAULT gen_random_uuid(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "isActive" boolean DEFAULT true, "isArchived" boolean DEFAULT false, "archivedAt" TIMESTAMP, "tenantId" uuid, "organizationId" uuid, "entityId" character varying NOT NULL, "entity" character varying NOT NULL, "type" character varying NOT NULL, "userId" uuid NOT NULL, CONSTRAINT "PK_8c3e00ebd02103caa1174cd5d9d" PRIMARY KEY ("id"))` + ); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_ec9817a3b53c5dd074af96276d" ON "subscription" ("type") `); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "subscription" ADD CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0"`); + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_8ccdfc22892c16950b568145d53"`); + await queryRunner.query(`ALTER TABLE "subscription" DROP CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ec9817a3b53c5dd074af96276d"`); + await queryRunner.query(`DROP INDEX "public"."IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "public"."IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP TABLE "subscription"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "type" varchar NOT NULL, "userId" varchar NOT NULL)` + ); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_ec9817a3b53c5dd074af96276d" ON "subscription" ("type") `); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_ec9817a3b53c5dd074af96276d"`); + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query( + `CREATE TABLE "temporary_subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "type" varchar NOT NULL, "userId" varchar NOT NULL, CONSTRAINT "FK_c86077795cb9a3ce80d19d670a5" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_8ccdfc22892c16950b568145d53" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_cc906b4bc892b048f1b654d2aa0" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "type", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "type", "userId" FROM "subscription"` + ); + await queryRunner.query(`DROP TABLE "subscription"`); + await queryRunner.query(`ALTER TABLE "temporary_subscription" RENAME TO "subscription"`); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_ec9817a3b53c5dd074af96276d" ON "subscription" ("type") `); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "IDX_ec9817a3b53c5dd074af96276d"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`ALTER TABLE "subscription" RENAME TO "temporary_subscription"`); + await queryRunner.query( + `CREATE TABLE "subscription" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entityId" varchar NOT NULL, "entity" varchar NOT NULL, "type" varchar NOT NULL, "userId" varchar NOT NULL)` + ); + await queryRunner.query( + `INSERT INTO "subscription"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "type", "userId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entityId", "entity", "type", "userId" FROM "temporary_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_subscription"`); + await queryRunner.query(`CREATE INDEX "IDX_cc906b4bc892b048f1b654d2aa" ON "subscription" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_ec9817a3b53c5dd074af96276d" ON "subscription" ("type") `); + await queryRunner.query(`CREATE INDEX "IDX_cb98de07e0868a9951e4d9b353" ON "subscription" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_404bc7ad0e4734744372d656fe" ON "subscription" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_8ccdfc22892c16950b568145d5" ON "subscription" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_c86077795cb9a3ce80d19d670a" ON "subscription" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_6eafe9ba53fdd744cd1cffede8" ON "subscription" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_a0ce0007cfcc8e6ee405d0272f" ON "subscription" ("isActive") `); + + await queryRunner.query(`DROP INDEX "IDX_cc906b4bc892b048f1b654d2aa"`); + await queryRunner.query(`DROP INDEX "IDX_ec9817a3b53c5dd074af96276d"`); + await queryRunner.query(`DROP INDEX "IDX_cb98de07e0868a9951e4d9b353"`); + await queryRunner.query(`DROP INDEX "IDX_404bc7ad0e4734744372d656fe"`); + await queryRunner.query(`DROP INDEX "IDX_8ccdfc22892c16950b568145d5"`); + await queryRunner.query(`DROP INDEX "IDX_c86077795cb9a3ce80d19d670a"`); + await queryRunner.query(`DROP INDEX "IDX_6eafe9ba53fdd744cd1cffede8"`); + await queryRunner.query(`DROP INDEX "IDX_a0ce0007cfcc8e6ee405d0272f"`); + await queryRunner.query(`DROP TABLE "subscription"`); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`subscription\` (\`deletedAt\` datetime(6) NULL, \`id\` varchar(36) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), \`isActive\` tinyint NULL DEFAULT 1, \`isArchived\` tinyint NULL DEFAULT 0, \`archivedAt\` datetime NULL, \`tenantId\` varchar(255) NULL, \`organizationId\` varchar(255) NULL, \`entityId\` varchar(255) NOT NULL, \`entity\` varchar(255) NOT NULL, \`type\` varchar(255) NOT NULL, \`userId\` varchar(255) NOT NULL, INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` (\`isActive\`), INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` (\`isArchived\`), INDEX \`IDX_c86077795cb9a3ce80d19d670a\` (\`tenantId\`), INDEX \`IDX_8ccdfc22892c16950b568145d5\` (\`organizationId\`), INDEX \`IDX_404bc7ad0e4734744372d656fe\` (\`entityId\`), INDEX \`IDX_cb98de07e0868a9951e4d9b353\` (\`entity\`), INDEX \`IDX_ec9817a3b53c5dd074af96276d\` (\`type\`), INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` (\`userId\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_c86077795cb9a3ce80d19d670a5\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_8ccdfc22892c16950b568145d53\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`subscription\` ADD CONSTRAINT \`FK_cc906b4bc892b048f1b654d2aa0\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_cc906b4bc892b048f1b654d2aa0\``); + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_8ccdfc22892c16950b568145d53\``); + await queryRunner.query(`ALTER TABLE \`subscription\` DROP FOREIGN KEY \`FK_c86077795cb9a3ce80d19d670a5\``); + + await queryRunner.query(`DROP INDEX \`IDX_cc906b4bc892b048f1b654d2aa\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_ec9817a3b53c5dd074af96276d\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_cb98de07e0868a9951e4d9b353\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_404bc7ad0e4734744372d656fe\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_8ccdfc22892c16950b568145d5\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_c86077795cb9a3ce80d19d670a\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_6eafe9ba53fdd744cd1cffede8\` ON \`subscription\``); + await queryRunner.query(`DROP INDEX \`IDX_a0ce0007cfcc8e6ee405d0272f\` ON \`subscription\``); + await queryRunner.query(`DROP TABLE \`subscription\``); + } +} From f6d5d77d4d45a622c3b032ab92e70c0c3ec0a90e Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 3 Dec 2024 17:46:52 +0200 Subject: [PATCH 30/30] fix: coderabbit suggestions --- packages/core/src/tasks/task.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/tasks/task.service.ts b/packages/core/src/tasks/task.service.ts index ccd9ba83509..086e6a487fe 100644 --- a/packages/core/src/tasks/task.service.ts +++ b/packages/core/src/tasks/task.service.ts @@ -110,7 +110,11 @@ export class TaskService extends TenantAwareCrudService { // Synchronize mentions if (data.description) { - await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionUserIds); + try { + await this.mentionService.updateEntityMentions(BaseEntityEnum.Task, id, mentionUserIds); + } catch (error) { + console.error('Error synchronizing mentions:', error); + } } // TODO : Subscribe assignees