From 70d77ea22fb73021bc1f4da0780a2235f1e86fb7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 5 Dec 2024 22:07:54 +0530 Subject: [PATCH] refactor: migrate api key module to DML (#10450) Fixes: FRMW-2827 --- .changeset/cuddly-students-travel.md | 5 + .../migrations/.snapshot-medusa-api-key.json | 64 +++++++--- .../src/migrations/Migration20241205122700.ts | 32 +++++ .../modules/api-key/src/models/api-key.ts | 114 ++++-------------- .../src/services/api-key-module-service.ts | 17 +-- 5 files changed, 115 insertions(+), 117 deletions(-) create mode 100644 .changeset/cuddly-students-travel.md create mode 100644 packages/modules/api-key/src/migrations/Migration20241205122700.ts diff --git a/.changeset/cuddly-students-travel.md b/.changeset/cuddly-students-travel.md new file mode 100644 index 0000000000..37353113d6 --- /dev/null +++ b/.changeset/cuddly-students-travel.md @@ -0,0 +1,5 @@ +--- +"@medusajs/api-key": patch +--- + +refactor: migrate api key module to DML diff --git a/packages/modules/api-key/src/migrations/.snapshot-medusa-api-key.json b/packages/modules/api-key/src/migrations/.snapshot-medusa-api-key.json index 2ca3413268..c8086a75f8 100644 --- a/packages/modules/api-key/src/migrations/.snapshot-medusa-api-key.json +++ b/packages/modules/api-key/src/migrations/.snapshot-medusa-api-key.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -56,7 +58,11 @@ "autoincrement": false, "primary": false, "nullable": false, - "mappedType": "text" + "enumItems": [ + "publishable", + "secret" + ], + "mappedType": "enum" }, "last_used_at": { "name": "last_used_at", @@ -77,6 +83,25 @@ "nullable": false, "mappedType": "text" }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -99,17 +124,8 @@ "default": "now()", "mappedType": "datetime" }, - "revoked_by": { - "name": "revoked_by", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, - "revoked_at": { - "name": "revoked_at", + "deleted_at": { + "name": "deleted_at", "type": "timestamptz", "unsigned": false, "autoincrement": false, @@ -123,24 +139,34 @@ "schema": "public", "indexes": [ { - "keyName": "IDX_api_key_token_unique", - "columnNames": ["token"], + "keyName": "IDX_api_key_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_deleted_at\" ON \"api_key\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_api_key_token_unique", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_api_key_token_unique\" ON \"api_key\" (token) WHERE deleted_at IS NULL" }, { "keyName": "IDX_api_key_type", - "columnNames": ["type"], + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_api_key_type\" ON \"api_key\" (type) WHERE deleted_at IS NULL" }, { "keyName": "api_key_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true diff --git a/packages/modules/api-key/src/migrations/Migration20241205122700.ts b/packages/modules/api-key/src/migrations/Migration20241205122700.ts new file mode 100644 index 0000000000..bc82b5d3da --- /dev/null +++ b/packages/modules/api-key/src/migrations/Migration20241205122700.ts @@ -0,0 +1,32 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241205122700 extends Migration { + async up(): Promise { + this.addSql( + 'alter table if exists "api_key" add column if not exists "deleted_at" timestamptz null;' + ) + this.addSql( + 'alter table if exists "api_key" alter column "type" type text using ("type"::text);' + ) + this.addSql( + 'alter table if exists "api_key" add constraint "api_key_type_check" check ("type" in (\'publishable\', \'secret\'));' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_api_key_deleted_at" ON "api_key" (deleted_at) WHERE deleted_at IS NULL;' + ) + } + + async down(): Promise { + this.addSql( + 'alter table if exists "api_key" drop constraint if exists "api_key_type_check";' + ) + + this.addSql( + 'alter table if exists "api_key" alter column "type" type text using ("type"::text);' + ) + this.addSql('drop index if exists "IDX_api_key_deleted_at";') + this.addSql( + 'alter table if exists "api_key" drop column if exists "deleted_at";' + ) + } +} diff --git a/packages/modules/api-key/src/models/api-key.ts b/packages/modules/api-key/src/models/api-key.ts index c5dd599469..621b738304 100644 --- a/packages/modules/api-key/src/models/api-key.ts +++ b/packages/modules/api-key/src/models/api-key.ts @@ -1,94 +1,26 @@ -import { - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" +import { model } from "@medusajs/framework/utils" -import { - BeforeCreate, - Entity, - Enum, - OnInit, - PrimaryKey, - Property, -} from "@mikro-orm/core" - -const TypeIndex = createPsqlIndexStatementHelper({ - tableName: "api_key", - columns: "type", -}) - -const TokenIndex = createPsqlIndexStatementHelper({ - tableName: "api_key", - columns: "token", - unique: true, -}) - -@Entity() -export default class ApiKey { - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ columnType: "text" }) - @TokenIndex.MikroORMIndex() - token: string - - @Property({ columnType: "text" }) - salt: string - - @Searchable() - @Property({ columnType: "text" }) - redacted: string - - @Searchable() - @Property({ columnType: "text" }) - title: string - - @Property({ columnType: "text" }) - @Enum({ items: ["publishable", "secret"] }) - @TypeIndex.MikroORMIndex() - type: "publishable" | "secret" - - @Property({ - columnType: "timestamptz", - nullable: true, +const ApiKey = model + .define("ApiKey", { + id: model.id({ prefix: "apk" }).primaryKey(), + token: model.text(), + salt: model.text(), + redacted: model.text().searchable(), + title: model.text().searchable(), + type: model.enum(["publishable", "secret"]), + last_used_at: model.dateTime().nullable(), + created_by: model.text(), + revoked_by: model.text().nullable(), + revoked_at: model.dateTime().nullable(), }) - last_used_at: Date | null = null + .indexes([ + { + on: ["token"], + unique: true, + }, + { + on: ["type"], + }, + ]) - @Property({ columnType: "text" }) - created_by: string - - @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 - - @Property({ columnType: "text", nullable: true }) - revoked_by: string | null = null - - @Property({ - columnType: "timestamptz", - nullable: true, - }) - revoked_at: Date | null = null - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "apk") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "apk") - } -} +export default ApiKey diff --git a/packages/modules/api-key/src/services/api-key-module-service.ts b/packages/modules/api-key/src/services/api-key-module-service.ts index ccb6536d38..baba804e31 100644 --- a/packages/modules/api-key/src/services/api-key-module-service.ts +++ b/packages/modules/api-key/src/services/api-key-module-service.ts @@ -5,6 +5,7 @@ import { FilterableApiKeyProps, FindConfig, IApiKeyModuleService, + InferEntityType, InternalModuleDeclaration, ModuleJoinerConfig, ModulesSdkTypes, @@ -46,7 +47,9 @@ export class ApiKeyModuleService implements IApiKeyModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService + protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > constructor( { baseRepository, apiKeyService }: InjectedDependencies, @@ -138,7 +141,7 @@ export class ApiKeyModuleService protected async createApiKeys_( data: ApiKeyTypes.CreateApiKeyDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise<[ApiKey[], TokenDTO[]]> { + ): Promise<[InferEntityType[], TokenDTO[]]> { await this.validateCreateApiKeys_(data, sharedContext) const normalizedInput: CreateApiKeyDTO[] = [] @@ -276,7 +279,7 @@ export class ApiKeyModuleService protected async updateApiKeys_( normalizedInput: UpdateApiKeyInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const updateRequest = normalizedInput.map((k) => ({ id: k.id, title: k.title, @@ -387,7 +390,7 @@ export class ApiKeyModuleService async revoke_( normalizedInput: RevokeApiKeyInput[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { await this.validateRevokeApiKeys_(normalizedInput) const updateRequest = normalizedInput.map((k) => { @@ -433,7 +436,7 @@ export class ApiKeyModuleService protected async authenticate_( token: string, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise | false> { // Since we only allow up to 2 active tokens, getitng the list and checking each token isn't an issue. // We can always filter on the redacted key if we add support for an arbitrary number of tokens. const secretKeys = await this.apiKeyService_.list( @@ -617,8 +620,8 @@ export class ApiKeyModuleService // We are mutating the object here as what microORM relies on non-enumerable fields for serialization, among other things. const omitToken = ( // We have to make salt optional before deleting it (and we do want it required in the DB) - key: Omit & { salt?: string } -): Omit => { + key: Omit, "salt"> & { salt?: string } +): Omit, "salt"> => { key.token = key.type === ApiKeyType.SECRET ? "" : key.token delete key.salt return key