diff --git a/packages/api-key/src/services/api-key-module-service.ts b/packages/api-key/src/services/api-key-module-service.ts index dbf91df884434..6b5176d4b10cc 100644 --- a/packages/api-key/src/services/api-key-module-service.ts +++ b/packages/api-key/src/services/api-key-module-service.ts @@ -13,7 +13,12 @@ import { } from "@medusajs/types" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { ApiKey } from "@models" -import { CreateApiKeyDTO, TokenDTO } from "@types" +import { + CreateApiKeyDTO, + RevokeApiKeyInput, + TokenDTO, + UpdateApiKeyInput, +} from "@types" import { ApiKeyType, InjectManager, @@ -133,61 +138,109 @@ export default class ApiKeyModuleService return [createdApiKeys, generatedTokens] } - async update( - selector: FilterableApiKeyProps, - data: Omit, + async upsert( + data: ApiKeyTypes.UpsertApiKeyDTO[], sharedContext?: Context ): Promise + async upsert( + data: ApiKeyTypes.UpsertApiKeyDTO, + sharedContext?: Context + ): Promise + @InjectTransactionManager("baseRepository_") + async upsert( + data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (apiKey): apiKey is UpdateApiKeyInput => !!apiKey.id + ) + const forCreate = input.filter( + (apiKey): apiKey is ApiKeyTypes.CreateApiKeyDTO => !apiKey.id + ) + + const operations: Promise[] = [] + + if (forCreate.length) { + const op = async () => { + const [createdApiKeys, generatedTokens] = await this.create_( + forCreate, + sharedContext + ) + const serializedResponse = await this.baseRepository_.serialize< + ApiKeyTypes.ApiKeyDTO[] + >(createdApiKeys, { + populate: true, + }) + + return serializedResponse.map( + (key) => + ({ + ...key, + token: + generatedTokens.find((t) => t.hashedToken === key.token) + ?.rawToken ?? key.token, + salt: undefined, + } as ApiKeyTypes.ApiKeyDTO) + ) + } + + operations.push(op()) + } + + if (forUpdate.length) { + const op = async () => { + const updateResp = await this.update_(forUpdate, sharedContext) + return await this.baseRepository_.serialize( + updateResp + ) + } + + operations.push(op()) + } + + const result = (await promiseAll(operations)).flat() + return Array.isArray(data) ? result : result[0] + } + async update( id: string, - data: Omit, + data: ApiKeyTypes.UpdateApiKeyDTO, sharedContext?: Context ): Promise async update( - data: ApiKeyTypes.UpdateApiKeyDTO[] + selector: FilterableApiKeyProps, + data: ApiKeyTypes.UpdateApiKeyDTO, + sharedContext?: Context ): Promise @InjectManager("baseRepository_") async update( - idOrSelectorOrData: - | string - | FilterableApiKeyProps - | ApiKeyTypes.UpdateApiKeyDTO[], - data?: Omit, + idOrSelector: string | FilterableApiKeyProps, + data: ApiKeyTypes.UpdateApiKeyDTO, @MedusaContext() sharedContext: Context = {} ): Promise { - const updatedApiKeys = await this.update_( - idOrSelectorOrData, + let normalizedInput = await this.normalizeUpdateInput_( + idOrSelector, data, sharedContext ) + const updatedApiKeys = await this.update_(normalizedInput, sharedContext) + const serializedResponse = await this.baseRepository_.serialize< ApiKeyTypes.ApiKeyDTO[] >(updatedApiKeys.map(omitToken), { populate: true, }) - return isString(idOrSelectorOrData) - ? serializedResponse[0] - : serializedResponse + return isString(idOrSelector) ? serializedResponse[0] : serializedResponse } @InjectTransactionManager("baseRepository_") protected async update_( - idOrSelectorOrData: - | string - | FilterableApiKeyProps - | ApiKeyTypes.UpdateApiKeyDTO[], - data?: Omit, + normalizedInput: UpdateApiKeyInput[], @MedusaContext() sharedContext: Context = {} ): Promise { - const normalizedInput = - await this.normalizeUpdateInput_( - idOrSelectorOrData, - data, - sharedContext - ) - const updateRequest = normalizedInput.map((k) => ({ id: k.id, title: k.title, @@ -259,33 +312,28 @@ export default class ApiKeyModuleService ] } - async revoke( - selector: FilterableApiKeyProps, - data: Omit, - sharedContext?: Context - ): Promise async revoke( id: string, - data: Omit, + data: ApiKeyTypes.RevokeApiKeyDTO, sharedContext?: Context ): Promise async revoke( - data: ApiKeyTypes.RevokeApiKeyDTO[] + selector: FilterableApiKeyProps, + data: ApiKeyTypes.RevokeApiKeyDTO, + sharedContext?: Context ): Promise @InjectManager("baseRepository_") async revoke( - idOrSelectorOrData: - | string - | FilterableApiKeyProps - | ApiKeyTypes.RevokeApiKeyDTO[], - data?: Omit, + idOrSelector: string | FilterableApiKeyProps, + data: ApiKeyTypes.RevokeApiKeyDTO, @MedusaContext() sharedContext: Context = {} ): Promise { - const revokedApiKeys = await this.revoke_( - idOrSelectorOrData, + const normalizedInput = await this.normalizeUpdateInput_( + idOrSelector, data, sharedContext ) + const revokedApiKeys = await this.revoke_(normalizedInput, sharedContext) const serializedResponse = await this.baseRepository_.serialize< ApiKeyTypes.ApiKeyDTO[] @@ -293,27 +341,14 @@ export default class ApiKeyModuleService populate: true, }) - return isString(idOrSelectorOrData) - ? serializedResponse[0] - : serializedResponse + return isString(idOrSelector) ? serializedResponse[0] : serializedResponse } @InjectTransactionManager("baseRepository_") async revoke_( - idOrSelectorOrData: - | string - | FilterableApiKeyProps - | ApiKeyTypes.RevokeApiKeyDTO[], - data?: Omit, + normalizedInput: RevokeApiKeyInput[], @MedusaContext() sharedContext: Context = {} ): Promise { - const normalizedInput = - await this.normalizeUpdateInput_( - idOrSelectorOrData, - data, - sharedContext - ) - await this.validateRevokeApiKeys_(normalizedInput) const updateRequest = normalizedInput.map((k) => { @@ -424,22 +459,18 @@ export default class ApiKeyModuleService } protected async normalizeUpdateInput_( - idOrSelectorOrData: string | FilterableApiKeyProps | T[], - data?: Omit, + idOrSelector: string | FilterableApiKeyProps, + data: Omit, sharedContext: Context = {} ): Promise { let normalizedInput: T[] = [] - if (isString(idOrSelectorOrData)) { - normalizedInput = [{ id: idOrSelectorOrData, ...data } as T] - } - - if (Array.isArray(idOrSelectorOrData)) { - normalizedInput = idOrSelectorOrData + if (isString(idOrSelector)) { + normalizedInput = [{ id: idOrSelector, ...data } as T] } - if (isObject(idOrSelectorOrData)) { + if (isObject(idOrSelector)) { const apiKeys = await this.apiKeyService_.list( - idOrSelectorOrData, + idOrSelector, {}, sharedContext ) @@ -457,7 +488,7 @@ export default class ApiKeyModuleService } protected async validateRevokeApiKeys_( - data: ApiKeyTypes.RevokeApiKeyDTO[], + data: RevokeApiKeyInput[], sharedContext: Context = {} ): Promise { if (!data.length) { diff --git a/packages/api-key/src/types/index.ts b/packages/api-key/src/types/index.ts index 2655904953297..f3b2746996699 100644 --- a/packages/api-key/src/types/index.ts +++ b/packages/api-key/src/types/index.ts @@ -1,4 +1,4 @@ -import { ApiKeyType } from "@medusajs/types" +import { ApiKeyType, RevokeApiKeyDTO, UpdateApiKeyDTO } from "@medusajs/types" import { IEventBusModuleService, Logger } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { @@ -21,3 +21,6 @@ export type TokenDTO = { salt: string redacted: string } + +export type UpdateApiKeyInput = UpdateApiKeyDTO & { id: string } +export type RevokeApiKeyInput = RevokeApiKeyDTO & { id: string } diff --git a/packages/core-flows/src/api-key/steps/revoke-api-keys.ts b/packages/core-flows/src/api-key/steps/revoke-api-keys.ts index a234b7e5220d6..6917eed6c5dea 100644 --- a/packages/core-flows/src/api-key/steps/revoke-api-keys.ts +++ b/packages/core-flows/src/api-key/steps/revoke-api-keys.ts @@ -4,12 +4,11 @@ import { IApiKeyModuleService, RevokeApiKeyDTO, } from "@medusajs/types" -import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" import { StepResponse, createStep } from "@medusajs/workflows-sdk" type RevokeApiKeysStepInput = { selector: FilterableApiKeyProps - revoke: Omit + revoke: RevokeApiKeyDTO } export const revokeApiKeysStepId = "revoke-api-keys" diff --git a/packages/core-flows/src/api-key/steps/update-api-keys.ts b/packages/core-flows/src/api-key/steps/update-api-keys.ts index 7394a27f32e0e..6b5f7227bad31 100644 --- a/packages/core-flows/src/api-key/steps/update-api-keys.ts +++ b/packages/core-flows/src/api-key/steps/update-api-keys.ts @@ -9,7 +9,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk" type UpdateApiKeysStepInput = { selector: FilterableApiKeyProps - update: Omit + update: UpdateApiKeyDTO } export const updateApiKeysStepId = "update-api-keys" @@ -41,7 +41,7 @@ export const updateApiKeysStep = createStep( ModuleRegistrationName.API_KEY ) - await service.update( + await service.upsert( prevData.map((r) => ({ id: r.id, title: r.title, diff --git a/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts b/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts index 6c1d4be0daeec..79baf41992627 100644 --- a/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts +++ b/packages/core-flows/src/api-key/workflows/revoke-api-keys.ts @@ -8,7 +8,7 @@ import { revokeApiKeysStep } from "../steps" type RevokeApiKeysStepInput = { selector: FilterableApiKeyProps - revoke: Omit + revoke: RevokeApiKeyDTO } type WorkflowInput = RevokeApiKeysStepInput diff --git a/packages/core-flows/src/api-key/workflows/update-api-keys.ts b/packages/core-flows/src/api-key/workflows/update-api-keys.ts index 452658a7e63e3..9f42051c4e9a1 100644 --- a/packages/core-flows/src/api-key/workflows/update-api-keys.ts +++ b/packages/core-flows/src/api-key/workflows/update-api-keys.ts @@ -8,7 +8,7 @@ import { updateApiKeysStep } from "../steps" type UpdateApiKeysStepInput = { selector: FilterableApiKeyProps - update: Omit + update: UpdateApiKeyDTO } type WorkflowInput = UpdateApiKeysStepInput diff --git a/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts b/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts index d2b6618fd4426..d572e8d826c4a 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/revoke/route.ts @@ -16,7 +16,7 @@ export const POST = async ( input: { selector: { id: req.params.id }, revoke: { - ...(req.validatedBody as Omit), + ...(req.validatedBody as Omit), revoked_by: req.auth.actor_id, } as RevokeApiKeyDTO, }, diff --git a/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts b/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts index 16283aed1aefc..46eb8644da9b8 100644 --- a/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/api-keys/[id]/route.ts @@ -37,7 +37,7 @@ export const POST = async ( const { result, errors } = await updateApiKeysWorkflow(req.scope).run({ input: { selector: { id: req.params.id }, - update: req.validatedBody, + update: req.validatedBody as UpdateApiKeyDTO, }, throwOnError: false, }) diff --git a/packages/types/src/api-key/mutations/api-key.ts b/packages/types/src/api-key/mutations/api-key.ts index d4444c619c883..168f9bf166a65 100644 --- a/packages/types/src/api-key/mutations/api-key.ts +++ b/packages/types/src/api-key/mutations/api-key.ts @@ -7,13 +7,18 @@ export interface CreateApiKeyDTO { // We could add revoked_at as a parameter (or expires_at that gets mapped to revoked_at internally) in order to support expiring tokens } +export interface UpsertApiKeyDTO { + id?: string + title?: string + type?: ApiKeyType + created_by?: string +} + export interface UpdateApiKeyDTO { - id: string title?: string } export interface RevokeApiKeyDTO { - id: string revoked_by: string revoke_in?: number // Seconds after which the token should be considered revoked } diff --git a/packages/types/src/api-key/service.ts b/packages/types/src/api-key/service.ts index d478f2aa8b240..ee9df48df8836 100644 --- a/packages/types/src/api-key/service.ts +++ b/packages/types/src/api-key/service.ts @@ -2,7 +2,12 @@ import { IModuleService } from "../modules-sdk" import { ApiKeyDTO, FilterableApiKeyProps } from "./common" import { FindConfig } from "../common" import { Context } from "../shared-context" -import { CreateApiKeyDTO, RevokeApiKeyDTO, UpdateApiKeyDTO } from "./mutations" +import { + CreateApiKeyDTO, + RevokeApiKeyDTO, + UpdateApiKeyDTO, + UpsertApiKeyDTO, +} from "./mutations" export interface IApiKeyModuleService extends IModuleService { /** @@ -14,32 +19,57 @@ export interface IApiKeyModuleService extends IModuleService { create(data: CreateApiKeyDTO, sharedContext?: Context): Promise /** - * Update an api key - * @param selector - * @param data - * @param sharedContext + * This method updates existing API keys, or creates new ones if they don't exist. + * + * @param {UpsertApiKeyDTO[]} data - The attributes to update or create for each API key. + * @returns {Promise} The updated and created API keys. + * + * @example + * {example-code} */ - update( - selector: FilterableApiKeyProps, - data: Omit, - sharedContext?: Context - ): Promise + upsert(data: UpsertApiKeyDTO[], sharedContext?: Context): Promise + /** - * Update an api key - * @param id - * @param data - * @param sharedContext + * This method updates an existing API key, or creates a new one if it doesn't exist. + * + * @param {UpsertApiKeyDTO} data - The attributes to update or create for the API key. + * @returns {Promise} The updated or created API key. + * + * @example + * {example-code} + */ + upsert(data: UpsertApiKeyDTO, sharedContext?: Context): Promise + + /** + * This method updates an existing API key. + * + * @param {string} id - The API key's ID. + * @param {UpdateApiKeyDTO} data - The details to update in the API key. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated API key. */ update( id: string, - data: Omit, + data: UpdateApiKeyDTO, sharedContext?: Context ): Promise + /** - * Update an api key - * @param data + * This method updates existing API keys. + * + * @param {FilterableApiKeyProps} selector - The filters to specify which API keys should be updated. + * @param {UpdateApiKeyDTO} data - The details to update in the API keys. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated API keys. + * + * @example + * {example-code} */ - update(data: UpdateApiKeyDTO[]): Promise + update( + selector: FilterableApiKeyProps, + data: UpdateApiKeyDTO, + sharedContext?: Context + ): Promise /** * Delete an api key @@ -93,7 +123,7 @@ export interface IApiKeyModuleService extends IModuleService { */ revoke( selector: FilterableApiKeyProps, - data: Omit, + data: RevokeApiKeyDTO, sharedContext?: Context ): Promise /** @@ -104,14 +134,9 @@ export interface IApiKeyModuleService extends IModuleService { */ revoke( id: string, - data: Omit, + data: RevokeApiKeyDTO, sharedContext?: Context ): Promise - /** - * Revokes an api key - * @param data - */ - revoke(data: RevokeApiKeyDTO[]): Promise /** * Check the validity of an api key