refactor: migrate api key module to DML (#10450)

Fixes: FRMW-2827
This commit is contained in:
Harminder Virk
2024-12-05 22:07:54 +05:30
committed by GitHub
parent 559fc6587a
commit 70d77ea22f
5 changed files with 115 additions and 117 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/api-key": patch
---
refactor: migrate api key module to DML

View File

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

View File

@@ -0,0 +1,32 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20241205122700 extends Migration {
async up(): Promise<void> {
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<void> {
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";'
)
}
}

View File

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

View File

@@ -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<ApiKey>
protected readonly apiKeyService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ApiKey>
>
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<typeof ApiKey>[], 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<ApiKey[]> {
): Promise<InferEntityType<typeof ApiKey>[]> {
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<ApiKey[]> {
): Promise<InferEntityType<typeof ApiKey>[]> {
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<ApiKey | false> {
): Promise<InferEntityType<typeof ApiKey> | 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<ApiKey, "salt"> & { salt?: string }
): Omit<ApiKey, "salt"> => {
key: Omit<InferEntityType<typeof ApiKey>, "salt"> & { salt?: string }
): Omit<InferEntityType<typeof ApiKey>, "salt"> => {
key.token = key.type === ApiKeyType.SECRET ? "" : key.token
delete key.salt
return key