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
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-12-02 12:36:40 +01:00
1 parent 4ef353a7b9
commit ac79585232
8 files changed
+106 -260

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/user": minor
---
feat(user): Migrate user module to DML
@@ -8,22 +8,16 @@ import jwt, { JwtPayload } from "jsonwebtoken"
jest.setTimeout(30000) jest.setTimeout(30000)
const expireDate = new Date().setMilliseconds(
new Date().getMilliseconds() + 60 * 60 * 24
)
const defaultInviteData = [ const defaultInviteData = [
{ {
id: "1", id: "1",
email: "user_1@test.com", email: "user_1@test.com",
token: "test", token: "test",
expires_at: expireDate,
}, },
{ {
id: "2", id: "2",
email: "user_2@test.com", email: "user_2@test.com",
token: "test", token: "test",
expires_at: expireDate,
}, },
] ]
@@ -1,5 +1,7 @@
{ {
"namespaces": ["public"], "namespaces": [
"public"
],
"name": "public", "name": "public",
"tables": [ "tables": [
{ {
@@ -97,32 +99,34 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"keyName": "IDX_invite_email", "keyName": "IDX_invite_deleted_at",
"columnNames": ["email"], "columnNames": [],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": 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", "keyName": "IDX_invite_token",
"columnNames": ["token"], "columnNames": [],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": false, "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_invite_token\" ON \"invite\" (token) WHERE deleted_at IS NULL" "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", "keyName": "invite_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -224,24 +228,26 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"keyName": "IDX_user_email", "keyName": "IDX_user_deleted_at",
"columnNames": ["email"], "columnNames": [],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": 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", "keyName": "IDX_user_email_unique",
"columnNames": ["deleted_at"], "columnNames": [],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": 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", "keyName": "user_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -0,0 +1,21 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20241202103352 extends Migration {
async up(): Promise<void> {
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<void> {
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;');
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as User } from "./user" export { User } from "./user"
export { default as Invite } from "./invite" export { Invite } from "./invite"
+20 -110
View File
@@ -1,112 +1,22 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate,
Entity,
Filter,
Index,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { DAL } from "@medusajs/framework/types" export const Invite = model
import { .define("invite", {
DALUtils, id: model.id({ prefix: "invite" }).primaryKey(),
createPsqlIndexStatementHelper, email: model.text().searchable(),
generateEntityId, accepted: model.boolean().default(false),
Searchable, token: model.text(),
} from "@medusajs/framework/utils" expires_at: model.dateTime(),
metadata: model.json().nullable(),
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,
}) })
@Searchable() .indexes([
@Property({ columnType: "text" }) {
email: string on: ["email"],
unique: true,
@Property({ columnType: "boolean" }) where: "deleted_at IS NULL",
accepted: boolean = false },
{
@Index({ on: ["token"],
name: inviteTokenIndexName, where: "deleted_at IS NULL",
expression: inviteTokenIndexStatement, },
}) ])
@Property({ columnType: "text" })
token: string
@Property({ columnType: "timestamptz" })
expires_at: Date
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | 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")
}
}
+16 -100
View File
@@ -1,102 +1,18 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate,
Entity,
Filter,
Index,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { DAL } from "@medusajs/framework/types" export const User = model
import { .define("user", {
createPsqlIndexStatementHelper, id: model.id({ prefix: "user" }).primaryKey(),
DALUtils, first_name: model.text().searchable().nullable(),
generateEntityId, last_name: model.text().searchable().nullable(),
Searchable, email: model.text().searchable(),
} from "@medusajs/framework/utils" avatar_url: model.text().nullable(),
metadata: model.json().nullable(),
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<string, unknown> | null = null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
}) })
created_at: Date .indexes([
{
@Property({ unique: true,
onCreate: () => new Date(), on: ["email"],
onUpdate: () => new Date(), where: "deleted_at IS NULL",
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")
}
}
@@ -1,6 +1,7 @@
import { import {
Context, Context,
DAL, DAL,
InferEntityType,
InternalModuleDeclaration, InternalModuleDeclaration,
ModulesSdkTypes, ModulesSdkTypes,
UserTypes, UserTypes,
@@ -9,6 +10,7 @@ import {
arrayDifference, arrayDifference,
CommonEvents, CommonEvents,
EmitEvents, EmitEvents,
generateEntityId,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
@@ -41,8 +43,12 @@ export default class UserModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<User> protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<Invite> InferEntityType<typeof User>
>
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Invite>
>
protected readonly config: { jwtSecret: string; expiresIn: number } protected readonly config: { jwtSecret: string; expiresIn: number }
constructor( constructor(
@@ -151,9 +157,7 @@ export default class UserModuleService
const updates = invites.map((invite) => { const updates = invites.map((invite) => {
return { return {
id: invite.id, id: invite.id,
expires_at: new Date().setMilliseconds( expires_at: new Date(Date.now() + this.config.expiresIn * 1000),
new Date().getMilliseconds() + this.config.expiresIn * 1000
),
token: this.generateToken({ id: invite.id, email: invite.email }), token: this.generateToken({ id: invite.id, email: invite.email }),
} }
}) })
@@ -296,7 +300,7 @@ export default class UserModuleService
private async createInvites_( private async createInvites_(
data: UserTypes.CreateInviteDTO[], data: UserTypes.CreateInviteDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<Invite[]> { ): Promise<InferEntityType<typeof Invite>[]> {
const alreadyExistingUsers = await this.listUsers({ const alreadyExistingUsers = await this.listUsers({
email: data.map((d) => d.email), email: data.map((d) => d.email),
}) })
@@ -311,26 +315,16 @@ export default class UserModuleService
} }
const toCreate = data.map((invite) => { const toCreate = data.map((invite) => {
const id = generateEntityId((invite as { id?: string }).id, "invite")
return { return {
...invite, ...invite,
expires_at: new Date(), id,
token: "placeholder", 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) return 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)
} }
// @ts-ignore // @ts-ignore