feat(medusa): PublishableApiKeys CRUD (#2567)
This commit is contained in:
214
integration-tests/api/__tests__/admin/publishable-api-key.js
Normal file
214
integration-tests/api/__tests__/admin/publishable-api-key.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const path = require("path")
|
||||
const { IdMap } = require("medusa-test-utils")
|
||||
|
||||
const startServerWithEnvironment =
|
||||
require("../../../helpers/start-server-with-environment").default
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { useDb } = require("../../../helpers/use-db")
|
||||
const adminSeeder = require("../../helpers/admin-seeder")
|
||||
const {
|
||||
simplePublishableApiKeyFactory,
|
||||
} = require("../../factories/simple-publishable-api-key-factory")
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const adminHeaders = {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
}
|
||||
|
||||
describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
const adminUserId = "admin_user"
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_PUBLISHABLE_API_KEYS: true },
|
||||
verbose: false,
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("GET /admin/publishable-api-keys/:id", () => {
|
||||
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simplePublishableApiKeyFactory(dbConnection, {
|
||||
id: pubKeyId,
|
||||
created_by: adminUserId,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("retrieve a publishable key by id ", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(
|
||||
`/admin/publishable-api-keys/${pubKeyId}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
expect(response.data.publishable_api_key).toMatchObject({
|
||||
id: pubKeyId,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
created_by: adminUserId,
|
||||
revoked_by: null,
|
||||
revoked_at: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/publishable-api-keys", () => {
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simplePublishableApiKeyFactory(dbConnection, {})
|
||||
await simplePublishableApiKeyFactory(dbConnection, {})
|
||||
await simplePublishableApiKeyFactory(dbConnection, {})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("list publishable keys", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.get(
|
||||
`/admin/publishable-api-keys?limit=2`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.count).toBe(3)
|
||||
expect(response.data.limit).toBe(2)
|
||||
expect(response.data.offset).toBe(0)
|
||||
expect(response.data.publishable_api_keys).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/publishable-api-keys", () => {
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("crete a publishable keys", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/publishable-api-keys`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.data.publishable_api_key).toMatchObject({
|
||||
created_by: "admin_user",
|
||||
id: expect.any(String),
|
||||
revoked_by: null,
|
||||
revoked_at: null,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /admin/publishable-api-keys/:id/revoke", () => {
|
||||
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simplePublishableApiKeyFactory(dbConnection, {
|
||||
id: pubKeyId,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("revoke a publishable key", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/publishable-api-keys/${pubKeyId}/revoke`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
expect(response.data.publishable_api_key).toMatchObject({
|
||||
id: pubKeyId,
|
||||
created_at: expect.any(String),
|
||||
updated_at: expect.any(String),
|
||||
revoked_by: adminUserId,
|
||||
revoked_at: expect.any(String),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("DELETE /admin/publishable-api-keys/:id", () => {
|
||||
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
|
||||
await simplePublishableApiKeyFactory(dbConnection, {
|
||||
id: pubKeyId,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
})
|
||||
|
||||
it("delete a publishable key", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response1 = await api.delete(
|
||||
`/admin/publishable-api-keys/${pubKeyId}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response1.status).toBe(200)
|
||||
expect(response1.data).toEqual({
|
||||
id: pubKeyId,
|
||||
object: "publishable_api_key",
|
||||
deleted: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await api.get(`/admin/publishable-api-keys/${pubKeyId}`, adminHeaders)
|
||||
} catch (e) {
|
||||
expect(e.response.status).toBe(404)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Connection } from "typeorm"
|
||||
import { PublishableApiKey } from "@medusajs/medusa"
|
||||
|
||||
export type PublishableApiKeyData = {
|
||||
id?: string
|
||||
revoked_at?: Date
|
||||
revoked_by?: string
|
||||
created_by?: string
|
||||
}
|
||||
|
||||
export const simplePublishableApiKeyFactory = async (
|
||||
connection: Connection,
|
||||
data: PublishableApiKeyData = {}
|
||||
): Promise<PublishableApiKey> => {
|
||||
const manager = connection.manager
|
||||
|
||||
const pubKey = manager.create(PublishableApiKey, data)
|
||||
|
||||
return await manager.save(pubKey)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export * from "./routes/admin/price-lists"
|
||||
export * from "./routes/admin/product-tags"
|
||||
export * from "./routes/admin/product-types"
|
||||
export * from "./routes/admin/products"
|
||||
export * from "./routes/admin/publishable-api-keys"
|
||||
export * from "./routes/admin/regions"
|
||||
export * from "./routes/admin/return-reasons"
|
||||
export * from "./routes/admin/returns"
|
||||
|
||||
@@ -20,6 +20,7 @@ import orderRoutes from "./orders"
|
||||
import priceListRoutes from "./price-lists"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import publishableApiKeyRoutes from "./publishable-api-keys"
|
||||
import productRoutes from "./products"
|
||||
import regionRoutes from "./regions"
|
||||
import returnReasonRoutes from "./return-reasons"
|
||||
@@ -89,6 +90,7 @@ export default (app, container, config) => {
|
||||
productRoutes(route, featureFlagRouter)
|
||||
productTagRoutes(route)
|
||||
productTypesRoutes(route)
|
||||
publishableApiKeyRoutes(route)
|
||||
regionRoutes(route, featureFlagRouter)
|
||||
returnReasonRoutes(route)
|
||||
returnRoutes(route)
|
||||
|
||||
@@ -6,8 +6,8 @@ import { IsOptional, IsString } from "class-validator"
|
||||
/**
|
||||
* @oas [get] /order-edits
|
||||
* operationId: "GetOrderEdits"
|
||||
* summary: "List an OrderEdit"
|
||||
* description: "List a OrderEdit."
|
||||
* summary: "List OrderEdits"
|
||||
* description: "List OrderEdits."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) q {string} Query used for searching order edit internal note.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Request, Response } from "express"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
|
||||
/**
|
||||
* @oas [post] /publishable-api-keys
|
||||
* operationId: "PostPublishableApiKeys"
|
||||
* summary: "Create a PublishableApiKey"
|
||||
* description: "Creates a PublishableApiKey."
|
||||
* x-authenticated: true
|
||||
* 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.create()
|
||||
* .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-keys' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* -d '{ "created_by": "user_123" }'
|
||||
* 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 publishableApiKeyService = req.scope.resolve(
|
||||
"publishableApiKeyService"
|
||||
) as PublishableApiKeyService
|
||||
|
||||
const manager = req.scope.resolve("manager") as EntityManager
|
||||
|
||||
const loggedInUserId = (req.user?.id ?? req.user?.userId) as string
|
||||
|
||||
const pubKey = await manager.transaction(async (transactionManager) => {
|
||||
return await publishableApiKeyService
|
||||
.withTransaction(transactionManager)
|
||||
.create({ loggedInUserId })
|
||||
})
|
||||
|
||||
return res.status(200).json({ publishable_api_key: pubKey })
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
|
||||
/**
|
||||
* @oas [delete] /publishable-api-keys/{id}
|
||||
* operationId: "DeletePublishableApiKeysPublishableApiKey"
|
||||
* summary: "Delete a PublishableApiKey"
|
||||
* description: "Deletes a PublishableApiKeys"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The ID of the PublishableApiKeys to delete.
|
||||
* 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.delete(key_id)
|
||||
* .then(({ id, object, deleted }) => {
|
||||
* console.log(id)
|
||||
* })
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request DELETE 'https://medusa-url.com/admin/publishable-api-key/{id}' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* security:
|
||||
* - api_token: []
|
||||
* - cookie_auth: []
|
||||
* tags:
|
||||
* - PublishableApiKey
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: The ID of the deleted PublishableApiKey.
|
||||
* object:
|
||||
* type: string
|
||||
* description: The type of the object that was deleted.
|
||||
* format: publishable_api_key
|
||||
* deleted:
|
||||
* type: boolean
|
||||
* description: Whether the PublishableApiKeys was deleted.
|
||||
* default: true
|
||||
* "400":
|
||||
* $ref: "#/components/responses/400_error"
|
||||
*/
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const publishableApiKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||
"publishableApiKeyService"
|
||||
)
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await publishableApiKeyService
|
||||
.withTransaction(transactionManager)
|
||||
.delete(id)
|
||||
})
|
||||
|
||||
res.status(200).send({
|
||||
id,
|
||||
object: "publishable_api_key",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Request, Response } from "express"
|
||||
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
|
||||
/**
|
||||
* @oas [get] /publishable-api-keys/{id}
|
||||
* operationId: "GetPublishableApiKeysPublishableApiKey"
|
||||
* summary: "Get a Publishable API Key"
|
||||
* description: "Retrieve the Publishable Api Key."
|
||||
* parameters:
|
||||
* - (path) id=* {string} The ID of the PublishableApiKey.
|
||||
* x-authenticated: true
|
||||
* 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.retrieve(pubKeyId)
|
||||
* .then(({ publishable_api_key }) => {
|
||||
* console.log(publishable_api_key.id)
|
||||
* })
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request GET 'https://medusa-url.com/admin/publishable-api-keys/pubkey_123' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* -d '{ "created_by": "user_123" }'
|
||||
* 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 publishableApiKeyService = req.scope.resolve(
|
||||
"publishableApiKeyService"
|
||||
) as PublishableApiKeyService
|
||||
|
||||
const pubKey = await publishableApiKeyService.retrieve(id)
|
||||
|
||||
return res.json({ publishable_api_key: pubKey })
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
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 { GetPublishableApiKeysParams } from "./list-publishable-api-keys"
|
||||
import { PublishableApiKey } from "../../../../models"
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use(
|
||||
"/publishable-api-keys",
|
||||
isFeatureFlagEnabled(PublishableAPIKeysFeatureFlag.key),
|
||||
route
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/",
|
||||
middlewares.wrap(require("./create-publishable-api-key").default)
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/:id",
|
||||
middlewares.wrap(require("./get-publishable-api-key").default)
|
||||
)
|
||||
|
||||
route.delete(
|
||||
"/:id",
|
||||
middlewares.wrap(require("./delete-publishable-api-key").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:id/revoke",
|
||||
middlewares.wrap(require("./revoke-publishable-api-key").default)
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/",
|
||||
transformQuery(GetPublishableApiKeysParams, {
|
||||
isList: true,
|
||||
}),
|
||||
middlewares.wrap(require("./list-publishable-api-keys").default)
|
||||
)
|
||||
}
|
||||
|
||||
export type AdminPublishableApiKeysRes = {
|
||||
publishable_api_key: PublishableApiKey
|
||||
}
|
||||
export type AdminPublishableApiKeysListRes = PaginatedResponse & {
|
||||
publishable_api_keys: PublishableApiKey[]
|
||||
}
|
||||
export type AdminPublishableApiKeyDeleteRes = DeleteResponse
|
||||
|
||||
export * from "./list-publishable-api-keys"
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Request, Response } from "express"
|
||||
import { IsOptional, IsString } from "class-validator"
|
||||
|
||||
import { extendedFindParamsMixin } from "../../../../types/common"
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
|
||||
/**
|
||||
* @oas [get] /publishable-api-keys
|
||||
* operationId: "GetPublishableApiKeys"
|
||||
* summary: "List PublishableApiKeys"
|
||||
* description: "List PublishableApiKeys."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) order_id {string} List publishable keys by id.
|
||||
* - (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.
|
||||
* - (query) fields {string} Comma separated list of fields to include in the results.
|
||||
* 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.publishableApiKeys.list()
|
||||
* .then(({ publishable_api_keys }) => {
|
||||
* console.log(publishable_api_keys)
|
||||
* })
|
||||
* - lang: Shell
|
||||
* label: cURL
|
||||
* source: |
|
||||
* curl --location --request GET 'https://medusa-url.com/admin/publishable-api-keys' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* security:
|
||||
* - api_token: []
|
||||
* - cookie_auth: []
|
||||
* tags:
|
||||
* - PublishableApiKeys
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* publishable_api_keys:
|
||||
* type: array
|
||||
* $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 publishableApiKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||
"publishableApiKeyService"
|
||||
)
|
||||
|
||||
const { filterableFields, listConfig } = req
|
||||
const { skip, take } = listConfig
|
||||
|
||||
const [pubKeys, count] = await publishableApiKeyService.listAndCount(
|
||||
filterableFields,
|
||||
listConfig
|
||||
)
|
||||
|
||||
return res.json({
|
||||
publishable_api_keys: pubKeys,
|
||||
count,
|
||||
limit: take,
|
||||
offset: skip,
|
||||
})
|
||||
}
|
||||
|
||||
export class GetPublishableApiKeysParams extends extendedFindParamsMixin({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}) {}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Request, Response } from "express"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||
|
||||
/**
|
||||
* @oas [post] /publishable-api-keys/{id}/revoke
|
||||
* operationId: "PostPublishableApiKeysPublishableApiKeyRevoke"
|
||||
* summary: "Revoke a PublishableApiKey"
|
||||
* description: "Revokes a PublishableApiKey."
|
||||
* parameters:
|
||||
* - (path) id=* {string} The ID of the PublishableApiKey.
|
||||
* x-authenticated: true
|
||||
* 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.revoke()
|
||||
* .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-keys/pubkey_123/revoke' \
|
||||
* --header 'Authorization: Bearer {api_token}'
|
||||
* -d '{ "created_by": "user_123", "revoked_by": "user_123" }'
|
||||
* 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 publishableApiKeyService = req.scope.resolve(
|
||||
"publishableApiKeyService"
|
||||
) as PublishableApiKeyService
|
||||
|
||||
const manager = req.scope.resolve("manager") as EntityManager
|
||||
|
||||
const loggedInUserId = (req.user?.id ?? req.user?.userId) as string
|
||||
|
||||
const pubKey = await manager.transaction(async (transactionManager) => {
|
||||
const publishableApiKeyServiceTx =
|
||||
publishableApiKeyService.withTransaction(transactionManager)
|
||||
|
||||
await publishableApiKeyServiceTx.revoke(id, { loggedInUserId })
|
||||
return await publishableApiKeyServiceTx.retrieve(id)
|
||||
})
|
||||
|
||||
return res.json({ publishable_api_key: pubKey })
|
||||
}
|
||||
@@ -1,6 +1,71 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { flatten, groupBy, merge } from "lodash"
|
||||
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
||||
|
||||
import { PublishableApiKey } from "../models/publishable-api-key"
|
||||
|
||||
@EntityRepository(PublishableApiKey)
|
||||
export class PublishableApiKeyRepository extends Repository<PublishableApiKey> {}
|
||||
export class PublishableApiKeyRepository extends Repository<PublishableApiKey> {
|
||||
public async findWithRelations(
|
||||
relations: (keyof PublishableApiKey | string)[] = [],
|
||||
idsOrOptionsWithoutRelations:
|
||||
| Omit<FindManyOptions<PublishableApiKey>, "relations">
|
||||
| string[] = {}
|
||||
): Promise<[PublishableApiKey[], number]> {
|
||||
let entities: PublishableApiKey[] = []
|
||||
let count = 0
|
||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
||||
count = idsOrOptionsWithoutRelations.length
|
||||
} else {
|
||||
const [results, resultCount] = await this.findAndCount(
|
||||
idsOrOptionsWithoutRelations
|
||||
)
|
||||
entities = results
|
||||
count = resultCount
|
||||
}
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
|
||||
const groupedRelations = {}
|
||||
for (const rel of relations) {
|
||||
const [topLevel] = rel.split(".")
|
||||
if (groupedRelations[topLevel]) {
|
||||
groupedRelations[topLevel].push(rel)
|
||||
} else {
|
||||
groupedRelations[topLevel] = [rel]
|
||||
}
|
||||
}
|
||||
|
||||
const entitiesIdsWithRelations = await Promise.all(
|
||||
Object.entries(groupedRelations).map(async ([_, rels]) => {
|
||||
return this.findByIds(entitiesIds, {
|
||||
select: ["id"],
|
||||
relations: rels as string[],
|
||||
})
|
||||
})
|
||||
).then(flatten)
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
|
||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||
return [
|
||||
Object.values(entitiesAndRelationsById).map((v) => merge({}, ...v)),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
public async findOneWithRelations(
|
||||
relations: Array<keyof PublishableApiKey> = [],
|
||||
optionsWithoutRelations: Omit<
|
||||
FindManyOptions<PublishableApiKey>,
|
||||
"relations"
|
||||
> = {}
|
||||
): Promise<PublishableApiKey> {
|
||||
// Limit 1
|
||||
optionsWithoutRelations.take = 1
|
||||
|
||||
const [result] = await this.findWithRelations(
|
||||
relations,
|
||||
optionsWithoutRelations
|
||||
)
|
||||
return result[0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
|
||||
import { EventBusService } from "../index"
|
||||
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||
import PublishableApiKeyService from "../publishable-api-key"
|
||||
|
||||
const pubKeyToRetrieve = {
|
||||
id: IdMap.getId("pub-key-to-retrieve"),
|
||||
created_at: new Date(),
|
||||
created_by: IdMap.getId("admin_user"),
|
||||
revoked_by: null,
|
||||
revoked_at: null,
|
||||
}
|
||||
|
||||
describe("PublishableApiKeyService", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const publishableApiKeyRepository = MockRepository({
|
||||
findOneWithRelations: (data) => ({ ...pubKeyToRetrieve, ...data }),
|
||||
create: (data) => {
|
||||
return {
|
||||
...pubKeyToRetrieve,
|
||||
...data,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const publishableApiKeyService = new PublishableApiKeyService({
|
||||
manager: MockManager,
|
||||
publishableApiKeyRepository: publishableApiKeyRepository,
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
})
|
||||
|
||||
it("should retrieve a publishable api key and call the repository with the right arguments", async () => {
|
||||
await publishableApiKeyService.retrieve(
|
||||
IdMap.getId("order-edit-with-changes")
|
||||
)
|
||||
expect(
|
||||
publishableApiKeyRepository.findOneWithRelations
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
publishableApiKeyRepository.findOneWithRelations
|
||||
).toHaveBeenCalledWith(undefined, {
|
||||
where: { id: IdMap.getId("order-edit-with-changes") },
|
||||
})
|
||||
})
|
||||
|
||||
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"),
|
||||
})
|
||||
|
||||
expect(publishableApiKeyRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(publishableApiKeyRepository.create).toHaveBeenCalledWith({
|
||||
created_by: IdMap.getId("admin_user"),
|
||||
})
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
||||
PublishableApiKeyService.Events.CREATED,
|
||||
{ id: expect.any(String) }
|
||||
)
|
||||
})
|
||||
|
||||
it("should revoke a publishable api key", async () => {
|
||||
await publishableApiKeyService.revoke("id", {
|
||||
loggedInUserId: IdMap.getId("admin_user"),
|
||||
})
|
||||
|
||||
expect(publishableApiKeyRepository.save).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
revoked_by: IdMap.getId("admin_user"),
|
||||
})
|
||||
)
|
||||
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
|
||||
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
||||
PublishableApiKeyService.Events.REVOKED,
|
||||
{ id: expect.any(String) }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -35,6 +35,7 @@ export { default as ProductService } from "./product"
|
||||
export { default as ProductCollectionService } from "./product-collection"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
export { default as ProductVariantService } from "./product-variant"
|
||||
import { default as PublishableApiKey } from "./publishable-api-key"
|
||||
export { default as RegionService } from "./region"
|
||||
export { default as ReturnService } from "./return"
|
||||
export { default as ReturnReasonService } from "./return-reason"
|
||||
|
||||
210
packages/medusa/src/services/publishable-api-key.ts
Normal file
210
packages/medusa/src/services/publishable-api-key.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
|
||||
import { FindConfig, Selector } from "../types/common"
|
||||
import { PublishableApiKey } from "../models"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import EventBusService from "./event-bus"
|
||||
import { buildQuery } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
|
||||
eventBusService: EventBusService
|
||||
publishableApiKeyRepository: typeof PublishableApiKeyRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* A service for PublishableApiKey business logic.
|
||||
*/
|
||||
class PublishableApiKeyService extends TransactionBaseService {
|
||||
static Events = {
|
||||
CREATED: "publishable_api_key.created",
|
||||
REVOKED: "publishable_api_key.revoked",
|
||||
}
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly publishableApiKeyRepository_: typeof PublishableApiKeyRepository
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
eventBusService,
|
||||
publishableApiKeyRepository,
|
||||
}: InjectedDependencies) {
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
this.eventBusService_ = eventBusService
|
||||
this.publishableApiKeyRepository_ = publishableApiKeyRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PublishableApiKey record.
|
||||
*
|
||||
* @params context - key creation context object
|
||||
*/
|
||||
async create(context: {
|
||||
loggedInUserId: string
|
||||
}): Promise<PublishableApiKey | never> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const publishableApiKeyRepo = manager.getCustomRepository(
|
||||
this.publishableApiKeyRepository_
|
||||
)
|
||||
|
||||
const publishableApiKey = publishableApiKeyRepo.create({
|
||||
created_by: context.loggedInUserId,
|
||||
})
|
||||
|
||||
await this.eventBusService_
|
||||
.withTransaction(manager)
|
||||
.emit(PublishableApiKeyService.Events.CREATED, {
|
||||
id: publishableApiKey.id,
|
||||
})
|
||||
|
||||
return await publishableApiKeyRepo.save(publishableApiKey)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a PublishableApiKey by id
|
||||
*
|
||||
* @param publishableApiKeyId - id of the key
|
||||
* @param config - a find config object
|
||||
*/
|
||||
async retrieve(
|
||||
publishableApiKeyId: string,
|
||||
config: FindConfig<PublishableApiKey> = {}
|
||||
): Promise<PublishableApiKey | never> {
|
||||
return await this.retrieve_({ id: publishableApiKeyId }, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic retrieve for selecting PublishableApiKEys by different attributes.
|
||||
*
|
||||
* @param selector - a PublishableApiKey selector object
|
||||
* @param config - a find config object
|
||||
*/
|
||||
protected async retrieve_(
|
||||
selector: Selector<PublishableApiKey>,
|
||||
config: FindConfig<PublishableApiKey> = {}
|
||||
): Promise<PublishableApiKey | never> {
|
||||
const repo = this.manager_.getCustomRepository(
|
||||
this.publishableApiKeyRepository_
|
||||
)
|
||||
|
||||
const { relations, ...query } = buildQuery(selector, config)
|
||||
const publishableApiKey = await repo.findOneWithRelations(
|
||||
relations as (keyof PublishableApiKey)[],
|
||||
query
|
||||
)
|
||||
|
||||
if (!publishableApiKey) {
|
||||
const selectorConstraints = Object.entries(selector)
|
||||
.map((key, value) => `${key}: ${value}`)
|
||||
.join(", ")
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Publishable key with ${selectorConstraints} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return publishableApiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists publishable API keys based on the provided parameters.
|
||||
*
|
||||
* @return an array containing publishable API keys and a total count of records that matches the query
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: Selector<PublishableApiKey>,
|
||||
config: FindConfig<PublishableApiKey> = {
|
||||
skip: 0,
|
||||
take: 20,
|
||||
}
|
||||
): Promise<[PublishableApiKey[], number]> {
|
||||
const manager = this.manager_
|
||||
const pubKeyRepo = manager.getCustomRepository(
|
||||
this.publishableApiKeyRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery(selector, config)
|
||||
|
||||
return await pubKeyRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Publishable API key.
|
||||
*
|
||||
* @param publishableApiKeyId - id of the key being deleted
|
||||
*/
|
||||
async delete(publishableApiKeyId: string): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const repo = manager.getCustomRepository(
|
||||
this.publishableApiKeyRepository_
|
||||
)
|
||||
|
||||
const publishableApiKey = await this.retrieve(publishableApiKeyId).catch()
|
||||
|
||||
if (publishableApiKey) {
|
||||
await repo.remove(publishableApiKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a PublishableApiKey
|
||||
*
|
||||
* @param publishableApiKeyId - id of the key
|
||||
* @param context - key revocation context object
|
||||
*/
|
||||
async revoke(
|
||||
publishableApiKeyId: string,
|
||||
context: {
|
||||
loggedInUserId: string
|
||||
}
|
||||
): Promise<void | never> {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const repo = manager.getCustomRepository(
|
||||
this.publishableApiKeyRepository_
|
||||
)
|
||||
|
||||
const pubKey = await this.retrieve(publishableApiKeyId)
|
||||
|
||||
if (pubKey.revoked_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`PublishableApiKey has already been revoked.`
|
||||
)
|
||||
}
|
||||
|
||||
pubKey.revoked_at = new Date()
|
||||
pubKey.revoked_by = context.loggedInUserId
|
||||
|
||||
await repo.save(pubKey)
|
||||
|
||||
await this.eventBusService_
|
||||
.withTransaction(manager)
|
||||
.emit(PublishableApiKeyService.Events.REVOKED, {
|
||||
id: pubKey.id,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the key is active (i.e. haven't been revoked or deleted yet)
|
||||
*
|
||||
* @param publishableApiKeyId - id of the key
|
||||
*/
|
||||
async isValid(publishableApiKeyId: string): Promise<boolean> {
|
||||
const pubKey = await this.retrieve(publishableApiKeyId)
|
||||
return pubKey.revoked_by === null
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishableApiKeyService
|
||||
Reference in New Issue
Block a user