diff --git a/package.json b/package.json index 72104fae1..12205ede1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "watch": "yarn build && ./node_modules/.bin/tsc --build --watch", "lint": "eslint \"./*.json\" \"packages/*/src/**/*.{ts,js,json}\" --fix", "test": "jest --testTimeout 30000", - "test:one": "jest --testTimeout 30000 packages/nestjs-invitation/src/services/invitation.service.spec.ts", + "test:one": "jest --testTimeout 30000 packages/nestjs-org/src/listeners/invitation-accepted-listener.spec.ts", "prepare": "husky install && yarn clean && yarn build", "test:watch": "jest --watch", "test:cov": "jest --coverage", diff --git a/packages/nestjs-invitation/src/__fixtures__/app.module.fixture.ts b/packages/nestjs-invitation/src/__fixtures__/app.module.fixture.ts index 704f26a85..ccf731b06 100644 --- a/packages/nestjs-invitation/src/__fixtures__/app.module.fixture.ts +++ b/packages/nestjs-invitation/src/__fixtures__/app.module.fixture.ts @@ -15,7 +15,6 @@ import { MailerModule, MailerService } from '@nestjs-modules/mailer'; import { InvitationModule } from '../invitation.module'; import { InvitationGetUserEventAsync } from '../events/invitation-get-user.event'; - import { InvitationEntityFixture } from './invitation/entities/invitation.entity.fixture'; import { InvitationAcceptedEventAsync } from '../events/invitation-accepted.event'; import { UserOtpEntityFixture } from './user/entities/user-otp-entity.fixture'; diff --git a/packages/nestjs-invitation/src/controllers/invitation.controller.e2e-spec.ts b/packages/nestjs-invitation/src/controllers/invitation.controller.e2e-spec.ts index 876e26ce6..951a7eb1f 100644 --- a/packages/nestjs-invitation/src/controllers/invitation.controller.e2e-spec.ts +++ b/packages/nestjs-invitation/src/controllers/invitation.controller.e2e-spec.ts @@ -4,7 +4,12 @@ import { INestApplication } from '@nestjs/common'; import { UserFactory } from '@concepta/nestjs-user/src/seeding'; import { ConfigService, ConfigType } from '@nestjs/config'; import { OtpService } from '@concepta/nestjs-otp'; -import { OtpInterface, UserInterface } from '@concepta/ts-common'; +import { + INVITATION_MODULE_CATEGORY_ORG_KEY, + INVITATION_MODULE_CATEGORY_USER_KEY, + OtpInterface, + UserInterface, +} from '@concepta/ts-common'; import { EmailService } from '@concepta/nestjs-email'; import { SeedingSource } from '@concepta/typeorm-seeding'; import { getDataSourceToken } from '@nestjs/typeorm'; @@ -17,19 +22,19 @@ import { invitationDefaultConfig } from '../config/invitation-default.config'; import { InvitationFactory } from '../invitation.factory'; import { InvitationEntityInterface } from '../interfaces/invitation.entity.interface'; import { InvitationSettingsInterface } from '../interfaces/invitation-settings.interface'; - import { AppModuleFixture } from '../__fixtures__/app.module.fixture'; import { InvitationEntityFixture } from '../__fixtures__/invitation/entities/invitation.entity.fixture'; import { UserEntityFixture } from '../__fixtures__/user/entities/user-entity.fixture'; describe('InvitationController (e2e)', () => { - const category = 'invitation'; + const userCategory = INVITATION_MODULE_CATEGORY_USER_KEY; + const orgCategory = INVITATION_MODULE_CATEGORY_ORG_KEY; const payload = { moreData: 'foo' }; let app: INestApplication; + let invitationFactory: InvitationFactory; let seedingSource: SeedingSource; let user: UserEntityFixture; - let invitation: InvitationEntityInterface; let otpService: OtpService; let configService: ConfigService; let config: ConfigType; @@ -60,13 +65,12 @@ describe('InvitationController (e2e)', () => { seedingSource, }); - const invitationFactory = new InvitationFactory({ + invitationFactory = new InvitationFactory({ entity: InvitationEntityFixture, seedingSource, }); user = await userFactory.create(); - invitation = await invitationFactory.create({ category, user }); }); afterEach(async () => { @@ -74,19 +78,71 @@ describe('InvitationController (e2e)', () => { return app ? await app.close() : undefined; }); + describe('Type: org', () => { + let invitation: InvitationEntityInterface; + + beforeEach(async () => { + invitation = await invitationFactory.create({ + category: orgCategory, + user, + }); + }); + + it('POST invitation', async () => { + await createInvite(app, { + email: user.email, + category: orgCategory, + payload, + }); + }); + + it('PATCH invitation-acceptance', async () => { + const { code } = invitation; + + const otp = await createOtp(config, otpService, user, orgCategory); + + const { passcode } = otp; + + await supertest(app.getHttpServer()) + .patch(`/invitation-acceptance/${code}`) + .send({ + passcode, + payload: { newPassword: 'hOdv2A2h%' }, + } as InvitationAcceptInviteDto) + .expect(200); + }); + }); + describe('Type: user', () => { + let invitation: InvitationEntityInterface; + + beforeEach(async () => { + invitation = await invitationFactory.create({ + category: userCategory, + user, + }); + }); + it('POST invitation', async () => { - await createInvite({ email: user.email, category, payload }); + await createInvite(app, { + email: user.email, + category: userCategory, + payload, + }); }); it('POST invitation (create new user)', async () => { - await createInvite({ email: 'test@mail.com', category, payload }); + await createInvite(app, { + email: 'test@mail.com', + category: userCategory, + payload, + }); }); it('POST invitation reattempt', async () => { - const invitationDto = await createInvite({ + const invitationDto = await createInvite(app, { email: 'test@mail.com', - category, + category: userCategory, payload, }); @@ -98,7 +154,7 @@ describe('InvitationController (e2e)', () => { it('PATCH invitation-acceptance', async () => { const { code } = invitation; - const otp = await createOtp(config, otpService, user, category); + const otp = await createOtp(config, otpService, user, userCategory); const { passcode } = otp; @@ -114,7 +170,7 @@ describe('InvitationController (e2e)', () => { it('GET invitation-acceptance', async () => { const { code } = invitation; - const otp = await createOtp(config, otpService, user, category); + const otp = await createOtp(config, otpService, user, userCategory); const { passcode } = otp; @@ -126,12 +182,12 @@ describe('InvitationController (e2e)', () => { it('GET invitation', async () => { const invitationCreateDto = { email: user.email, - category, + category: userCategory, payload, } as InvitationCreateDto; - const invite1 = await createInvite(invitationCreateDto); - const invite2 = await createInvite(invitationCreateDto); - const invite3 = await createInvite(invitationCreateDto); + const invite1 = await createInvite(app, invitationCreateDto); + const invite2 = await createInvite(app, invitationCreateDto); + const invite3 = await createInvite(app, invitationCreateDto); const response = await supertest(app.getHttpServer()) .get(`/invitation?s={"email": "${invitationCreateDto.email}"}`) @@ -147,9 +203,9 @@ describe('InvitationController (e2e)', () => { }); it('GET invitation/:id', async () => { - const invitation = await createInvite({ + const invitation = await createInvite(app, { email: user.email, - category, + category: userCategory, payload, }); @@ -163,9 +219,9 @@ describe('InvitationController (e2e)', () => { }); it('DELETE invitation/:id', async () => { - const invitation = await createInvite({ + const invitation = await createInvite(app, { email: user.email, - category, + category: userCategory, payload, }); @@ -177,20 +233,21 @@ describe('InvitationController (e2e)', () => { .get(`/invitation/${invitation.id}`) .expect(404); }); - - const createInvite = async ( - invitationCreateDto: InvitationCreateDto, - ): Promise => { - const response = await supertest(app.getHttpServer()) - .post('/invitation') - .send(invitationCreateDto) - .expect(201); - - return response.body as InvitationDto; - }; }); }); +const createInvite = async ( + app: INestApplication, + invitationCreateDto: InvitationCreateDto, +): Promise => { + const response = await supertest(app.getHttpServer()) + .post('/invitation') + .send(invitationCreateDto) + .expect(201); + + return response.body as InvitationDto; +}; + const createOtp = async ( config: ConfigType, otpService: OtpService, diff --git a/packages/nestjs-invitation/src/controllers/invitation.controller.ts b/packages/nestjs-invitation/src/controllers/invitation.controller.ts index 6899e9d0d..93747b8ce 100644 --- a/packages/nestjs-invitation/src/controllers/invitation.controller.ts +++ b/packages/nestjs-invitation/src/controllers/invitation.controller.ts @@ -103,6 +103,7 @@ export class InvitationController email, category, code: randomUUID(), + constraints: payload, }); if (user !== undefined && invite !== undefined) { diff --git a/packages/nestjs-invitation/src/entities/invitation-postgres.entity.ts b/packages/nestjs-invitation/src/entities/invitation-postgres.entity.ts index 86f5ced2c..7fe856478 100644 --- a/packages/nestjs-invitation/src/entities/invitation-postgres.entity.ts +++ b/packages/nestjs-invitation/src/entities/invitation-postgres.entity.ts @@ -8,8 +8,9 @@ import { import { AuditPostgresEmbed } from '@concepta/typeorm-common'; import { InvitationEntityInterface } from '../interfaces/invitation.entity.interface'; +import { LiteralObject } from '@nestjs/common'; -//TODO check this entity later +// TODO check this entity later export abstract class InvitationPostgresEntity implements InvitationEntityInterface { @@ -31,5 +32,8 @@ export abstract class InvitationPostgresEntity @Column() category!: string; + @Column({ type: 'jsonb' }) + constraints?: LiteralObject; + user!: ReferenceIdInterface; } diff --git a/packages/nestjs-invitation/src/entities/invitation-sqlite.entity.ts b/packages/nestjs-invitation/src/entities/invitation-sqlite.entity.ts index ab404cb78..cbb5ba639 100644 --- a/packages/nestjs-invitation/src/entities/invitation-sqlite.entity.ts +++ b/packages/nestjs-invitation/src/entities/invitation-sqlite.entity.ts @@ -8,6 +8,7 @@ import { import { AuditSqlLiteEmbed } from '@concepta/typeorm-common'; import { InvitationEntityInterface } from '../interfaces/invitation.entity.interface'; +import { LiteralObject } from '@nestjs/common'; //TODO check this entity later export abstract class InvitationSqliteEntity @@ -31,5 +32,8 @@ export abstract class InvitationSqliteEntity @Column() category!: string; + @Column({ type: 'simple-json', nullable: true }) + constraints?: LiteralObject; + user!: ReferenceIdInterface; } diff --git a/packages/nestjs-invitation/src/services/invitation-acceptance.service.spec.ts b/packages/nestjs-invitation/src/services/invitation-acceptance.service.spec.ts index d6e714697..f4fe916e3 100644 --- a/packages/nestjs-invitation/src/services/invitation-acceptance.service.spec.ts +++ b/packages/nestjs-invitation/src/services/invitation-acceptance.service.spec.ts @@ -1,7 +1,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { getDataSourceToken } from '@nestjs/typeorm'; -import { OtpInterface, UserInterface } from '@concepta/ts-common'; +import { + INVITATION_MODULE_CATEGORY_USER_KEY, + OtpInterface, + UserInterface, +} from '@concepta/ts-common'; import { UserEntityInterface } from '@concepta/nestjs-user'; import { OtpService } from '@concepta/nestjs-otp'; import { UserFactory } from '@concepta/nestjs-user/src/seeding'; @@ -10,18 +14,16 @@ import { EmailService } from '@concepta/nestjs-email'; import { EventDispatchService } from '@concepta/nestjs-event'; import { INVITATION_MODULE_SETTINGS_TOKEN } from '../invitation.constants'; - import { InvitationFactory } from '../invitation.factory'; import { InvitationSettingsInterface } from '../interfaces/invitation-settings.interface'; import { InvitationEntityInterface } from '../interfaces/invitation.entity.interface'; import { InvitationAcceptanceService } from './invitation-acceptance.service'; - import { AppModuleFixture } from '../__fixtures__/app.module.fixture'; import { InvitationEntityFixture } from '../__fixtures__/invitation/entities/invitation.entity.fixture'; import { UserEntityFixture } from '../__fixtures__/user/entities/user-entity.fixture'; describe(InvitationAcceptanceService, () => { - const category = 'invitation'; + const category = INVITATION_MODULE_CATEGORY_USER_KEY; let spyEmailService: jest.SpyInstance; let spyEventDispatchService: jest.SpyInstance; diff --git a/packages/nestjs-invitation/src/services/invitation-acceptance.service.ts b/packages/nestjs-invitation/src/services/invitation-acceptance.service.ts index 6b1199b55..cf2c18888 100644 --- a/packages/nestjs-invitation/src/services/invitation-acceptance.service.ts +++ b/packages/nestjs-invitation/src/services/invitation-acceptance.service.ts @@ -100,7 +100,7 @@ export class InvitationAcceptanceService extends BaseService { const invitationAcceptedEventAsync = new InvitationAcceptedEventAsync({ - ...invitationDto, + invitation: invitationDto, data: payload, queryOptions, }); @@ -109,7 +109,7 @@ export class InvitationAcceptanceService extends BaseService it === true); + return eventResult.every((it) => it === true); } async sendEmail(email: string): Promise { diff --git a/packages/nestjs-invitation/src/services/invitation-revocation.service.spec.ts b/packages/nestjs-invitation/src/services/invitation-revocation.service.spec.ts index a47684b50..6d4ec2f66 100644 --- a/packages/nestjs-invitation/src/services/invitation-revocation.service.spec.ts +++ b/packages/nestjs-invitation/src/services/invitation-revocation.service.spec.ts @@ -2,27 +2,25 @@ import { Repository } from 'typeorm'; import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { getDataSourceToken } from '@nestjs/typeorm'; - import { UserEntityInterface } from '@concepta/nestjs-user'; import { OtpService } from '@concepta/nestjs-otp'; import { EmailService } from '@concepta/nestjs-email'; import { UserFactory } from '@concepta/nestjs-user/src/seeding'; import { SeedingSource } from '@concepta/typeorm-seeding'; import { getDynamicRepositoryToken } from '@concepta/nestjs-typeorm-ext'; +import { INVITATION_MODULE_CATEGORY_USER_KEY } from '@concepta/ts-common'; import { INVITATION_MODULE_INVITATION_ENTITY_KEY } from '../invitation.constants'; - import { InvitationFactory } from '../invitation.factory'; import { InvitationSendService } from './invitation-send.service'; import { InvitationRevocationService } from './invitation-revocation.service'; import { InvitationEntityInterface } from '../interfaces/invitation.entity.interface'; - import { AppModuleFixture } from '../__fixtures__/app.module.fixture'; import { InvitationEntityFixture } from '../__fixtures__/invitation/entities/invitation.entity.fixture'; import { UserEntityFixture } from '../__fixtures__/user/entities/user-entity.fixture'; describe(InvitationRevocationService, () => { - const category = 'invitation'; + const category = INVITATION_MODULE_CATEGORY_USER_KEY; let spyEmailService: jest.SpyInstance; diff --git a/packages/nestjs-invitation/src/services/invitation-send.service.spec.ts b/packages/nestjs-invitation/src/services/invitation-send.service.spec.ts index 36a2f4630..976fa8133 100644 --- a/packages/nestjs-invitation/src/services/invitation-send.service.spec.ts +++ b/packages/nestjs-invitation/src/services/invitation-send.service.spec.ts @@ -3,17 +3,16 @@ import { Repository } from 'typeorm'; import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { getDataSourceToken } from '@nestjs/typeorm'; - import { UserEntityInterface } from '@concepta/nestjs-user'; import { EmailService } from '@concepta/nestjs-email'; import { getDynamicRepositoryToken } from '@concepta/nestjs-typeorm-ext'; import { SeedingSource } from '@concepta/typeorm-seeding'; import { UserFactory } from '@concepta/nestjs-user/src/seeding'; +import { INVITATION_MODULE_CATEGORY_USER_KEY } from '@concepta/ts-common'; import { INVITATION_MODULE_SETTINGS_TOKEN } from '../invitation.constants'; import { InvitationSendService } from './invitation-send.service'; import { InvitationSettingsInterface } from '../interfaces/invitation-settings.interface'; - import { AppModuleFixture } from '../__fixtures__/app.module.fixture'; import { UserEntityFixture } from '../__fixtures__/user/entities/user-entity.fixture'; import { UserOtpEntityFixture } from '../__fixtures__/user/entities/user-otp-entity.fixture'; @@ -73,14 +72,18 @@ describe(InvitationSendService, () => { it('Should send invitation email', async () => { const inviteCode = randomUUID(); - await invitationSendService.send(testUser, inviteCode, 'invitation'); + await invitationSendService.send( + testUser, + inviteCode, + INVITATION_MODULE_CATEGORY_USER_KEY, + ); const otps = await userOtpRepo.find({ where: { assignee: { id: testUser.id } }, }); expect(otps.length).toEqual(1); - expect(otps[0].category).toEqual('invitation'); + expect(otps[0].category).toEqual(INVITATION_MODULE_CATEGORY_USER_KEY); expect(spyEmailService).toHaveBeenCalledTimes(1); const { passcode, expirationDate } = otps[0]; diff --git a/packages/nestjs-org/package.json b/packages/nestjs-org/package.json index 8c9c8bdcb..7a90d5146 100644 --- a/packages/nestjs-org/package.json +++ b/packages/nestjs-org/package.json @@ -15,6 +15,7 @@ "@concepta/nestjs-access-control": "^4.0.0-alpha.22", "@concepta/nestjs-common": "^4.0.0-alpha.22", "@concepta/nestjs-crud": "^4.0.0-alpha.22", + "@concepta/nestjs-event": "^4.0.0-alpha.22", "@concepta/nestjs-typeorm-ext": "^4.0.0-alpha.22", "@concepta/ts-common": "^4.0.0-alpha.22", "@concepta/ts-core": "^4.0.0-alpha.22", @@ -25,6 +26,8 @@ }, "devDependencies": { "@concepta/typeorm-seeding": "^4.0.0-beta.0", + "@concepta/nestjs-invitation": "^4.0.0-alpha.22", + "@concepta/nestjs-user": "^4.0.0-alpha.22", "@faker-js/faker": "^6.0.0-alpha.6", "@nestjs/testing": "^9.0.0", "@nestjs/typeorm": "^9.0.0", diff --git a/packages/nestjs-org/src/__fixtures__/invitation-accepted.event.ts b/packages/nestjs-org/src/__fixtures__/invitation-accepted.event.ts new file mode 100644 index 000000000..c94838c90 --- /dev/null +++ b/packages/nestjs-org/src/__fixtures__/invitation-accepted.event.ts @@ -0,0 +1,7 @@ +import { EventAsync } from '@concepta/nestjs-event'; +import { InvitationAcceptedEventPayloadInterface } from '@concepta/ts-common'; + +export class InvitationAcceptedEventAsync extends EventAsync< + InvitationAcceptedEventPayloadInterface, + boolean +> {} diff --git a/packages/nestjs-org/src/__fixtures__/invitation.entity.fixture.ts b/packages/nestjs-org/src/__fixtures__/invitation.entity.fixture.ts new file mode 100644 index 000000000..fbe5ba4d6 --- /dev/null +++ b/packages/nestjs-org/src/__fixtures__/invitation.entity.fixture.ts @@ -0,0 +1,11 @@ +import { Entity, ManyToOne } from 'typeorm'; +import { InvitationSqliteEntity } from '@concepta/nestjs-invitation'; +import { UserEntityFixture } from './user-entity.fixture'; + +@Entity() +export class InvitationEntityFixture extends InvitationSqliteEntity { + @ManyToOne(() => UserEntityFixture, (user) => user.invitations, { + nullable: false, + }) + user!: UserEntityFixture; +} diff --git a/packages/nestjs-org/src/__fixtures__/org-entity.fixture.ts b/packages/nestjs-org/src/__fixtures__/org-entity.fixture.ts index f3343ac56..d10b93535 100644 --- a/packages/nestjs-org/src/__fixtures__/org-entity.fixture.ts +++ b/packages/nestjs-org/src/__fixtures__/org-entity.fixture.ts @@ -1,6 +1,8 @@ -import { Entity, ManyToOne } from 'typeorm'; +import { Entity, ManyToOne, OneToMany } from 'typeorm'; + import { OrgSqliteEntity } from '../entities/org-sqlite.entity'; import { OwnerEntityFixture } from './owner-entity.fixture'; +import { OrgMemberEntityFixture } from './org-member.entity.fixture'; /** * Org Entity Fixture @@ -9,4 +11,7 @@ import { OwnerEntityFixture } from './owner-entity.fixture'; export class OrgEntityFixture extends OrgSqliteEntity { @ManyToOne(() => OwnerEntityFixture, (user) => user.orgs, { nullable: false }) owner!: OwnerEntityFixture; + + @OneToMany(() => OrgMemberEntityFixture, (orgMember) => orgMember.org) + orgMembers!: OrgMemberEntityFixture[]; } diff --git a/packages/nestjs-org/src/__fixtures__/org-member.entity.fixture.ts b/packages/nestjs-org/src/__fixtures__/org-member.entity.fixture.ts new file mode 100644 index 000000000..975acb840 --- /dev/null +++ b/packages/nestjs-org/src/__fixtures__/org-member.entity.fixture.ts @@ -0,0 +1,18 @@ +import { Entity, ManyToOne } from 'typeorm'; + +import { UserEntityFixture } from './user-entity.fixture'; +import { OrgEntityFixture } from './org-entity.fixture'; +import { OrgMemberSqliteEntity } from '../entities/org-member-sqlite.entity'; + +@Entity() +export class OrgMemberEntityFixture extends OrgMemberSqliteEntity { + @ManyToOne(() => OrgEntityFixture, (org) => org.orgMembers, { + nullable: false, + }) + org!: OrgEntityFixture; + + @ManyToOne(() => UserEntityFixture, (user) => user.orgMembers, { + nullable: false, + }) + user!: UserEntityFixture; +} diff --git a/packages/nestjs-org/src/__fixtures__/user-entity.fixture.ts b/packages/nestjs-org/src/__fixtures__/user-entity.fixture.ts new file mode 100644 index 000000000..b7b8dadce --- /dev/null +++ b/packages/nestjs-org/src/__fixtures__/user-entity.fixture.ts @@ -0,0 +1,14 @@ +import { Entity, OneToMany } from 'typeorm'; +import { UserSqliteEntity } from '@concepta/nestjs-user'; + +import { OrgMemberEntityFixture } from './org-member.entity.fixture'; +import { InvitationEntityFixture } from './invitation.entity.fixture'; + +@Entity() +export class UserEntityFixture extends UserSqliteEntity { + @OneToMany(() => OrgMemberEntityFixture, (orgMember) => orgMember.org) + orgMembers!: OrgMemberEntityFixture[]; + + @OneToMany(() => InvitationEntityFixture, (invitation) => invitation.user) + invitations?: InvitationEntityFixture[]; +} diff --git a/packages/nestjs-org/src/entities/org-member-postgres.entity.ts b/packages/nestjs-org/src/entities/org-member-postgres.entity.ts new file mode 100644 index 000000000..5e9d0e573 --- /dev/null +++ b/packages/nestjs-org/src/entities/org-member-postgres.entity.ts @@ -0,0 +1,33 @@ +import { Column, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + AuditInterface, + ReferenceId, + ReferenceIdInterface, +} from '@concepta/ts-core'; +import { AuditPostgresEmbed } from '@concepta/typeorm-common'; + +import { OrgMemberEntityInterface } from '../interfaces/org-member-entity.interface'; + +@Unique(['userId', 'orgId']) +export abstract class OrgMemberPostgresEntity + implements OrgMemberEntityInterface +{ + @PrimaryGeneratedColumn('uuid') + id!: ReferenceId; + + @Column(() => AuditPostgresEmbed, {}) + audit!: AuditInterface; + + @Column('boolean', { default: true }) + active = true; + + @Column() + userId!: ReferenceId; + + @Column() + orgId!: ReferenceId; + + user!: ReferenceIdInterface; + + org!: ReferenceIdInterface; +} diff --git a/packages/nestjs-org/src/entities/org-member-sqlite.entity.ts b/packages/nestjs-org/src/entities/org-member-sqlite.entity.ts new file mode 100644 index 000000000..1a1788988 --- /dev/null +++ b/packages/nestjs-org/src/entities/org-member-sqlite.entity.ts @@ -0,0 +1,33 @@ +import { Column, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + AuditInterface, + ReferenceId, + ReferenceIdInterface, +} from '@concepta/ts-core'; +import { AuditSqlLiteEmbed } from '@concepta/typeorm-common'; + +import { OrgMemberEntityInterface } from '../interfaces/org-member-entity.interface'; + +@Unique(['userId', 'orgId']) +export abstract class OrgMemberSqliteEntity + implements OrgMemberEntityInterface +{ + @PrimaryGeneratedColumn('uuid') + id!: ReferenceId; + + @Column(() => AuditSqlLiteEmbed, {}) + audit!: AuditInterface; + + @Column('boolean', { default: true }) + active = true; + + @Column() + userId!: ReferenceId; + + @Column() + orgId!: ReferenceId; + + user!: ReferenceIdInterface; + + org!: ReferenceIdInterface; +} diff --git a/packages/nestjs-org/src/exceptions/org-member.exception.ts b/packages/nestjs-org/src/exceptions/org-member.exception.ts new file mode 100644 index 000000000..55f9bbf0a --- /dev/null +++ b/packages/nestjs-org/src/exceptions/org-member.exception.ts @@ -0,0 +1,9 @@ +import { ExceptionInterface } from '@concepta/ts-core'; + +export class OrgMemberException extends Error implements ExceptionInterface { + errorCode = 'ORG_MEMBER_ERROR'; + + constructor(message: string) { + super(message); + } +} diff --git a/packages/nestjs-org/src/exceptions/org-not-found.exception.ts b/packages/nestjs-org/src/exceptions/org-not-found.exception.ts new file mode 100644 index 000000000..5687efb71 --- /dev/null +++ b/packages/nestjs-org/src/exceptions/org-not-found.exception.ts @@ -0,0 +1,9 @@ +import { ExceptionInterface } from '@concepta/ts-core'; + +export class OrgNotFoundException extends Error implements ExceptionInterface { + errorCode = 'ORG_NOT_FOUND_ERROR'; + + constructor(message = 'The org was not found') { + super(message); + } +} diff --git a/packages/nestjs-org/src/interfaces/org-entities-options.interface.ts b/packages/nestjs-org/src/interfaces/org-entities-options.interface.ts index f17bec6f7..a2048fa1e 100644 --- a/packages/nestjs-org/src/interfaces/org-entities-options.interface.ts +++ b/packages/nestjs-org/src/interfaces/org-entities-options.interface.ts @@ -1,9 +1,15 @@ import { TypeOrmExtEntityOptionInterface } from '@concepta/nestjs-typeorm-ext'; -import { ORG_MODULE_ORG_ENTITY_KEY } from '../org.constants'; + +import { + ORG_MODULE_ORG_MEMBER_ENTITY_KEY, + ORG_MODULE_ORG_ENTITY_KEY, +} from '../org.constants'; import { OrgEntityInterface } from './org-entity.interface'; +import { OrgMemberEntityInterface } from './org-member-entity.interface'; export interface OrgEntitiesOptionsInterface { entities: { [ORG_MODULE_ORG_ENTITY_KEY]: TypeOrmExtEntityOptionInterface; + [ORG_MODULE_ORG_MEMBER_ENTITY_KEY]: TypeOrmExtEntityOptionInterface; }; } diff --git a/packages/nestjs-org/src/interfaces/org-member-creatable.interface.ts b/packages/nestjs-org/src/interfaces/org-member-creatable.interface.ts new file mode 100644 index 000000000..dd8d6e769 --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-creatable.interface.ts @@ -0,0 +1,4 @@ +import { OrgMemberInterface } from '@concepta/ts-common'; + +export interface OrgMemberCreatableInterface + extends Pick {} diff --git a/packages/nestjs-org/src/interfaces/org-member-entity.interface.ts b/packages/nestjs-org/src/interfaces/org-member-entity.interface.ts new file mode 100644 index 000000000..196cd1af5 --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-entity.interface.ts @@ -0,0 +1,3 @@ +import { OrgMemberInterface } from '@concepta/ts-common'; + +export interface OrgMemberEntityInterface extends OrgMemberInterface {} diff --git a/packages/nestjs-org/src/interfaces/org-member-lookup-service.interface.ts b/packages/nestjs-org/src/interfaces/org-member-lookup-service.interface.ts new file mode 100644 index 000000000..11b5e5183 --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-lookup-service.interface.ts @@ -0,0 +1,13 @@ +import { + LookupIdInterface, + ReferenceId, + ReferenceIdInterface, +} from '@concepta/ts-core'; +import { QueryOptionsInterface } from '@concepta/typeorm-common'; + +export interface OrgMemberLookupServiceInterface + extends LookupIdInterface< + ReferenceId, + ReferenceIdInterface, + QueryOptionsInterface + > {} diff --git a/packages/nestjs-org/src/interfaces/org-member-mutate-service.interface.ts b/packages/nestjs-org/src/interfaces/org-member-mutate-service.interface.ts new file mode 100644 index 000000000..b6dc85b3d --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-mutate-service.interface.ts @@ -0,0 +1,10 @@ +import { CreateOneInterface } from '@concepta/ts-core'; + +import { OrgMemberEntityInterface } from './org-member-entity.interface'; +import { OrgMemberCreatableInterface } from './org-member-creatable.interface'; + +export interface OrgMemberMutateServiceInterface + extends CreateOneInterface< + OrgMemberCreatableInterface, + OrgMemberEntityInterface + > {} diff --git a/packages/nestjs-org/src/interfaces/org-member-service.interface.ts b/packages/nestjs-org/src/interfaces/org-member-service.interface.ts new file mode 100644 index 000000000..63f510176 --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-service.interface.ts @@ -0,0 +1,14 @@ +import { OrgMemberEntityInterface } from './org-member-entity.interface'; +import { OrgMemberCreatableInterface } from './org-member-creatable.interface'; +import { QueryOptionsInterface } from '@concepta/typeorm-common'; + +export interface OrgMemberServiceInterface { + add( + orgMember: OrgMemberCreatableInterface, + queryOptions?: QueryOptionsInterface, + ): Promise; + remove( + id: string, + queryOptions?: QueryOptionsInterface, + ): Promise; +} diff --git a/packages/nestjs-org/src/interfaces/org-member-updatable.interface.ts b/packages/nestjs-org/src/interfaces/org-member-updatable.interface.ts new file mode 100644 index 000000000..2a52c23b5 --- /dev/null +++ b/packages/nestjs-org/src/interfaces/org-member-updatable.interface.ts @@ -0,0 +1,4 @@ +import { OrgMemberInterface } from '@concepta/ts-common'; + +export interface OrgMemberUpdatableInterface + extends Pick {} diff --git a/packages/nestjs-org/src/interfaces/org-settings.interface.ts b/packages/nestjs-org/src/interfaces/org-settings.interface.ts index 50ec426ec..3eb9c2aa8 100644 --- a/packages/nestjs-org/src/interfaces/org-settings.interface.ts +++ b/packages/nestjs-org/src/interfaces/org-settings.interface.ts @@ -1 +1,11 @@ -export interface OrgSettingsInterface {} +import { + EventAsyncInterface, + EventClassInterface, +} from '@concepta/nestjs-event'; +import { InvitationAcceptedEventPayloadInterface } from '@concepta/ts-common'; + +export interface OrgSettingsInterface { + invitationRequestEvent?: EventClassInterface< + EventAsyncInterface + >; +} diff --git a/packages/nestjs-org/src/listeners/invitation-accepted-listener.spec.ts b/packages/nestjs-org/src/listeners/invitation-accepted-listener.spec.ts new file mode 100644 index 000000000..377fcf620 --- /dev/null +++ b/packages/nestjs-org/src/listeners/invitation-accepted-listener.spec.ts @@ -0,0 +1,140 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; +import { CrudModule } from '@concepta/nestjs-crud'; +import { INVITATION_MODULE_CATEGORY_ORG_KEY } from '@concepta/ts-common'; +import { SeedingSource } from '@concepta/typeorm-seeding'; +import { EventDispatchService, EventModule } from '@concepta/nestjs-event'; +import { UserEntityInterface, UserModule } from '@concepta/nestjs-user'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { InvitationEntityInterface } from '@concepta/nestjs-invitation/src/interfaces/invitation.entity.interface'; +import { UserFactory } from '@concepta/nestjs-user/src/user.factory'; +import { InvitationFactory } from '@concepta/nestjs-invitation/src/invitation.factory'; + +import { InvitationAcceptedListener } from './invitation-accepted-listener'; +import { OrgEntityFixture } from '../__fixtures__/org-entity.fixture'; +import { OwnerEntityFixture } from '../__fixtures__/owner-entity.fixture'; +import { OrgMemberEntityFixture } from '../__fixtures__/org-member.entity.fixture'; +import { UserEntityFixture } from '../__fixtures__/user-entity.fixture'; +import { OrgModule } from '../org.module'; +import { OwnerLookupServiceFixture } from '../__fixtures__/owner-lookup-service.fixture'; +import { OwnerModuleFixture } from '../__fixtures__/owner.module.fixture'; +import { InvitationAcceptedEventAsync } from '../__fixtures__/invitation-accepted.event'; +import { InvitationEntityFixture } from '../__fixtures__/invitation.entity.fixture'; +import { OrgFactory } from '../seeding/org.factory'; +import { OrgEntityInterface } from '../interfaces/org-entity.interface'; +import { OwnerFactoryFixture } from '../__fixtures__/owner-factory.fixture'; + +describe(InvitationAcceptedListener, () => { + const category = INVITATION_MODULE_CATEGORY_ORG_KEY; + let app: INestApplication; + let eventDispatchService: EventDispatchService; + let seedingSource: SeedingSource; + let testUser: UserEntityInterface; + let testOwner: OwnerEntityFixture; + let testOrg: OrgEntityInterface; + let testInvitation: InvitationEntityInterface; + + beforeEach(async () => { + const testingModule: TestingModule = await Test.createTestingModule({ + imports: [ + EventModule.forRoot({}), + TypeOrmExtModule.forRoot({ + type: 'sqlite', + database: ':memory:', + logging: 'all', + synchronize: true, + entities: [ + OrgEntityFixture, + OwnerEntityFixture, + OrgMemberEntityFixture, + UserEntityFixture, + InvitationEntityFixture, + ], + }), + UserModule.forRoot({ + entities: { + user: { + entity: UserEntityFixture, + }, + }, + }), + OrgModule.forRootAsync({ + inject: [OwnerLookupServiceFixture], + useFactory: (ownerLookupService: OwnerLookupServiceFixture) => ({ + ownerLookupService, + settings: { + invitationRequestEvent: InvitationAcceptedEventAsync, + }, + }), + entities: { + org: { + entity: OrgEntityFixture, + }, + orgMember: { + entity: OrgMemberEntityFixture, + }, + }, + }), + CrudModule.forRoot({}), + OwnerModuleFixture.register(), + ], + }).compile(); + app = testingModule.createNestApplication(); + await app.init(); + + eventDispatchService = + testingModule.get(EventDispatchService); + + seedingSource = new SeedingSource({ + dataSource: testingModule.get(getDataSourceToken()), + }); + + const orgFactory = new OrgFactory({ + entity: OrgEntityFixture, + seedingSource, + }); + + const userFactory = new UserFactory({ + entity: UserEntityFixture, + seedingSource, + }); + const ownerFactory = new OwnerFactoryFixture({ + entity: OwnerEntityFixture, + seedingSource, + }); + + const invitationFactory = new InvitationFactory({ + entity: InvitationEntityFixture, + seedingSource, + }); + + testUser = await userFactory.create(); + testOwner = await ownerFactory.create(); + testOrg = await orgFactory.create({ + owner: testOwner, + }); + testInvitation = await invitationFactory.create({ + user: testUser, + category, + constraints: { orgId: testOrg.id }, + }); + }); + + it('event should be listened', async () => { + const invitationAcceptedEventAsync = new InvitationAcceptedEventAsync({ + invitation: testInvitation, + data: { + userId: testInvitation.user.id, + }, + }); + + const eventResult = await eventDispatchService.async( + invitationAcceptedEventAsync, + ); + + const result = eventResult.every((it) => it === true); + + expect(result); + }); +}); diff --git a/packages/nestjs-org/src/listeners/invitation-accepted-listener.ts b/packages/nestjs-org/src/listeners/invitation-accepted-listener.ts new file mode 100644 index 000000000..8e7055c89 --- /dev/null +++ b/packages/nestjs-org/src/listeners/invitation-accepted-listener.ts @@ -0,0 +1,78 @@ +import { + EventAsyncInterface, + EventListenerOn, + EventListenService, +} from '@concepta/nestjs-event'; +import { + INVITATION_MODULE_CATEGORY_ORG_KEY, + InvitationAcceptedEventPayloadInterface, +} from '@concepta/ts-common'; +import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common'; + +import { ORG_MODULE_SETTINGS_TOKEN } from '../org.constants'; +import { OrgSettingsInterface } from '../interfaces/org-settings.interface'; +import { OrgMemberException } from '../exceptions/org-member.exception'; +import { OrgMemberService } from '../services/org-member.service'; + +@Injectable() +export class InvitationAcceptedListener + extends EventListenerOn< + EventAsyncInterface + > + implements OnModuleInit +{ + constructor( + @Inject(ORG_MODULE_SETTINGS_TOKEN) + private settings: OrgSettingsInterface, + private orgMemberService: OrgMemberService, + @Optional() + @Inject(EventListenService) + private eventListenService?: EventListenService, + ) { + super(); + } + + onModuleInit() { + if (this.eventListenService && this.settings.invitationRequestEvent) { + this.eventListenService.on(this.settings.invitationRequestEvent, this); + } + } + + async listen( + event: EventAsyncInterface< + InvitationAcceptedEventPayloadInterface, + boolean + >, + ) { + // check only for invitation of type category + if ( + event?.payload?.invitation?.category === + INVITATION_MODULE_CATEGORY_ORG_KEY + ) { + const { userId } = event?.payload?.data ?? {}; + const { orgId } = event?.payload?.invitation?.constraints ?? {}; + + if (typeof userId !== 'string') { + throw new OrgMemberException( + 'The invitation accepted event payload received has invalid content. The payload must have the "userId" property.', + ); + } + + if (typeof orgId !== 'string') { + throw new OrgMemberException( + 'The org of invitation does not have orgId in constraints', + ); + } + + await this.orgMemberService.add( + { userId, orgId }, + event.payload?.queryOptions, + ); + + return true; + } + + // return true by default + return true; + } +} diff --git a/packages/nestjs-org/src/org.constants.ts b/packages/nestjs-org/src/org.constants.ts index e88444a11..9b1482186 100644 --- a/packages/nestjs-org/src/org.constants.ts +++ b/packages/nestjs-org/src/org.constants.ts @@ -4,3 +4,4 @@ export const ORG_MODULE_DEFAULT_SETTINGS_TOKEN = export const ORG_MODULE_OWNER_LOOKUP_SERVICE_TOKEN = 'ORG_MODULE_OWNER_LOOKUP_SERVICE_TOKEN'; export const ORG_MODULE_ORG_ENTITY_KEY = 'org'; +export const ORG_MODULE_ORG_MEMBER_ENTITY_KEY = 'orgMember'; diff --git a/packages/nestjs-org/src/org.controller.e2e-spec.ts b/packages/nestjs-org/src/org.controller.e2e-spec.ts index 420b6332b..36332c14a 100644 --- a/packages/nestjs-org/src/org.controller.e2e-spec.ts +++ b/packages/nestjs-org/src/org.controller.e2e-spec.ts @@ -5,15 +5,18 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import { TypeOrmExtModule } from '@concepta/nestjs-typeorm-ext'; import { CrudModule } from '@concepta/nestjs-crud'; import { SeedingSource } from '@concepta/typeorm-seeding'; + import { OrgFactory } from './seeding/org.factory'; import { OrgSeeder } from './seeding/org.seeder'; import { OrgModule } from './org.module'; - import { OrgEntityFixture } from './__fixtures__/org-entity.fixture'; import { OwnerEntityFixture } from './__fixtures__/owner-entity.fixture'; import { OwnerLookupServiceFixture } from './__fixtures__/owner-lookup-service.fixture'; import { OwnerModuleFixture } from './__fixtures__/owner.module.fixture'; import { OwnerFactoryFixture } from './__fixtures__/owner-factory.fixture'; +import { OrgMemberEntityFixture } from './__fixtures__/org-member.entity.fixture'; +import { UserEntityFixture } from './__fixtures__/user-entity.fixture'; +import { InvitationEntityFixture } from './__fixtures__/invitation.entity.fixture'; describe('OrgController (e2e)', () => { describe('Rest', () => { @@ -27,7 +30,13 @@ describe('OrgController (e2e)', () => { type: 'sqlite', database: ':memory:', synchronize: true, - entities: [OrgEntityFixture, OwnerEntityFixture], + entities: [ + OrgEntityFixture, + OwnerEntityFixture, + OrgMemberEntityFixture, + UserEntityFixture, + InvitationEntityFixture, + ], }), OrgModule.registerAsync({ inject: [OwnerLookupServiceFixture], @@ -38,6 +47,9 @@ describe('OrgController (e2e)', () => { org: { entity: OrgEntityFixture, }, + orgMember: { + entity: OrgMemberEntityFixture, + }, }, }), CrudModule.forRoot({}), diff --git a/packages/nestjs-org/src/org.module-definition.ts b/packages/nestjs-org/src/org.module-definition.ts index 052ffdbc4..6f6145f91 100644 --- a/packages/nestjs-org/src/org.module-definition.ts +++ b/packages/nestjs-org/src/org.module-definition.ts @@ -17,20 +17,18 @@ import { ORG_MODULE_OWNER_LOOKUP_SERVICE_TOKEN, ORG_MODULE_ORG_ENTITY_KEY, } from './org.constants'; - import { OrgOptionsInterface } from './interfaces/org-options.interface'; import { OrgOptionsExtrasInterface } from './interfaces/org-options-extras.interface'; import { OrgEntitiesOptionsInterface } from './interfaces/org-entities-options.interface'; import { OrgSettingsInterface } from './interfaces/org-settings.interface'; import { OrgEntityInterface } from './interfaces/org-entity.interface'; import { OrgOwnerLookupServiceInterface } from './interfaces/org-owner-lookup-service.interface'; - import { OrgLookupService } from './services/org-lookup.service'; import { OrgMutateService } from './services/org-mutate.service'; import { OrgCrudService } from './services/org-crud.service'; import { OrgController } from './org.controller'; - import { orgDefaultConfig } from './config/org-default.config'; +import { InvitationAcceptedListener } from './listeners/invitation-accepted-listener'; const RAW_OPTIONS_TOKEN = Symbol('__ORG_MODULE_RAW_OPTIONS_TOKEN__'); @@ -85,6 +83,7 @@ export function createOrgProviders(options: { return [ ...(options.providers ?? []), OrgCrudService, + InvitationAcceptedListener, createOrgSettingsProvider(options.overrides), createOrgOwnerLookupServiceProvider(options.overrides), createOrgLookupServiceProvider(options.overrides), diff --git a/packages/nestjs-org/src/org.module.spec.ts b/packages/nestjs-org/src/org.module.spec.ts index 18a1981fd..666dd2317 100644 --- a/packages/nestjs-org/src/org.module.spec.ts +++ b/packages/nestjs-org/src/org.module.spec.ts @@ -20,6 +20,9 @@ import { OrgEntityFixture } from './__fixtures__/org-entity.fixture'; import { OwnerEntityFixture } from './__fixtures__/owner-entity.fixture'; import { OwnerLookupServiceFixture } from './__fixtures__/owner-lookup-service.fixture'; import { OwnerModuleFixture } from './__fixtures__/owner.module.fixture'; +import { OrgMemberEntityFixture } from './__fixtures__/org-member.entity.fixture'; +import { UserEntityFixture } from './__fixtures__/user-entity.fixture'; +import { InvitationEntityFixture } from './__fixtures__/invitation.entity.fixture'; describe('OrgModule', () => { let orgModule: OrgModule; @@ -37,7 +40,13 @@ describe('OrgModule', () => { TypeOrmExtModule.forRoot({ type: 'sqlite', database: ':memory:', - entities: [OrgEntityFixture, OwnerEntityFixture], + entities: [ + OrgEntityFixture, + OwnerEntityFixture, + OrgMemberEntityFixture, + UserEntityFixture, + InvitationEntityFixture, + ], }), OrgModule.forRootAsync({ inject: [OwnerLookupServiceFixture], @@ -48,6 +57,9 @@ describe('OrgModule', () => { org: { entity: OrgEntityFixture, }, + orgMember: { + entity: OrgMemberEntityFixture, + }, }, }), CrudModule.forRoot({}), diff --git a/packages/nestjs-org/src/org.module.ts b/packages/nestjs-org/src/org.module.ts index f10848cd7..b1cc8d974 100644 --- a/packages/nestjs-org/src/org.module.ts +++ b/packages/nestjs-org/src/org.module.ts @@ -1,4 +1,5 @@ import { DynamicModule, Module } from '@nestjs/common'; + import { OrgController } from './org.controller'; import { OrgLookupService } from './services/org-lookup.service'; import { OrgCrudService } from './services/org-crud.service'; @@ -12,13 +13,28 @@ import { createOrgImports, createOrgProviders, } from './org.module-definition'; +import { OrgMemberMutateService } from './services/org-member-mutate.service'; +import { OrgMemberLookupService } from './services/org-member-lookup.service'; +import { OrgMemberService } from './services/org-member.service'; /** * Org Module */ @Module({ - providers: [OrgLookupService, OrgMutateService, OrgCrudService], - exports: [OrgLookupService, OrgMutateService, OrgCrudService], + providers: [ + OrgMemberService, + OrgLookupService, + OrgMutateService, + OrgCrudService, + OrgMemberLookupService, + OrgMemberMutateService, + ], + exports: [ + OrgLookupService, + OrgMutateService, + OrgCrudService, + OrgMemberService, + ], controllers: [OrgController], }) export class OrgModule extends OrgModuleClass { diff --git a/packages/nestjs-org/src/services/org-member-lookup.service.ts b/packages/nestjs-org/src/services/org-member-lookup.service.ts new file mode 100644 index 000000000..efdf71c8e --- /dev/null +++ b/packages/nestjs-org/src/services/org-member-lookup.service.ts @@ -0,0 +1,24 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { LookupService } from '@concepta/typeorm-common'; + +import { ORG_MODULE_ORG_MEMBER_ENTITY_KEY } from '../org.constants'; +import { OrgMemberEntityInterface } from '../interfaces/org-member-entity.interface'; +import { OrgMemberLookupServiceInterface } from '../interfaces/org-member-lookup-service.interface'; + +/** + * Org lookup service + */ +@Injectable() +export class OrgMemberLookupService + extends LookupService + implements OrgMemberLookupServiceInterface +{ + constructor( + @InjectDynamicRepository(ORG_MODULE_ORG_MEMBER_ENTITY_KEY) + repo: Repository, + ) { + super(repo); + } +} diff --git a/packages/nestjs-org/src/services/org-member-mutate.service.ts b/packages/nestjs-org/src/services/org-member-mutate.service.ts new file mode 100644 index 000000000..3bf8831d8 --- /dev/null +++ b/packages/nestjs-org/src/services/org-member-mutate.service.ts @@ -0,0 +1,34 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { MutateService } from '@concepta/typeorm-common'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { Type } from '@concepta/ts-core'; + +import { OrgMemberMutateServiceInterface } from '../interfaces/org-member-mutate-service.interface'; +import { OrgMemberEntityInterface } from '../interfaces/org-member-entity.interface'; +import { ORG_MODULE_ORG_MEMBER_ENTITY_KEY } from '../org.constants'; +import { OrgMemberCreatableInterface } from '../interfaces/org-member-creatable.interface'; +import { OrgMemberUpdatableInterface } from '../interfaces/org-member-updatable.interface'; + +/** + * User mutate service + */ +@Injectable() +export class OrgMemberMutateService + extends MutateService< + OrgMemberEntityInterface, + OrgMemberCreatableInterface, + OrgMemberUpdatableInterface + > + implements OrgMemberMutateServiceInterface +{ + constructor( + @InjectDynamicRepository(ORG_MODULE_ORG_MEMBER_ENTITY_KEY) + repo: Repository, + ) { + super(repo); + } + + protected createDto!: Type; + protected updateDto!: Type; +} diff --git a/packages/nestjs-org/src/services/org-member.service.ts b/packages/nestjs-org/src/services/org-member.service.ts new file mode 100644 index 000000000..bf7d8f63b --- /dev/null +++ b/packages/nestjs-org/src/services/org-member.service.ts @@ -0,0 +1,55 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectDynamicRepository } from '@concepta/nestjs-typeorm-ext'; +import { BaseService, QueryOptionsInterface } from '@concepta/typeorm-common'; + +import { ORG_MODULE_ORG_MEMBER_ENTITY_KEY } from '../org.constants'; +import { OrgMemberEntityInterface } from '../interfaces/org-member-entity.interface'; +import { OrgMemberServiceInterface } from '../interfaces/org-member-service.interface'; +import { OrgMemberCreatableInterface } from '../interfaces/org-member-creatable.interface'; +import { OrgLookupService } from './org-lookup.service'; +import { OrgMemberLookupService } from './org-member-lookup.service'; +import { OrgMemberMutateService } from './org-member-mutate.service'; +import { OrgMemberException } from '../exceptions/org-member.exception'; + +@Injectable() +export class OrgMemberService + extends BaseService + implements OrgMemberServiceInterface +{ + constructor( + @InjectDynamicRepository(ORG_MODULE_ORG_MEMBER_ENTITY_KEY) + repo: Repository, + private orgLookupService: OrgLookupService, + private orgMemberLookupService: OrgMemberLookupService, + private orgMemberMutateService: OrgMemberMutateService, + ) { + super(repo); + } + + async add( + orgMember: OrgMemberCreatableInterface, + queryOptions?: QueryOptionsInterface, + ): Promise { + const orgMemberFound = await this.orgMemberLookupService.findOne( + { where: orgMember }, + queryOptions, + ); + + if (orgMemberFound) { + const { userId, orgId } = orgMember; + throw new OrgMemberException( + `Can't create OrgMember, the the combination of userid: ${userId} and orgId: ${orgId} already exists`, + ); + } + + return await this.orgMemberMutateService.create(orgMember, queryOptions); + } + + async remove( + id: string, + queryOptions?: QueryOptionsInterface, + ): Promise { + return await this.orgMemberMutateService.remove({ id }, queryOptions); + } +} diff --git a/packages/nestjs-org/tsconfig.json b/packages/nestjs-org/tsconfig.json index 926303f4d..86fb29b06 100644 --- a/packages/nestjs-org/tsconfig.json +++ b/packages/nestjs-org/tsconfig.json @@ -27,6 +27,9 @@ }, { "path": "../nestjs-typeorm-ext" + }, + { + "path": "../nestjs-invitation" } ] } diff --git a/packages/nestjs-user/src/index.ts b/packages/nestjs-user/src/index.ts index c4528dec6..429068a09 100644 --- a/packages/nestjs-user/src/index.ts +++ b/packages/nestjs-user/src/index.ts @@ -13,3 +13,6 @@ export { UserLookupServiceInterface } from './interfaces/user-lookup-service.int export { UserMutateServiceInterface } from './interfaces/user-mutate-service.interface'; export { UserResource } from './user.types'; + +export { UserException } from './exceptions/user-exception'; +export { UserNotFoundException } from './exceptions/user-not-found-exception'; diff --git a/packages/nestjs-user/src/listeners/invitation-accepted-listener.ts b/packages/nestjs-user/src/listeners/invitation-accepted-listener.ts index 797ce4858..988cda419 100644 --- a/packages/nestjs-user/src/listeners/invitation-accepted-listener.ts +++ b/packages/nestjs-user/src/listeners/invitation-accepted-listener.ts @@ -3,7 +3,10 @@ import { EventListenerOn, EventListenService, } from '@concepta/nestjs-event'; -import { InvitationAcceptedEventPayloadInterface } from '@concepta/ts-common'; +import { + INVITATION_MODULE_CATEGORY_USER_KEY, + InvitationAcceptedEventPayloadInterface, +} from '@concepta/ts-common'; import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common'; import { USER_MODULE_SETTINGS_TOKEN } from '../user.constants'; @@ -45,8 +48,11 @@ export class InvitationAcceptedListener >, ) { // check only for invitation of type category - if (event.payload.category === 'invitation') { - const { userId, newPassword } = event?.payload.data ?? {}; + if ( + event?.payload?.invitation?.category === + INVITATION_MODULE_CATEGORY_USER_KEY + ) { + const { userId, newPassword } = event?.payload?.data ?? {}; if (typeof userId !== 'string' || typeof newPassword !== 'string') { throw new UserException( diff --git a/packages/ts-common/package.json b/packages/ts-common/package.json index 439188a05..aba060f51 100644 --- a/packages/ts-common/package.json +++ b/packages/ts-common/package.json @@ -12,6 +12,7 @@ "dist/**/!(*.spec|*.e2e-spec|*.fixture).{js,d.ts}" ], "dependencies": { + "@concepta/nestjs-common": "^4.0.0-alpha.22", "@concepta/nestjs-event": "^4.0.0-alpha.22", "@concepta/ts-core": "^4.0.0-alpha.22" } diff --git a/packages/ts-common/src/index.ts b/packages/ts-common/src/index.ts index b4b524a0e..bbbc44b96 100644 --- a/packages/ts-common/src/index.ts +++ b/packages/ts-common/src/index.ts @@ -16,6 +16,7 @@ export { OrgInterface } from './org/interfaces/org.interface'; export { OrgOwnerInterface } from './org/interfaces/org-owner.interface'; export { OrgCreatableInterface } from './org/interfaces/org-creatable.interface'; export { OrgUpdatableInterface } from './org/interfaces/org-updatable.interface'; +export { OrgMemberInterface } from './org/interfaces/org-member.interface'; export { UserInterface } from './user/interfaces/user.interface'; export { UserCreatableInterface } from './user/interfaces/user-creatable.interface'; @@ -43,3 +44,8 @@ export { InvitationInterface } from './invitation/interfaces/invitation.interfac export { InvitationAcceptedEventPayloadInterface } from './invitation/interfaces/invitation-accepted-event-payload.interface'; export { InvitationGetUserEventPayloadInterface } from './invitation/interfaces/invitation-get-user-event-payload.interface'; export { InvitationGetUserEventResponseInterface } from './invitation/interfaces/invitation-get-user-event-response.interface'; + +export { + INVITATION_MODULE_CATEGORY_USER_KEY, + INVITATION_MODULE_CATEGORY_ORG_KEY, +} from './invitation/invitation.contants'; diff --git a/packages/ts-common/src/invitation/interfaces/invitation-accepted-event-payload.interface.ts b/packages/ts-common/src/invitation/interfaces/invitation-accepted-event-payload.interface.ts index a40a5082e..2304dcc2b 100644 --- a/packages/ts-common/src/invitation/interfaces/invitation-accepted-event-payload.interface.ts +++ b/packages/ts-common/src/invitation/interfaces/invitation-accepted-event-payload.interface.ts @@ -2,10 +2,11 @@ import { LiteralObject, ReferenceQueryOptionsInterface, } from '@concepta/ts-core'; + import { InvitationInterface } from './invitation.interface'; -export interface InvitationAcceptedEventPayloadInterface - extends InvitationInterface { +export interface InvitationAcceptedEventPayloadInterface { + invitation: InvitationInterface; data?: LiteralObject; queryOptions?: ReferenceQueryOptionsInterface; } diff --git a/packages/ts-common/src/invitation/interfaces/invitation.interface.ts b/packages/ts-common/src/invitation/interfaces/invitation.interface.ts index 074b7a6c7..17ef26a70 100644 --- a/packages/ts-common/src/invitation/interfaces/invitation.interface.ts +++ b/packages/ts-common/src/invitation/interfaces/invitation.interface.ts @@ -1,4 +1,5 @@ import { + LiteralObject, ReferenceActiveInterface, ReferenceAuditInterface, ReferenceIdInterface, @@ -12,4 +13,5 @@ export interface InvitationInterface code: string; category: string; user: ReferenceIdInterface; + constraints?: LiteralObject; } diff --git a/packages/ts-common/src/invitation/invitation.contants.ts b/packages/ts-common/src/invitation/invitation.contants.ts new file mode 100644 index 000000000..dafd7d34a --- /dev/null +++ b/packages/ts-common/src/invitation/invitation.contants.ts @@ -0,0 +1,2 @@ +export const INVITATION_MODULE_CATEGORY_USER_KEY = 'user'; +export const INVITATION_MODULE_CATEGORY_ORG_KEY = 'org'; diff --git a/packages/ts-common/src/org/interfaces/org-member.interface.ts b/packages/ts-common/src/org/interfaces/org-member.interface.ts new file mode 100644 index 000000000..a99ef0812 --- /dev/null +++ b/packages/ts-common/src/org/interfaces/org-member.interface.ts @@ -0,0 +1,13 @@ +import { + ReferenceActiveInterface, + ReferenceAuditInterface, + ReferenceIdInterface, +} from '@concepta/ts-core'; + +export interface OrgMemberInterface + extends ReferenceIdInterface, + ReferenceActiveInterface, + ReferenceAuditInterface { + orgId: string; + userId: string; +} diff --git a/packages/ts-common/src/org/interfaces/org.interface.ts b/packages/ts-common/src/org/interfaces/org.interface.ts index 277d42d45..d0c36e63b 100644 --- a/packages/ts-common/src/org/interfaces/org.interface.ts +++ b/packages/ts-common/src/org/interfaces/org.interface.ts @@ -4,6 +4,7 @@ import { ReferenceIdInterface, } from '@concepta/ts-core'; import { OrgOwnerInterface } from './org-owner.interface'; +import { OrgMemberInterface } from './org-member.interface'; export interface OrgInterface extends ReferenceIdInterface, @@ -14,4 +15,5 @@ export interface OrgInterface * Name */ name: string; + members?: OrgMemberInterface; } diff --git a/packages/typeorm-common/src/services/base.service.ts b/packages/typeorm-common/src/services/base.service.ts index f4dae9bba..0928f2d13 100644 --- a/packages/typeorm-common/src/services/base.service.ts +++ b/packages/typeorm-common/src/services/base.service.ts @@ -26,10 +26,10 @@ export abstract class BaseService { /** * Find One wrapper. * - * @private * @param findOneOptions Find options + * @param queryOptions */ - protected async findOne( + async findOne( findOneOptions: FindOneOptions, queryOptions?: QueryOptionsInterface, ): Promise { diff --git a/packages/typeorm-common/src/services/lookup.service.ts b/packages/typeorm-common/src/services/lookup.service.ts index 9ee419115..70c9ef8a3 100644 --- a/packages/typeorm-common/src/services/lookup.service.ts +++ b/packages/typeorm-common/src/services/lookup.service.ts @@ -27,6 +27,7 @@ export abstract class LookupService * Get entity for the given id. * * @param id the id + * @param queryOptions */ async byId( id: ReferenceId,