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
2024-12-02 12:36:40 +01:00
committed by GitHub
parent 4ef353a7b9
commit ac79585232
8 changed files with 106 additions and 260 deletions

View File

@@ -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,
},
]

View File

@@ -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

View File

@@ -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;');
}
}

View File

@@ -1,2 +1,2 @@
export { default as User } from "./user"
export { default as Invite } from "./invite"
export { User } from "./user"
export { Invite } from "./invite"

View File

@@ -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<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")
}
}
.indexes([
{
on: ["email"],
unique: true,
where: "deleted_at IS NULL",
},
{
on: ["token"],
where: "deleted_at IS NULL",
},
])

View File

@@ -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<string, unknown> | 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",
},
])

View File

@@ -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<User>
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<Invite>
protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof User>
>
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Invite>
>
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<Invite[]> {
): Promise<InferEntityType<typeof Invite>[]> {
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