feat(medusa, medusa-js, medusa-react): PublishableApiKey "update" endpoint & add "title" property (#2609)

**What**
- update PK endpoint
  - medusa-js/react implementation
- add a title property to the entity
  - update the migration file
  - pass a title on create
  - list PKs by title
  - update the client libs with new param signatures
- change id prefix to: "pk_"
This commit is contained in:
Frane Polić
2022-11-16 05:35:22 +01:00
committed by GitHub
parent ccfc5f666d
commit 03fc9e18e9
17 changed files with 390 additions and 30 deletions

View File

@@ -82,9 +82,15 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
await simplePublishableApiKeyFactory(dbConnection, {})
await simplePublishableApiKeyFactory(dbConnection, {})
await simplePublishableApiKeyFactory(dbConnection, {})
await simplePublishableApiKeyFactory(dbConnection, {
title: "just a title",
})
await simplePublishableApiKeyFactory(dbConnection, {
title: "special title 1",
})
await simplePublishableApiKeyFactory(dbConnection, {
title: "special title 2",
})
})
afterEach(async () => {
@@ -105,6 +111,30 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
expect(response.data.offset).toBe(0)
expect(response.data.publishable_api_keys).toHaveLength(2)
})
it("list publishable keys with query search", async () => {
const api = useApi()
const response = await api.get(
`/admin/publishable-api-keys?q=special`,
adminHeaders
)
expect(response.data.count).toBe(2)
expect(response.data.limit).toBe(20)
expect(response.data.offset).toBe(0)
expect(response.data.publishable_api_keys).toHaveLength(2)
expect(response.data.publishable_api_keys).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "special title 1",
}),
expect.objectContaining({
title: "special title 2",
}),
])
)
})
})
describe("POST /admin/publishable-api-keys", () => {
@@ -122,7 +152,7 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
const response = await api.post(
`/admin/publishable-api-keys`,
{},
{ title: "Store api key" },
adminHeaders
)
@@ -130,6 +160,45 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
expect(response.data.publishable_api_key).toMatchObject({
created_by: "admin_user",
id: expect.any(String),
title: "Store api key",
revoked_by: null,
revoked_at: null,
created_at: expect.any(String),
updated_at: expect.any(String),
})
})
})
describe("POST /admin/publishable-api-keys/:id", () => {
const pubKeyId = IdMap.getId("pubkey-get-id-update")
beforeEach(async () => {
await adminSeeder(dbConnection)
await simplePublishableApiKeyFactory(dbConnection, {
id: pubKeyId,
title: "Initial key title",
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("update a publishable key", async () => {
const api = useApi()
const response = await api.post(
`/admin/publishable-api-keys/${pubKeyId}`,
{ title: "Changed title" },
adminHeaders
)
expect(response.status).toBe(200)
expect(response.data.publishable_api_key).toMatchObject({
id: pubKeyId,
title: "Changed title",
revoked_by: null,
revoked_at: null,
created_at: expect.any(String),

View File

@@ -9,7 +9,6 @@ const adminSeeder = require("../../helpers/admin-seeder")
const {
simpleSalesChannelFactory,
simpleProductFactory,
simpleCartFactory,
} = require("../../factories")
const { simpleOrderFactory } = require("../../factories")
const orderSeeder = require("../../helpers/order-seeder")

View File

@@ -3,7 +3,6 @@ import {
MoneyAmount,
PriceListType,
PriceListStatus,
CustomerGroup,
} from "@medusajs/medusa"
import faker from "faker"
import { Connection } from "typeorm"

View File

@@ -1,4 +1,6 @@
import faker from "faker"
import { Connection } from "typeorm"
import { PublishableApiKey } from "@medusajs/medusa"
export type PublishableApiKeyData = {
@@ -6,6 +8,7 @@ export type PublishableApiKeyData = {
revoked_at?: Date
revoked_by?: string
created_by?: string
title?: string
}
export const simplePublishableApiKeyFactory = async (
@@ -14,7 +17,10 @@ export const simplePublishableApiKeyFactory = async (
): Promise<PublishableApiKey> => {
const manager = connection.manager
const pubKey = manager.create(PublishableApiKey, data)
const pubKey = manager.create(PublishableApiKey, {
...data,
title: data.title || `${faker.commerce.department()} API key`,
})
return await manager.save(pubKey)
}

View File

@@ -5,6 +5,8 @@ import {
AdminPublishableApiKeysRes,
GetPublishableApiKeysParams,
AdminPublishableApiKeysListRes,
AdminPostPublishableApiKeysReq,
AdminPostPublishableApiKeysPublishableApiKeyReq,
} from "@medusajs/medusa"
import { ResponsePromise } from "../../typings"
@@ -41,13 +43,22 @@ class AdminPublishableApiKeyResource extends BaseResource {
}
create(
payload: {},
payload: AdminPostPublishableApiKeysReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPublishableApiKeysRes> {
const path = `/admin/publishable-api-keys`
return this.client.request("POST", path, payload, {}, customHeaders)
}
update(
id: string,
payload: AdminPostPublishableApiKeysPublishableApiKeyReq,
customHeaders: Record<string, any> = {}
) {
const path = `/admin/publishable-api-keys/${id}`
return this.client.request("POST", path, payload, {}, customHeaders)
}
delete(
id: string,
customHeaders: Record<string, any> = {}

View File

@@ -980,6 +980,18 @@ export const adminHandlers = [
)
}),
rest.post("/admin/publishable-api-keys/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
publishable_api_key: {
...fixtures.get("publishable_api_key"),
...(req.body as any),
},
})
)
}),
rest.post("/admin/publishable-api-keys/", (req, res, ctx) => {
return res(
ctx.status(200),

View File

@@ -4,6 +4,8 @@ import { Response } from "@medusajs/medusa-js"
import {
AdminPublishableApiKeyDeleteRes,
AdminPublishableApiKeysRes,
AdminPostPublishableApiKeysPublishableApiKeyReq,
AdminPostPublishableApiKeysReq,
} from "@medusajs/medusa"
import { buildOptions } from "../../utils/buildOptions"
@@ -11,16 +13,47 @@ import { useMedusa } from "../../../contexts"
import { adminPublishableApiKeysKeys } from "."
export const useAdminCreatePublishableApiKey = (
options?: UseMutationOptions<Response<AdminPublishableApiKeysRes>, Error, {}>
options?: UseMutationOptions<
Response<AdminPublishableApiKeysRes>,
Error,
AdminPostPublishableApiKeysReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: {}) => client.admin.publishableApiKeys.create(payload),
(payload: AdminPostPublishableApiKeysReq) =>
client.admin.publishableApiKeys.create(payload),
buildOptions(queryClient, [adminPublishableApiKeysKeys.lists()], options)
)
}
export const useAdminUpdatePublishableApiKey = (
id: string,
options?: UseMutationOptions<
Response<AdminPublishableApiKeysRes>,
Error,
AdminPostPublishableApiKeysPublishableApiKeyReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostPublishableApiKeysPublishableApiKeyReq) =>
client.admin.publishableApiKeys.update(id, payload),
buildOptions(
queryClient,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(id),
adminPublishableApiKeysKeys.details(),
],
options
)
)
}
export const useAdminDeletePublishableApiKey = (
id: string,
options?: UseMutationOptions<

View File

@@ -1,12 +1,13 @@
import { renderHook } from "@testing-library/react-hooks"
import { createWrapper } from "../../../utils"
import {
useAdminDeletePublishableApiKey,
useAdminRevokePublishableApiKey,
useAdminUpdatePublishableApiKey,
useAdminCreatePublishableApiKey,
} from "../../../../src"
import { createWrapper } from "../../../utils"
import { fixtures } from "../../../../mocks/data"
import { useAdminCreatePublishableApiKey } from "../../../../src"
describe("useAdminCreatePublishableApiKey hook", () => {
test("Created a publishable api key", async () => {
@@ -17,7 +18,7 @@ describe("useAdminCreatePublishableApiKey hook", () => {
}
)
result.current.mutate({})
result.current.mutate({ title: "Mandatory title" })
await waitFor(() => result.current.isSuccess)
@@ -25,6 +26,7 @@ describe("useAdminCreatePublishableApiKey hook", () => {
expect(result.current.data).toEqual(
expect.objectContaining({
publishable_api_key: {
title: "Mandatory title",
...fixtures.get("publishable_api_key"),
},
})
@@ -32,6 +34,34 @@ describe("useAdminCreatePublishableApiKey hook", () => {
})
})
describe("useAdminUpdatePublishableApiKey hook", () => {
test("updates an publishable key and returns it", async () => {
const pubKey = {
title: "changed title",
}
const { result, waitFor } = renderHook(
() =>
useAdminUpdatePublishableApiKey(fixtures.get("publishable_api_key").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate(pubKey)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.publishable_api_key).toEqual(
expect.objectContaining({
...fixtures.get("publishable_api_key"),
...pubKey,
})
)
})
})
describe("useAdminRevokePublishableApiKey hook", () => {
test("Revoke a publishable api key", async () => {
const id = "pubkey_1234"

View File

@@ -1,5 +1,6 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { IsString } from "class-validator"
import PublishableApiKeyService from "../../../../services/publishable-api-key"
@@ -8,6 +9,16 @@ import PublishableApiKeyService from "../../../../services/publishable-api-key"
* operationId: "PostPublishableApiKeys"
* summary: "Create a PublishableApiKey"
* description: "Creates a PublishableApiKey."
* requestBody:
* content:
* application/json:
* schema:
* required:
* - title
* properties:
* title:
* description: A title for the publishable api key
* type: string
* x-authenticated: true
* x-codeSamples:
* - lang: JavaScript
@@ -59,14 +70,20 @@ export default async (req: Request, res: Response) => {
) as PublishableApiKeyService
const manager = req.scope.resolve("manager") as EntityManager
const data = req.validatedBody as AdminPostPublishableApiKeysReq
const loggedInUserId = (req.user?.id ?? req.user?.userId) as string
const pubKey = await manager.transaction(async (transactionManager) => {
return await publishableApiKeyService
.withTransaction(transactionManager)
.create({ loggedInUserId })
.create(data, { loggedInUserId })
})
return res.status(200).json({ publishable_api_key: pubKey })
}
export class AdminPostPublishableApiKeysReq {
@IsString()
title: string
}

View File

@@ -2,10 +2,15 @@ import { Router } from "express"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import { GetPublishableApiKeysParams } from "./list-publishable-api-keys"
import { PublishableApiKey } from "../../../../models"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import { AdminPostPublishableApiKeysReq } from "./create-publishable-api-key"
import { AdminPostPublishableApiKeysPublishableApiKeyReq } from "./update-publishable-api-key"
const route = Router()
@@ -18,6 +23,7 @@ export default (app) => {
route.post(
"/",
transformBody(AdminPostPublishableApiKeysReq),
middlewares.wrap(require("./create-publishable-api-key").default)
)
@@ -26,6 +32,12 @@ export default (app) => {
middlewares.wrap(require("./get-publishable-api-key").default)
)
route.post(
"/:id",
transformBody(AdminPostPublishableApiKeysPublishableApiKeyReq),
middlewares.wrap(require("./update-publishable-api-key").default)
)
route.delete(
"/:id",
middlewares.wrap(require("./delete-publishable-api-key").default)
@@ -54,3 +66,5 @@ export type AdminPublishableApiKeysListRes = PaginatedResponse & {
export type AdminPublishableApiKeyDeleteRes = DeleteResponse
export * from "./list-publishable-api-keys"
export * from "./create-publishable-api-key"
export * from "./update-publishable-api-key"

View File

@@ -11,7 +11,7 @@ import PublishableApiKeyService from "../../../../services/publishable-api-key"
* description: "List PublishableApiKeys."
* x-authenticated: true
* parameters:
* - (query) order_id {string} List publishable keys by id.
* - (query) q {string} Query used for searching publishable api keys by title.
* - (query) limit=20 {number} The number of items in the response
* - (query) offset=0 {number} The offset of items in response
* - (query) expand {string} Comma separated list of relations to include in the results.
@@ -84,4 +84,8 @@ export default async (req: Request, res: Response) => {
export class GetPublishableApiKeysParams extends extendedFindParamsMixin({
limit: 20,
offset: 0,
}) {}
}) {
@IsString()
@IsOptional()
q?: string
}

View File

@@ -0,0 +1,97 @@
import { Request, Response } from "express"
import { IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import PublishableApiKeyService from "../../../../services/publishable-api-key"
/**
* @oas [post] /publishable-api-key/{id}
* operationId: "PostPublishableApiKysPublishableApiKey"
* summary: "Updates a PublishableApiKey"
* description: "Updates a PublishableApiKey."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the PublishableApiKey.
* requestBody:
* content:
* application/json:
* schema:
* properties:
* title:
* description: A title to update for the key.
* type: string
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.publishableApiKey.update(publishable_key_id, {
* title: "new title"
* })
* .then(({ publishable_api_key }) => {
* console.log(publishable_api_key.id)
* })
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/admin/publishable-api-key/{id}' \
* --header 'Authorization: Bearer {api_token}' \
* --header 'Content-Type: application/json' \
* --data-raw '{
* "title": "updated title"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - PublishableApiKey
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* publishable_api_key:
* $ref: "#/components/schemas/publishable_api_key"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const { validatedBody } = req as {
validatedBody: AdminPostPublishableApiKeysPublishableApiKeyReq
}
const publishableApiKeysService: PublishableApiKeyService = req.scope.resolve(
"publishableApiKeyService"
)
const manager: EntityManager = req.scope.resolve("manager")
const updatedKey = await manager.transaction(async (transactionManager) => {
return await publishableApiKeysService
.withTransaction(transactionManager)
.update(id, validatedBody)
})
res.status(200).json({ publishable_api_key: updatedKey })
}
export class AdminPostPublishableApiKeysPublishableApiKeyReq {
@IsString()
@IsOptional()
title?: string
}

View File

@@ -12,7 +12,7 @@ export class publishableApiKey1667815005070 implements MigrationInterface {
`CREATE TABLE "publishable_api_key_sales_channel" ("sales_channel_id" character varying NOT NULL, "publishable_key_id" character varying NOT NULL, CONSTRAINT "PK_68eaeb14bdac8954460054c567c" PRIMARY KEY ("sales_channel_id", "publishable_key_id"))`
)
await queryRunner.query(
`CREATE TABLE "publishable_api_key" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_by" character varying, "revoked_by" character varying, "revoked_at" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_9e613278673a87de92c606b4494" PRIMARY KEY ("id"))`
`CREATE TABLE "publishable_api_key" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "created_by" character varying, "revoked_by" character varying, "revoked_at" TIMESTAMP WITH TIME ZONE, "title" character varying NOT NULL, CONSTRAINT "PK_9e613278673a87de92c606b4494" PRIMARY KEY ("id"))`
)
}

View File

@@ -17,9 +17,12 @@ export class PublishableApiKey extends BaseEntity {
@Column({ type: resolveDbType("timestamptz"), nullable: true })
revoked_at?: Date
@Column()
title: string
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "pubkey")
this.id = generateEntityId(this.id, "pk")
}
}
@@ -32,7 +35,7 @@ export class PublishableApiKey extends BaseEntity {
* id:
* type: string
* description: The key's ID
* example: pak_01G1G5V27GYX4QXNARRQCW1N8T
* example: pk_01G1G5V27GYX4QXNARRQCW1N8T
* created_by:
* type: string
* description: "The unique identifier of the user that created the key."

View File

@@ -48,13 +48,17 @@ describe("PublishableApiKeyService", () => {
})
it("should create a publishable api key and call the repository with the right arguments as well as the event bus service", async () => {
await publishableApiKeyService.create({
loggedInUserId: IdMap.getId("admin_user"),
})
await publishableApiKeyService.create(
{ title: "API key title" },
{
loggedInUserId: IdMap.getId("admin_user"),
}
)
expect(publishableApiKeyRepository.create).toHaveBeenCalledTimes(1)
expect(publishableApiKeyRepository.create).toHaveBeenCalledWith({
created_by: IdMap.getId("admin_user"),
title: "API key title",
})
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
@@ -63,6 +67,19 @@ describe("PublishableApiKeyService", () => {
)
})
it("should update a publishable api key", async () => {
await publishableApiKeyService.update(pubKeyToRetrieve.id, {
title: "new title",
})
expect(publishableApiKeyRepository.save).toHaveBeenLastCalledWith(
expect.objectContaining({
id: pubKeyToRetrieve.id,
title: "new title",
})
)
})
it("should revoke a publishable api key", async () => {
await publishableApiKeyService.revoke("id", {
loggedInUserId: IdMap.getId("admin_user"),

View File

@@ -1,4 +1,4 @@
import { EntityManager } from "typeorm"
import { EntityManager, ILike } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
@@ -6,7 +6,11 @@ import { FindConfig, Selector } from "../types/common"
import { PublishableApiKey } from "../models"
import { TransactionBaseService } from "../interfaces"
import EventBusService from "./event-bus"
import { buildQuery } from "../utils"
import { buildQuery, isDefined, isString } from "../utils"
import {
CreatePublishableApiKeyInput,
UpdatePublishableApiKeyInput,
} from "../types/publishable-api-key"
type InjectedDependencies = {
manager: EntityManager
@@ -45,17 +49,22 @@ class PublishableApiKeyService extends TransactionBaseService {
/**
* Create a PublishableApiKey record.
*
* @params context - key creation context object
* @param data - partial data for creating the entity
* @param context - key creation context object
*/
async create(context: {
loggedInUserId: string
}): Promise<PublishableApiKey | never> {
async create(
data: CreatePublishableApiKeyInput,
context: {
loggedInUserId: string
}
): Promise<PublishableApiKey | never> {
return await this.atomicPhase_(async (manager) => {
const publishableApiKeyRepo = manager.getCustomRepository(
this.publishableApiKeyRepository_
)
const publishableApiKey = publishableApiKeyRepo.create({
...data,
created_by: context.loggedInUserId,
})
@@ -122,7 +131,7 @@ class PublishableApiKeyService extends TransactionBaseService {
* @return an array containing publishable API keys and a total count of records that matches the query
*/
async listAndCount(
selector: Selector<PublishableApiKey>,
selector: Selector<PublishableApiKey> & { q?: string },
config: FindConfig<PublishableApiKey> = {
skip: 0,
take: 20,
@@ -133,11 +142,44 @@ class PublishableApiKeyService extends TransactionBaseService {
this.publishableApiKeyRepository_
)
let q
if (isString(selector.q)) {
q = selector.q
delete selector.q
}
const query = buildQuery(selector, config)
if (q) {
query.where.title = ILike(`%${q}%`)
}
return await pubKeyRepo.findAndCount(query)
}
async update(
publishableApiKeyId: string,
data: UpdatePublishableApiKeyInput
): Promise<PublishableApiKey> {
{
return await this.atomicPhase_(async (manager) => {
const publishableApiKeyRepository = manager.getCustomRepository(
this.publishableApiKeyRepository_
)
const pubKey = await this.retrieve(publishableApiKeyId)
for (const key of Object.keys(data)) {
if (isDefined(data[key])) {
pubKey[key] = data[key]
}
}
return await publishableApiKeyRepository.save(pubKey)
})
}
}
/**
* Delete Publishable API key.
*

View File

@@ -0,0 +1,7 @@
export type CreatePublishableApiKeyInput = {
title: string
}
export type UpdatePublishableApiKeyInput = {
title?: string
}