From ac7958523218fe01528ad25ef6a203f620ecb6dd Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 2 Dec 2024 12:36:40 +0100 Subject: [PATCH] feat(user): Migrate user module to DML (#10389) * feat(user): Migrate user module to DML * Create rotten-tigers-worry.md * update indexes names following conventions * remove duplicate modifier --- .changeset/rotten-tigers-worry.md | 5 + .../__tests__/invite.spec.ts | 6 - .../src/migrations/.snapshot-medusa-user.json | 48 ++++--- .../src/migrations/Migration20241202103352.ts | 21 +++ packages/modules/user/src/models/index.ts | 4 +- packages/modules/user/src/models/invite.ts | 130 +++--------------- packages/modules/user/src/models/user.ts | 116 +++------------- .../modules/user/src/services/user-module.ts | 36 ++--- 8 files changed, 106 insertions(+), 260 deletions(-) create mode 100644 .changeset/rotten-tigers-worry.md create mode 100644 packages/modules/user/src/migrations/Migration20241202103352.ts diff --git a/.changeset/rotten-tigers-worry.md b/.changeset/rotten-tigers-worry.md new file mode 100644 index 0000000000..345072b7a2 --- /dev/null +++ b/.changeset/rotten-tigers-worry.md @@ -0,0 +1,5 @@ +--- +"@medusajs/user": minor +--- + +feat(user): Migrate user module to DML diff --git a/packages/modules/user/integration-tests/__tests__/invite.spec.ts b/packages/modules/user/integration-tests/__tests__/invite.spec.ts index bddd63e45a..29d5d9e8a6 100644 --- a/packages/modules/user/integration-tests/__tests__/invite.spec.ts +++ b/packages/modules/user/integration-tests/__tests__/invite.spec.ts @@ -8,22 +8,16 @@ import jwt, { JwtPayload } from "jsonwebtoken" jest.setTimeout(30000) -const expireDate = new Date().setMilliseconds( - new Date().getMilliseconds() + 60 * 60 * 24 -) - const defaultInviteData = [ { id: "1", email: "user_1@test.com", token: "test", - expires_at: expireDate, }, { id: "2", email: "user_2@test.com", token: "test", - expires_at: expireDate, }, ] diff --git a/packages/modules/user/src/migrations/.snapshot-medusa-user.json b/packages/modules/user/src/migrations/.snapshot-medusa-user.json index b6643bf04d..6a2047179a 100644 --- a/packages/modules/user/src/migrations/.snapshot-medusa-user.json +++ b/packages/modules/user/src/migrations/.snapshot-medusa-user.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -97,32 +99,34 @@ "schema": "public", "indexes": [ { - "keyName": "IDX_invite_email", - "columnNames": ["email"], + "keyName": "IDX_invite_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_invite_email\" ON \"invite\" (email) WHERE deleted_at IS NULL" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_invite_deleted_at\" ON \"invite\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_invite_email_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_invite_email_unique\" ON \"invite\" (email) WHERE deleted_at IS NULL" }, { "keyName": "IDX_invite_token", - "columnNames": ["token"], + "columnNames": [], "composite": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_invite_token\" ON \"invite\" (token) WHERE deleted_at IS NULL" }, - { - "keyName": "IDX_invite_deleted_at", - "columnNames": ["deleted_at"], - "composite": false, - "primary": false, - "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_invite_deleted_at\" ON \"invite\" (deleted_at) WHERE deleted_at IS NOT NULL" - }, { "keyName": "invite_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -224,24 +228,26 @@ "schema": "public", "indexes": [ { - "keyName": "IDX_user_email", - "columnNames": ["email"], + "keyName": "IDX_user_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_user_email\" ON \"user\" (email) WHERE deleted_at IS NULL" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_user_deleted_at\" ON \"user\" (deleted_at) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_user_deleted_at", - "columnNames": ["deleted_at"], + "keyName": "IDX_user_email_unique", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_user_deleted_at\" ON \"user\" (deleted_at) WHERE deleted_at IS NOT NULL" + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_user_email_unique\" ON \"user\" (email) WHERE deleted_at IS NULL" }, { "keyName": "user_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true diff --git a/packages/modules/user/src/migrations/Migration20241202103352.ts b/packages/modules/user/src/migrations/Migration20241202103352.ts new file mode 100644 index 0000000000..4b1c30e8b5 --- /dev/null +++ b/packages/modules/user/src/migrations/Migration20241202103352.ts @@ -0,0 +1,21 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20241202103352 extends Migration { + + async up(): Promise { + this.addSql('drop index if exists "IDX_invite_email";'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_invite_email_unique" ON "invite" (email) WHERE deleted_at IS NULL;'); + + this.addSql('drop index if exists "IDX_user_email";'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL;'); + } + + async down(): Promise { + this.addSql('drop index if exists "IDX_invite_email_unique";'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_invite_email" ON "invite" (email) WHERE deleted_at IS NULL;'); + + this.addSql('drop index if exists "IDX_user_email_unique";'); + this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email" ON "user" (email) WHERE deleted_at IS NULL;'); + } + +} diff --git a/packages/modules/user/src/models/index.ts b/packages/modules/user/src/models/index.ts index 2ef84f37ca..c5b647ead5 100644 --- a/packages/modules/user/src/models/index.ts +++ b/packages/modules/user/src/models/index.ts @@ -1,2 +1,2 @@ -export { default as User } from "./user" -export { default as Invite } from "./invite" +export { User } from "./user" +export { Invite } from "./invite" diff --git a/packages/modules/user/src/models/invite.ts b/packages/modules/user/src/models/invite.ts index fd180b2a87..f2e9cce0c8 100644 --- a/packages/modules/user/src/models/invite.ts +++ b/packages/modules/user/src/models/invite.ts @@ -1,112 +1,22 @@ -import { - BeforeCreate, - Entity, - Filter, - Index, - OnInit, - OptionalProps, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" -import { DAL } from "@medusajs/framework/types" -import { - DALUtils, - createPsqlIndexStatementHelper, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" - -const inviteEmailIndexName = "IDX_invite_email" -const inviteEmailIndexStatement = createPsqlIndexStatementHelper({ - name: inviteEmailIndexName, - tableName: "invite", - columns: "email", - where: "deleted_at IS NULL", - unique: true, -}).expression - -const inviteTokenIndexName = "IDX_invite_token" -const inviteTokenIndexStatement = createPsqlIndexStatementHelper({ - name: inviteTokenIndexName, - tableName: "invite", - columns: "token", - where: "deleted_at IS NULL", -}).expression - -const inviteDeletedAtIndexName = "IDX_invite_deleted_at" -const inviteDeletedAtIndexStatement = createPsqlIndexStatementHelper({ - name: inviteDeletedAtIndexName, - tableName: "invite", - columns: "deleted_at", - where: "deleted_at IS NOT NULL", -}).expression - -type OptionalFields = - | "metadata" - | "accepted" - | DAL.SoftDeletableModelDateColumns -@Entity({ tableName: "invite" }) -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export default class Invite { - [OptionalProps]: OptionalFields - - @PrimaryKey({ columnType: "text" }) - id: string - - @Index({ - name: inviteEmailIndexName, - expression: inviteEmailIndexStatement, +export const Invite = model + .define("invite", { + id: model.id({ prefix: "invite" }).primaryKey(), + email: model.text().searchable(), + accepted: model.boolean().default(false), + token: model.text(), + expires_at: model.dateTime(), + metadata: model.json().nullable(), }) - @Searchable() - @Property({ columnType: "text" }) - email: string - - @Property({ columnType: "boolean" }) - accepted: boolean = false - - @Index({ - name: inviteTokenIndexName, - expression: inviteTokenIndexStatement, - }) - @Property({ columnType: "text" }) - token: string - - @Property({ columnType: "timestamptz" }) - expires_at: Date - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @Index({ - name: inviteDeletedAtIndexName, - expression: inviteDeletedAtIndexStatement, - }) - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "invite") - } - - @BeforeCreate() - beforeCreate() { - this.id = generateEntityId(this.id, "invite") - } -} + .indexes([ + { + on: ["email"], + unique: true, + where: "deleted_at IS NULL", + }, + { + on: ["token"], + where: "deleted_at IS NULL", + }, + ]) diff --git a/packages/modules/user/src/models/user.ts b/packages/modules/user/src/models/user.ts index 0c8b9acff9..00a25abcc9 100644 --- a/packages/modules/user/src/models/user.ts +++ b/packages/modules/user/src/models/user.ts @@ -1,102 +1,18 @@ -import { - BeforeCreate, - Entity, - Filter, - Index, - OnInit, - OptionalProps, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" -import { DAL } from "@medusajs/framework/types" -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" - -const userEmailIndexName = "IDX_user_email" -const userEmailIndexStatement = createPsqlIndexStatementHelper({ - name: userEmailIndexName, - unique: true, - tableName: "user", - columns: "email", - where: "deleted_at IS NULL", -}) - -const userDeletedAtIndexName = "IDX_user_deleted_at" -const userDeletedAtIndexStatement = createPsqlIndexStatementHelper({ - name: userDeletedAtIndexName, - tableName: "user", - columns: "deleted_at", - where: "deleted_at IS NOT NULL", -}).expression - -type OptionalFields = - | "first_name" - | "last_name" - | "metadata" - | "avatar_url" - | DAL.SoftDeletableModelDateColumns - -@Entity() -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export default class User { - [OptionalProps]?: OptionalFields - - @PrimaryKey({ columnType: "text" }) - id!: string - - @Searchable() - @Property({ columnType: "text", nullable: true }) - first_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - last_name: string | null = null - - @userEmailIndexStatement.MikroORMIndex() - @Searchable() - @Property({ columnType: "text" }) - email: string - - @Property({ columnType: "text", nullable: true }) - avatar_url: string | null = null - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", +export const User = model + .define("user", { + id: model.id({ prefix: "user" }).primaryKey(), + first_name: model.text().searchable().nullable(), + last_name: model.text().searchable().nullable(), + email: model.text().searchable(), + avatar_url: model.text().nullable(), + metadata: model.json().nullable(), }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @Index({ - name: userDeletedAtIndexName, - expression: userDeletedAtIndexStatement, - }) - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at?: Date | null = null - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "user") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "user") - } -} + .indexes([ + { + unique: true, + on: ["email"], + where: "deleted_at IS NULL", + }, + ]) diff --git a/packages/modules/user/src/services/user-module.ts b/packages/modules/user/src/services/user-module.ts index be41c2b0f4..2174748d41 100644 --- a/packages/modules/user/src/services/user-module.ts +++ b/packages/modules/user/src/services/user-module.ts @@ -1,6 +1,7 @@ import { Context, DAL, + InferEntityType, InternalModuleDeclaration, ModulesSdkTypes, UserTypes, @@ -9,6 +10,7 @@ import { arrayDifference, CommonEvents, EmitEvents, + generateEntityId, InjectManager, InjectTransactionManager, MedusaContext, @@ -41,8 +43,12 @@ export default class UserModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly userService_: ModulesSdkTypes.IMedusaInternalService - protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService + protected readonly userService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > protected readonly config: { jwtSecret: string; expiresIn: number } constructor( @@ -151,9 +157,7 @@ export default class UserModuleService const updates = invites.map((invite) => { return { id: invite.id, - expires_at: new Date().setMilliseconds( - new Date().getMilliseconds() + this.config.expiresIn * 1000 - ), + expires_at: new Date(Date.now() + this.config.expiresIn * 1000), token: this.generateToken({ id: invite.id, email: invite.email }), } }) @@ -296,7 +300,7 @@ export default class UserModuleService private async createInvites_( data: UserTypes.CreateInviteDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const alreadyExistingUsers = await this.listUsers({ email: data.map((d) => d.email), }) @@ -311,26 +315,16 @@ export default class UserModuleService } const toCreate = data.map((invite) => { + const id = generateEntityId((invite as { id?: string }).id, "invite") return { ...invite, - expires_at: new Date(), - token: "placeholder", + id, + expires_at: new Date(Date.now() + this.config.expiresIn * 1000), + token: this.generateToken({ id, email: invite.email }), } }) - const created = await this.inviteService_.create(toCreate, sharedContext) - - const updates = created.map((invite) => { - return { - id: invite.id, - expires_at: new Date().setMilliseconds( - new Date().getMilliseconds() + this.config.expiresIn * 1000 - ), - token: this.generateToken({ id: invite.id, email: invite.email }), - } - }) - - return await this.inviteService_.update(updates, sharedContext) + return await this.inviteService_.create(toCreate, sharedContext) } // @ts-ignore