feat(medusa): PublishableApiKeys SC scopes (#2590)
**What**
- add/ remove sales channels to/from PublishableApiKey in batch
- associate created cart with SC defined by PK
- filter products if PK with SC association is present
- retrieve a list of sales channels for a PK
- implement 3 new middleware
- `extendRequestParams`
- _extend req object with PK scopes (a list of sales channels) if a publishable key is present in the header of the request_
- `validateProductSalesChannelAssociation`
- _validate if the passed product id belongs to a SC from the PK's scope_
- `validateSalesChannelParam`
- _validate that passed SC ids in the req body/query are within the scope of the PK_
**How**
- The general idea was to reuse existing logic in the controller layer which expects `sales_channel_id` array to be passed. The middleware sets associated SC ids in the request context if a PK is present. These ids are then merged in the controller and passed to the service layer.
**TODO**
- filter response from the search endpoint (CORE-824)
**Testing**
- _integration tests_
- add sales channels to the publishable API key scope
- remove sales channels from the publishable API key scope
- returns products from a specific channel associated with a publishable key
- returns products from multiples sales channels associated with a publishable key
- returns all products if PK is not passed
- returns all products if passed PK doesn't have associated channels
- should assign sales channel to order on cart completion if PK is present in the header
- list sales channels from the publishable api key
- throws because sales channel in query/body is not in the scope of passed PK
---
**Discussion**
- what about the other endpoints (e.g. GET /store/product/:id - do we return 404 if the product is not in the SC associated with passed PK)
- what about products search route
- what about `/admin/products` & `/admin/orders` routes (do we add the middleware there as well)
---
RESOLVES CORE-792
RESOLVES CORE-793
RESOLVES CORE-816
This commit is contained in:
1 parent
1b21af87ab
commit
322d462311
21 files changed
+1551
-20
No files matched your search
@@ -9,6 +9,11 @@ const adminSeeder = require("../../helpers/admin-seeder")
|
|||||||
const {
|
const {
|
||||||
simplePublishableApiKeyFactory,
|
simplePublishableApiKeyFactory,
|
||||||
} = require("../../factories/simple-publishable-api-key-factory")
|
} = require("../../factories/simple-publishable-api-key-factory")
|
||||||
|
const {
|
||||||
|
simpleSalesChannelFactory,
|
||||||
|
simpleProductFactory,
|
||||||
|
simpleRegionFactory,
|
||||||
|
} = require("../../factories")
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
@@ -18,6 +23,13 @@ const adminHeaders = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customerData = {
|
||||||
|
email: "medusa@test.hr",
|
||||||
|
password: "medusatest",
|
||||||
|
first_name: "medusa",
|
||||||
|
last_name: "medusa",
|
||||||
|
}
|
||||||
|
|
||||||
describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
|
describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
|
||||||
let medusaProcess
|
let medusaProcess
|
||||||
let dbConnection
|
let dbConnection
|
||||||
@@ -27,7 +39,10 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
|
|||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
const [process, connection] = await startServerWithEnvironment({
|
const [process, connection] = await startServerWithEnvironment({
|
||||||
cwd,
|
cwd,
|
||||||
env: { MEDUSA_FF_PUBLISHABLE_API_KEYS: true },
|
env: {
|
||||||
|
MEDUSA_FF_PUBLISHABLE_API_KEYS: true,
|
||||||
|
MEDUSA_FF_SALES_CHANNELS: true,
|
||||||
|
},
|
||||||
verbose: false,
|
verbose: false,
|
||||||
})
|
})
|
||||||
dbConnection = connection
|
dbConnection = connection
|
||||||
@@ -280,4 +295,777 @@ describe("[MEDUSA_FF_PUBLISHABLE_API_KEYS] Publishable API keys", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/publishable-api-keys/:id/sales-channels/batch", () => {
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id-batch")
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel2 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name 2",
|
||||||
|
description: "test description 2",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("add sales channels to the publishable api key scope", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: salesChannel1.id },
|
||||||
|
{ id: salesChannel2.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const mappings = await dbConnection.manager.query(
|
||||||
|
`SELECT *
|
||||||
|
FROM publishable_api_key_sales_channel
|
||||||
|
WHERE publishable_key_id = '${pubKeyId}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
|
||||||
|
expect(mappings).toEqual([
|
||||||
|
{
|
||||||
|
sales_channel_id: salesChannel1.id,
|
||||||
|
publishable_key_id: pubKeyId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sales_channel_id: salesChannel2.id,
|
||||||
|
publishable_key_id: pubKeyId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(response.data.publishable_api_key).toMatchObject({
|
||||||
|
id: pubKeyId,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /admin/publishable-api-keys/:id/sales-channels/batch", () => {
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id-batch-v2")
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
let salesChannel3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel2 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name 2",
|
||||||
|
description: "test description 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel3 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name 3",
|
||||||
|
description: "test description 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
await dbConnection.manager.query(
|
||||||
|
`INSERT INTO
|
||||||
|
publishable_api_key_sales_channel
|
||||||
|
(publishable_key_id, sales_channel_id)
|
||||||
|
VALUES
|
||||||
|
('${pubKeyId}', '${salesChannel1.id}'),
|
||||||
|
('${pubKeyId}', '${salesChannel2.id}'),
|
||||||
|
('${pubKeyId}', '${salesChannel3.id}');`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("remove sales channels from the publishable api key scope", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.delete(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: salesChannel1.id },
|
||||||
|
{ id: salesChannel2.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...adminHeaders,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const mappings = await dbConnection.manager.query(
|
||||||
|
`SELECT *
|
||||||
|
FROM publishable_api_key_sales_channel
|
||||||
|
WHERE publishable_key_id = '${pubKeyId}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
|
||||||
|
expect(mappings).toEqual([
|
||||||
|
{
|
||||||
|
sales_channel_id: salesChannel3.id,
|
||||||
|
publishable_key_id: pubKeyId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(response.data.publishable_api_key).toMatchObject({
|
||||||
|
id: pubKeyId,
|
||||||
|
created_at: expect.any(String),
|
||||||
|
updated_at: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /admin/publishable-api-keys/:id/sales-channels", () => {
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id-batch-v2")
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
let salesChannel3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel2 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name 2",
|
||||||
|
description: "test description 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel3 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "test name 3",
|
||||||
|
description: "test description 3",
|
||||||
|
})
|
||||||
|
|
||||||
|
await dbConnection.manager.query(
|
||||||
|
`INSERT INTO
|
||||||
|
publishable_api_key_sales_channel
|
||||||
|
(publishable_key_id, sales_channel_id)
|
||||||
|
VALUES
|
||||||
|
('${pubKeyId}', '${salesChannel1.id}'),
|
||||||
|
('${pubKeyId}', '${salesChannel2.id}');`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("list sales channels from the publishable api key", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.sales_channels).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel1.id,
|
||||||
|
deleted_at: null,
|
||||||
|
name: "test name",
|
||||||
|
description: "test description",
|
||||||
|
is_disabled: false,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: salesChannel2.id,
|
||||||
|
deleted_at: null,
|
||||||
|
name: "test name 2",
|
||||||
|
description: "test description 2",
|
||||||
|
is_disabled: false,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /store/products", () => {
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||||
|
|
||||||
|
let salesChannel1
|
||||||
|
let salesChannel2
|
||||||
|
let product1
|
||||||
|
let product2
|
||||||
|
let product3
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "salesChannel1",
|
||||||
|
description: "salesChannel1",
|
||||||
|
})
|
||||||
|
|
||||||
|
salesChannel2 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "salesChannel2",
|
||||||
|
description: "salesChannel2",
|
||||||
|
})
|
||||||
|
|
||||||
|
product1 = await simpleProductFactory(dbConnection, {
|
||||||
|
title: "prod 1",
|
||||||
|
status: "published",
|
||||||
|
sales_channels: [salesChannel1],
|
||||||
|
})
|
||||||
|
|
||||||
|
product2 = await simpleProductFactory(dbConnection, {
|
||||||
|
title: "prod 2",
|
||||||
|
status: "published",
|
||||||
|
sales_channels: [salesChannel2],
|
||||||
|
})
|
||||||
|
|
||||||
|
product3 = await simpleProductFactory(dbConnection, {
|
||||||
|
title: "prod 3",
|
||||||
|
status: "published",
|
||||||
|
})
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
created_by: adminUserId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns products from a specific channel associated with a publishable key", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: salesChannel1.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns products from multiples sales channels associated with a publishable key", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: salesChannel1.id },
|
||||||
|
{ id: salesChannel2.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(2)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("SC param overrides PK channels (but SK still needs to be in the PK's scope", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: salesChannel1.id },
|
||||||
|
{ id: salesChannel2.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(
|
||||||
|
`/store/products?sales_channel_id[0]=${salesChannel2.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(1)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns all products if PK is not passed", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: salesChannel1.id },
|
||||||
|
{ id: salesChannel2.id },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
// "x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(3)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product3.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns all products if passed PK doesn't have associated channels", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.products.length).toBe(3)
|
||||||
|
expect(response.data.products).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product2.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product3.id,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws because sales channel param is not in the scope of passed PK", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: salesChannel1.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.get(
|
||||||
|
`/store/products?sales_channel_id[]=${salesChannel2.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data.errors[0]).toEqual(
|
||||||
|
`Provided sales channel id param: ${salesChannel2.id} is not associated with the Publishable API Key passed in the header of the request.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("GET /store/products/:id", () => {
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||||
|
|
||||||
|
let salesChannel1
|
||||||
|
let product1
|
||||||
|
let product2
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
salesChannel1 = await simpleSalesChannelFactory(dbConnection, {
|
||||||
|
name: "salesChannel1",
|
||||||
|
description: "salesChannel1",
|
||||||
|
})
|
||||||
|
|
||||||
|
product1 = await simpleProductFactory(dbConnection, {
|
||||||
|
title: "prod 1",
|
||||||
|
status: "published",
|
||||||
|
sales_channels: [salesChannel1],
|
||||||
|
})
|
||||||
|
|
||||||
|
product2 = await simpleProductFactory(dbConnection, {
|
||||||
|
title: "prod 2",
|
||||||
|
status: "published",
|
||||||
|
})
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
created_by: adminUserId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("retrieve a products from a specific channel associated with a publishable key", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: salesChannel1.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.get(`/store/products/${product1.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: product1.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("return 400 because requested product is not in the SC associated with a publishable key", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: salesChannel1.id }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.get(`/store/products/${product2.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("correctly returns a product if passed PK has no associated SCs", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
let response = await api
|
||||||
|
.get(`/store/products/${product1.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
response = await api
|
||||||
|
.get(`/store/products/${product2.id}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return err.response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /store/carts/:id", () => {
|
||||||
|
let product
|
||||||
|
const pubKeyId = IdMap.getId("pubkey-get-id")
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
await simpleRegionFactory(dbConnection, {
|
||||||
|
id: "test-region",
|
||||||
|
})
|
||||||
|
|
||||||
|
await simplePublishableApiKeyFactory(dbConnection, {
|
||||||
|
id: pubKeyId,
|
||||||
|
created_by: adminUserId,
|
||||||
|
})
|
||||||
|
|
||||||
|
product = await simpleProductFactory(dbConnection, {
|
||||||
|
sales_channels: [
|
||||||
|
{
|
||||||
|
id: "sales-channel",
|
||||||
|
name: "Sales channel",
|
||||||
|
description: "Sales channel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sales-channel2",
|
||||||
|
name: "Sales channel2",
|
||||||
|
description: "Sales channel2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
return await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should assign sales channel to order on cart completion if PK is present in the header", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: "sales-channel" }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const customerRes = await api.post("/store/customers", customerData, {
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createCartRes = await api.post(
|
||||||
|
"/store/carts",
|
||||||
|
{
|
||||||
|
region_id: "test-region",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = createCartRes.data.cart
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}`, {
|
||||||
|
customer_id: customerRes.data.customer.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||||
|
|
||||||
|
const createdOrder = await api.post(
|
||||||
|
`/store/carts/${cart.id}/complete-cart`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(createdOrder.data.type).toEqual("order")
|
||||||
|
expect(createdOrder.status).toEqual(200)
|
||||||
|
expect(createdOrder.data.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
sales_channel_id: "sales-channel",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("SC from params defines where product is assigned (passed SC still has to be in the scope of PK from the header)", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [
|
||||||
|
{ id: "sales-channel" },
|
||||||
|
{ id: "sales-channel2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const customerRes = await api.post("/store/customers", customerData, {
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createCartRes = await api.post(
|
||||||
|
"/store/carts",
|
||||||
|
{
|
||||||
|
sales_channel_id: "sales-channel2",
|
||||||
|
region_id: "test-region",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const cart = createCartRes.data.cart
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}`, {
|
||||||
|
customer_id: customerRes.data.customer.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||||
|
|
||||||
|
const createdOrder = await api.post(
|
||||||
|
`/store/carts/${cart.id}/complete-cart`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(createdOrder.data.type).toEqual("order")
|
||||||
|
expect(createdOrder.status).toEqual(200)
|
||||||
|
expect(createdOrder.data.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
sales_channel_id: "sales-channel2",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should throw because SC id in the body is not in the scope of PK from the header", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/publishable-api-keys/${pubKeyId}/sales-channels/batch`,
|
||||||
|
{
|
||||||
|
sales_channel_ids: [{ id: "sales-channel" }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post(
|
||||||
|
"/store/carts",
|
||||||
|
{
|
||||||
|
sales_channel_id: "sales-channel2", // SC not in the PK scope
|
||||||
|
region_id: "test-region",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
"x-publishable-api-key": pubKeyId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data.errors[0]).toEqual(
|
||||||
|
`Provided sales channel id param: sales-channel2 is not associated with the Publishable API Key passed in the header of the request.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -6,15 +6,9 @@ const { useApi } = require("../../../../helpers/use-api")
|
|||||||
const { useDb } = require("../../../../helpers/use-db")
|
const { useDb } = require("../../../../helpers/use-db")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
simpleCartFactory,
|
|
||||||
simpleRegionFactory,
|
simpleRegionFactory,
|
||||||
simpleShippingOptionFactory,
|
|
||||||
simpleCustomShippingOptionFactory,
|
|
||||||
simpleProductFactory,
|
simpleProductFactory,
|
||||||
simplePriceListFactory,
|
|
||||||
simpleDiscountFactory,
|
|
||||||
} = require("../../../factories")
|
} = require("../../../factories")
|
||||||
const { IdMap } = require("medusa-test-utils")
|
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
|
import PublishableApiKeyService from "../../../services/publishable-api-key"
|
||||||
|
|
||||||
|
export type PublishableApiKeyScopes = {
|
||||||
|
sales_channel_id: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The middleware, in case that a key is present in the request header,
|
||||||
|
* attaches ids of resources within the scope of the key to the req object.
|
||||||
|
*
|
||||||
|
* @param req - request object
|
||||||
|
* @param res - response object
|
||||||
|
* @param next - next middleware call
|
||||||
|
*
|
||||||
|
* @throws if sales channel id is passed as a url or body param
|
||||||
|
* but that id is not in the scope defined by the PK from the header
|
||||||
|
*/
|
||||||
|
async function extendRequestParams(
|
||||||
|
req: Request & { publishableApiKeyScopes: PublishableApiKeyScopes },
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const pubKey = req.get("x-publishable-api-key")
|
||||||
|
|
||||||
|
if (pubKey) {
|
||||||
|
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||||
|
"publishableApiKeyService"
|
||||||
|
)
|
||||||
|
|
||||||
|
req.publishableApiKeyScopes = await publishableKeyService.getResourceScopes(
|
||||||
|
pubKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { extendRequestParams }
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
|
import PublishableApiKeyService from "../../../services/publishable-api-key"
|
||||||
|
import { ProductService } from "../../../services"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The middleware check if requested product is assigned to a SC associated with PK in the header.
|
||||||
|
*
|
||||||
|
* @param req - request object
|
||||||
|
* @param res - response object
|
||||||
|
* @param next - next middleware call
|
||||||
|
*/
|
||||||
|
async function validateProductSalesChannelAssociation(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const pubKey = req.get("x-publishable-api-key")
|
||||||
|
|
||||||
|
if (pubKey) {
|
||||||
|
const productService: ProductService = req.scope.resolve("productService")
|
||||||
|
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||||
|
"publishableApiKeyService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const { sales_channel_id: salesChannelIds } =
|
||||||
|
await publishableKeyService.getResourceScopes(pubKey)
|
||||||
|
|
||||||
|
if (
|
||||||
|
salesChannelIds.length &&
|
||||||
|
!(await productService.isProductInSalesChannels(
|
||||||
|
req.params.id,
|
||||||
|
salesChannelIds
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
req.errors = req.errors ?? []
|
||||||
|
req.errors.push(
|
||||||
|
`Product with id: ${req.params.id} is not associated with sales channels defined by the Publishable API Key passed in the header of the request.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { validateProductSalesChannelAssociation }
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
|
import { PublishableApiKeyScopes } from "./extend-request-params"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The middleware will return 400 if sales channel id is passed as an url or body param
|
||||||
|
* but that id is not in the scope of the PK from the header.
|
||||||
|
*
|
||||||
|
* NOTE: must be applied after the `extendRequestParams` middleware
|
||||||
|
*
|
||||||
|
* @param req - request object
|
||||||
|
* @param res - response object
|
||||||
|
* @param next - next middleware call
|
||||||
|
*/
|
||||||
|
async function validateSalesChannelParam(
|
||||||
|
req: Request & { publishableApiKeyScopes: PublishableApiKeyScopes },
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const pubKey = req.get("x-publishable-api-key")
|
||||||
|
|
||||||
|
if (pubKey) {
|
||||||
|
const scopes = req.publishableApiKeyScopes
|
||||||
|
let channelIds = req.body.sales_channel_id || req.query.sales_channel_id
|
||||||
|
|
||||||
|
if (!channelIds) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIds = !Array.isArray(channelIds) ? [channelIds] : channelIds
|
||||||
|
|
||||||
|
if (
|
||||||
|
scopes.sales_channel_id.length &&
|
||||||
|
!channelIds.every((sc) => scopes.sales_channel_id.includes(sc))
|
||||||
|
) {
|
||||||
|
req.errors = req.errors ?? []
|
||||||
|
req.errors.push(
|
||||||
|
`Provided sales channel id param: ${channelIds} is not associated with the Publishable API Key passed in the header of the request.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { validateSalesChannelParam }
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { IsArray, ValidateNested } from "class-validator"
|
||||||
|
import { Request, Response } from "express"
|
||||||
|
import { Type } from "class-transformer"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
|
import { ProductBatchSalesChannel } from "../../../../types/sales-channels"
|
||||||
|
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [post] /publishable-api-keys/{id}/sales-channels/batch
|
||||||
|
* operationId: "PostPublishableApiKeySalesChannelsChannelsBatch"
|
||||||
|
* summary: "Add sales channel to a publishable api key scope"
|
||||||
|
* description: "Assign a batch of sales channels to a publishable api key."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Publishable Api Key.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* required:
|
||||||
|
* - sales_channel_ids
|
||||||
|
* properties:
|
||||||
|
* sales_channel_ids:
|
||||||
|
* description: The IDs of the sales channels to add to the publishable api key
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* description: The ID of the sales channel
|
||||||
|
* 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.addSalesChannels(publishableApiKeyId, {
|
||||||
|
* sales_channel_ids: [
|
||||||
|
* {
|
||||||
|
* id: channel_id
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* })
|
||||||
|
* .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/afasf/batch' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}' \
|
||||||
|
* --header 'Content-Type: application/json' \
|
||||||
|
* --data-raw '{
|
||||||
|
* "sales_channel_ids": [
|
||||||
|
* {
|
||||||
|
* "id": "{sales_channel_id}"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Publishable Api Key
|
||||||
|
* 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): Promise<void> => {
|
||||||
|
const validatedBody =
|
||||||
|
req.validatedBody as AdminPostPublishableApiKeySalesChannelsBatchReq
|
||||||
|
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const publishableApiKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||||
|
"publishableApiKeyService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
const publishableApiKey = await manager.transaction(
|
||||||
|
async (transactionManager) => {
|
||||||
|
await publishableApiKeyService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.addSalesChannels(
|
||||||
|
id,
|
||||||
|
validatedBody.sales_channel_ids.map((p) => p.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await publishableApiKeyService.retrieve(id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ publishable_api_key: publishableApiKey })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminPostPublishableApiKeySalesChannelsBatchReq {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ProductBatchSalesChannel)
|
||||||
|
sales_channel_ids: ProductBatchSalesChannel[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { IsArray, ValidateNested } from "class-validator"
|
||||||
|
import { Request, Response } from "express"
|
||||||
|
import { Type } from "class-transformer"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
|
import { ProductBatchSalesChannel } from "../../../../types/sales-channels"
|
||||||
|
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [delete] /publishable-api-keys/{id}/sales-channels/batch
|
||||||
|
* operationId: "DeletePublishableApiKeySalesChannelsChannelsBatch"
|
||||||
|
* summary: "Remove sales channel from a publishable api key scope"
|
||||||
|
* description: "Remove a batch of sales channels from a publishable api key."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Publishable Api Key.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* required:
|
||||||
|
* - sales_channel_ids
|
||||||
|
* properties:
|
||||||
|
* sales_channel_ids:
|
||||||
|
* description: The IDs of the sales channels to delete from the publishable api key
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* description: The ID of the sales channel
|
||||||
|
* 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.removeSalesChannels(publishableApiKeyId, {
|
||||||
|
* sales_channel_ids: [
|
||||||
|
* {
|
||||||
|
* id: channel_id
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* })
|
||||||
|
* .then(({ publishable_api_key }) => {
|
||||||
|
* console.log(publishable_api_key.id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request DELETE 'https://medusa-url.com/admin/publishable-api-keys/afasf/batch' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}' \
|
||||||
|
* --header 'Content-Type: application/json' \
|
||||||
|
* --data-raw '{
|
||||||
|
* "sales_channel_ids": [
|
||||||
|
* {
|
||||||
|
* "id": "{sales_channel_id}"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Publishable Api Key
|
||||||
|
* 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): Promise<void> => {
|
||||||
|
const validatedBody =
|
||||||
|
req.validatedBody as AdminDeletePublishableApiKeySalesChannelsBatchReq
|
||||||
|
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const publishableApiKeyService: PublishableApiKeyService = req.scope.resolve(
|
||||||
|
"publishableApiKeyService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
const publishableApiKey = await manager.transaction(
|
||||||
|
async (transactionManager) => {
|
||||||
|
await publishableApiKeyService
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.removeSalesChannels(
|
||||||
|
id,
|
||||||
|
validatedBody.sales_channel_ids.map((p) => p.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await publishableApiKeyService.retrieve(id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ publishable_api_key: publishableApiKey })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminDeletePublishableApiKeySalesChannelsBatchReq {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ProductBatchSalesChannel)
|
||||||
|
sales_channel_ids: ProductBatchSalesChannel[]
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ import { PublishableApiKey } from "../../../../models"
|
|||||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||||
import { AdminPostPublishableApiKeysReq } from "./create-publishable-api-key"
|
import { AdminPostPublishableApiKeysReq } from "./create-publishable-api-key"
|
||||||
import { AdminPostPublishableApiKeysPublishableApiKeyReq } from "./update-publishable-api-key"
|
import { AdminPostPublishableApiKeysPublishableApiKeyReq } from "./update-publishable-api-key"
|
||||||
|
import { AdminDeletePublishableApiKeySalesChannelsBatchReq } from "./delete-channels-batch"
|
||||||
|
import { AdminPostPublishableApiKeySalesChannelsBatchReq } from "./add-channels-batch"
|
||||||
|
import { GetPublishableApiKeySalesChannelsParams } from "./list-publishable-api-key-sales-channels"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -55,6 +58,26 @@ export default (app) => {
|
|||||||
}),
|
}),
|
||||||
middlewares.wrap(require("./list-publishable-api-keys").default)
|
middlewares.wrap(require("./list-publishable-api-keys").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
"/:id/sales-channels",
|
||||||
|
transformQuery(GetPublishableApiKeySalesChannelsParams, { isList: true }),
|
||||||
|
middlewares.wrap(
|
||||||
|
require("./list-publishable-api-key-sales-channels").default
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/:id/sales-channels/batch",
|
||||||
|
transformBody(AdminPostPublishableApiKeySalesChannelsBatchReq),
|
||||||
|
middlewares.wrap(require("./add-channels-batch").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
"/:id/sales-channels/batch",
|
||||||
|
transformBody(AdminDeletePublishableApiKeySalesChannelsBatchReq),
|
||||||
|
middlewares.wrap(require("./delete-channels-batch").default)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminPublishableApiKeysRes = {
|
export type AdminPublishableApiKeysRes = {
|
||||||
@@ -65,6 +88,9 @@ export type AdminPublishableApiKeysListRes = PaginatedResponse & {
|
|||||||
}
|
}
|
||||||
export type AdminPublishableApiKeyDeleteRes = DeleteResponse
|
export type AdminPublishableApiKeyDeleteRes = DeleteResponse
|
||||||
|
|
||||||
|
export * from "./add-channels-batch"
|
||||||
|
export * from "./delete-channels-batch"
|
||||||
export * from "./list-publishable-api-keys"
|
export * from "./list-publishable-api-keys"
|
||||||
|
export * from "./list-publishable-api-key-sales-channels"
|
||||||
export * from "./create-publishable-api-key"
|
export * from "./create-publishable-api-key"
|
||||||
export * from "./update-publishable-api-key"
|
export * from "./update-publishable-api-key"
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
|
import PublishableApiKeyService from "../../../../services/publishable-api-key"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [get] /publishable-api-keys/:id/sales-channels
|
||||||
|
* operationId: "GetPublishableApiKeySalesChannels"
|
||||||
|
* summary: "List PublishableApiKey's SalesChannels"
|
||||||
|
* description: "List PublishableApiKey's SalesChannels"
|
||||||
|
* 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.publishableApiKeys.listSalesChannels()
|
||||||
|
* .then(({ sales_channels, limit, offset, count }) => {
|
||||||
|
* console.log(sales_channels)
|
||||||
|
* })
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request GET 'https://medusa-url.com/admin/publishable-api-keys/pk_123/sales-channels' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - PublishableApiKeySalesChannels
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* properties:
|
||||||
|
* sales_channels:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/sales_channel"
|
||||||
|
* "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: PublishableApiKeyService = req.scope.resolve(
|
||||||
|
"publishableApiKeyService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const salesChannels = await publishableApiKeyService.listSalesChannels(id)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
sales_channels: salesChannels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetPublishableApiKeySalesChannelsParams {}
|
||||||
@@ -2,12 +2,11 @@ import {
|
|||||||
DateComparisonOperator,
|
DateComparisonOperator,
|
||||||
extendedFindParamsMixin,
|
extendedFindParamsMixin,
|
||||||
} from "../../../../types/common"
|
} from "../../../../types/common"
|
||||||
import { IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"
|
import { IsOptional, IsString, ValidateNested } from "class-validator"
|
||||||
import { Request, Response } from "express"
|
import { Request, Response } from "express"
|
||||||
|
|
||||||
import { SalesChannelService } from "../../../../services"
|
import { SalesChannelService } from "../../../../services"
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { removeUndefinedProperties } from "../../../../utils"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /sales-channels
|
* @oas [get] /sales-channels
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { FlagRouter } from "../../../../utils/flag-router"
|
|||||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||||
import { CartCreateProps } from "../../../../types/cart"
|
import { CartCreateProps } from "../../../../types/cart"
|
||||||
import { isDefined } from "../../../../utils"
|
import { isDefined } from "../../../../utils"
|
||||||
|
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /carts
|
* @oas [post] /carts
|
||||||
@@ -159,6 +160,23 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
|
||||||
|
if (
|
||||||
|
!toCreate.sales_channel_id &&
|
||||||
|
req.publishableApiKeyScopes?.sales_channel_id.length
|
||||||
|
) {
|
||||||
|
if (req.publishableApiKeyScopes.sales_channel_id.length > 1) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.UNEXPECTED_STATE,
|
||||||
|
"The PublishableApiKey provided in the request header has multiple associated sales channels."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
toCreate.sales_channel_id =
|
||||||
|
req.publishableApiKeyScopes.sales_channel_id[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let cart: Cart
|
let cart: Cart
|
||||||
await entityManager.transaction(async (manager) => {
|
await entityManager.transaction(async (manager) => {
|
||||||
cart = await cartService.withTransaction(manager).create(toCreate)
|
cart = await cartService.withTransaction(manager).create(toCreate)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Router } from "express"
|
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
|
import { RequestHandler, Router } from "express"
|
||||||
|
|
||||||
import { Cart, Order, Swap } from "../../../../"
|
import { Cart, Order, Swap } from "../../../../"
|
||||||
import { DeleteResponse, FindParams } from "../../../../types/common"
|
import { DeleteResponse, FindParams } from "../../../../types/common"
|
||||||
import middlewares, {
|
import middlewares, {
|
||||||
@@ -8,6 +9,10 @@ import middlewares, {
|
|||||||
} from "../../../middlewares"
|
} from "../../../middlewares"
|
||||||
import { StorePostCartsCartReq } from "./update-cart"
|
import { StorePostCartsCartReq } from "./update-cart"
|
||||||
import { StorePostCartReq } from "./create-cart"
|
import { StorePostCartReq } from "./create-cart"
|
||||||
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||||
|
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
||||||
|
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
|
||||||
|
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -17,7 +22,7 @@ export default (app, container) => {
|
|||||||
|
|
||||||
app.use("/carts", route)
|
app.use("/carts", route)
|
||||||
|
|
||||||
if (featureFlagRouter.isFeatureEnabled("sales_channels")) {
|
if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) {
|
||||||
defaultStoreCartRelations.push("sales_channel")
|
defaultStoreCartRelations.push("sales_channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +42,21 @@ export default (app, container) => {
|
|||||||
middlewares.wrap(require("./get-cart").default)
|
middlewares.wrap(require("./get-cart").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
route.post(
|
const createMiddlewares = [
|
||||||
"/",
|
|
||||||
middlewareService.usePreCartCreation(),
|
middlewareService.usePreCartCreation(),
|
||||||
transformBody(StorePostCartReq),
|
transformBody(StorePostCartReq),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
|
||||||
|
createMiddlewares.push(
|
||||||
|
extendRequestParams as unknown as RequestHandler,
|
||||||
|
validateSalesChannelParam as unknown as RequestHandler
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/",
|
||||||
|
...createMiddlewares,
|
||||||
middlewares.wrap(require("./create-cart").default)
|
middlewares.wrap(require("./create-cart").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,14 @@ export default (app, container, config) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||||
|
|
||||||
route.use(middlewares.authenticateCustomer())
|
route.use(middlewares.authenticateCustomer())
|
||||||
|
|
||||||
authRoutes(route)
|
authRoutes(route)
|
||||||
collectionRoutes(route)
|
collectionRoutes(route)
|
||||||
customerRoutes(route, container)
|
customerRoutes(route, container)
|
||||||
productRoutes(route)
|
productRoutes(route, featureFlagRouter)
|
||||||
productTypesRoutes(route)
|
productTypesRoutes(route)
|
||||||
orderRoutes(route)
|
orderRoutes(route)
|
||||||
orderEditRoutes(route)
|
orderEditRoutes(route)
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
import { Router } from "express"
|
import { RequestHandler, Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
|
|
||||||
import { Product } from "../../../.."
|
import { Product } from "../../../.."
|
||||||
import { PaginatedResponse } from "../../../../types/common"
|
|
||||||
import middlewares from "../../../middlewares"
|
import middlewares from "../../../middlewares"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
import { PaginatedResponse } from "../../../../types/common"
|
||||||
|
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
|
||||||
|
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
||||||
|
import { validateProductSalesChannelAssociation } from "../../../middlewares/publishable-api-key/validate-product-sales-channel-association"
|
||||||
|
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app, featureFlagRouter: FlagRouter) => {
|
||||||
app.use("/products", route)
|
app.use("/products", route)
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
|
||||||
|
route.use(
|
||||||
|
"/",
|
||||||
|
extendRequestParams as unknown as RequestHandler,
|
||||||
|
validateSalesChannelParam as unknown as RequestHandler
|
||||||
|
)
|
||||||
|
route.use("/:id", validateProductSalesChannelAssociation)
|
||||||
|
}
|
||||||
|
|
||||||
route.get("/", middlewares.wrap(require("./list-products").default))
|
route.get("/", middlewares.wrap(require("./list-products").default))
|
||||||
route.post("/search", middlewares.wrap(require("./search").default))
|
|
||||||
route.get("/:id", middlewares.wrap(require("./get-product").default))
|
route.get("/:id", middlewares.wrap(require("./get-product").default))
|
||||||
|
route.post("/search", middlewares.wrap(require("./search").default))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators
|
|||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
|
||||||
import { IsType } from "../../../../utils/validators/is-type"
|
import { IsType } from "../../../../utils/validators/is-type"
|
||||||
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
|
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [get] /products
|
* @oas [get] /products
|
||||||
@@ -195,8 +197,18 @@ export default async (req, res) => {
|
|||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
const regionService: RegionService = req.scope.resolve("regionService")
|
const regionService: RegionService = req.scope.resolve("regionService")
|
||||||
|
|
||||||
|
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||||
|
|
||||||
const validated = await validator(StoreGetProductsParams, req.query)
|
const validated = await validator(StoreGetProductsParams, req.query)
|
||||||
|
|
||||||
|
if (featureFlagRouter.isFeatureEnabled(PublishableAPIKeysFeatureFlag.key)) {
|
||||||
|
if (req.publishableApiKeyScopes?.sales_channel_id.length) {
|
||||||
|
validated.sales_channel_id =
|
||||||
|
validated.sales_channel_id ||
|
||||||
|
req.publishableApiKeyScopes.sales_channel_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filterableFields: StoreGetProductsParams = omit(validated, [
|
const filterableFields: StoreGetProductsParams = omit(validated, [
|
||||||
"fields",
|
"fields",
|
||||||
"expand",
|
"expand",
|
||||||
|
|||||||
@@ -397,6 +397,22 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
return [products, count]
|
return [products, count]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isProductInSalesChannels(
|
||||||
|
id: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
(await this.createQueryBuilder("product")
|
||||||
|
.leftJoin(
|
||||||
|
"product.sales_channels",
|
||||||
|
"sales_channels",
|
||||||
|
"sales_channels.id IN (:...salesChannelIds)",
|
||||||
|
{ salesChannelIds }
|
||||||
|
)
|
||||||
|
.getCount()) > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private _cleanOptions(
|
private _cleanOptions(
|
||||||
options: FindWithoutRelationsOptions
|
options: FindWithoutRelationsOptions
|
||||||
): WithRequiredProperty<FindWithoutRelationsOptions, "where"> {
|
): WithRequiredProperty<FindWithoutRelationsOptions, "where"> {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { EntityRepository, In, Repository } from "typeorm"
|
||||||
|
|
||||||
|
import { PublishableApiKeySalesChannel, SalesChannel } from "../models"
|
||||||
|
|
||||||
|
@EntityRepository(PublishableApiKeySalesChannel)
|
||||||
|
export class PublishableApiKeySalesChannelRepository extends Repository<PublishableApiKeySalesChannel> {
|
||||||
|
/**
|
||||||
|
* Query a list of sales channels that are assigned to the publishable key scope
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId - id of the key to retrieve channels for
|
||||||
|
*/
|
||||||
|
public async findSalesChannels(
|
||||||
|
publishableApiKeyId: string
|
||||||
|
): Promise<SalesChannel[]> {
|
||||||
|
const data = await this.createQueryBuilder("PublishableKeySalesChannel")
|
||||||
|
.select("PublishableKeySalesChannel.sales_channel_id")
|
||||||
|
.innerJoinAndMapOne(
|
||||||
|
"PublishableKeySalesChannel.sales_channel_id",
|
||||||
|
SalesChannel,
|
||||||
|
"SalesChannel",
|
||||||
|
"PublishableKeySalesChannel.sales_channel_id = SalesChannel.id"
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
"PublishableKeySalesChannel.publishable_key_id = :publishableApiKeyId",
|
||||||
|
{
|
||||||
|
publishableApiKeyId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.getMany()
|
||||||
|
|
||||||
|
return data.map(
|
||||||
|
(record) => record.sales_channel_id as unknown as SalesChannel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign (multiple) sales channels to the Publishable Key scope
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId - publishable key id
|
||||||
|
* @param salesChannelIds - an array of SC ids
|
||||||
|
*/
|
||||||
|
public async addSalesChannels(
|
||||||
|
publishableApiKeyId: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
await this.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into("publishable_api_key_sales_channel")
|
||||||
|
.values(
|
||||||
|
salesChannelIds.map((id) => ({
|
||||||
|
sales_channel_id: id,
|
||||||
|
publishable_key_id: publishableApiKeyId,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.orIgnore()
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove multiple sales channels from the PK scope
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId -publishable key id
|
||||||
|
* @param salesChannelIds - an array of SC ids
|
||||||
|
*/
|
||||||
|
public async removeSalesChannels(
|
||||||
|
publishableApiKeyId: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
await this.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.from("publishable_api_key_sales_channel")
|
||||||
|
.where({
|
||||||
|
sales_channel_id: In(salesChannelIds),
|
||||||
|
publishable_key_id: publishableApiKeyId,
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { flatten, groupBy, merge } from "lodash"
|
import { flatten, groupBy, merge } from "lodash"
|
||||||
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
|
||||||
|
|
||||||
import { PublishableApiKey } from "../models/publishable-api-key"
|
import { PublishableApiKey } from "../models"
|
||||||
|
|
||||||
@EntityRepository(PublishableApiKey)
|
@EntityRepository(PublishableApiKey)
|
||||||
export class PublishableApiKeyRepository extends Repository<PublishableApiKey> {
|
export class PublishableApiKeyRepository extends Repository<PublishableApiKey> {
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ describe("PublishableApiKeyService", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const publishableApiKeySalesChannelRepository = MockRepository({})
|
||||||
|
|
||||||
const publishableApiKeyService = new PublishableApiKeyService({
|
const publishableApiKeyService = new PublishableApiKeyService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
publishableApiKeySalesChannelRepository:
|
||||||
|
publishableApiKeySalesChannelRepository,
|
||||||
publishableApiKeyRepository: publishableApiKeyRepository,
|
publishableApiKeyRepository: publishableApiKeyRepository,
|
||||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -324,6 +324,29 @@ class ProductService extends TransactionBaseService {
|
|||||||
return await productTagRepo.listTagsByUsage(count)
|
return await productTagRepo.listTagsByUsage(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the product is assigned to at least one of the provided sales channels.
|
||||||
|
*
|
||||||
|
* @param id - product id
|
||||||
|
* @param salesChannelIds - an array of sales channel ids
|
||||||
|
*/
|
||||||
|
async isProductInSalesChannels(
|
||||||
|
id: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
const product = await this.retrieve_(
|
||||||
|
{ id },
|
||||||
|
{ relations: ["sales_channels"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: reimplement this to use db level check
|
||||||
|
const productsSalesChannels = product.sales_channels.map(
|
||||||
|
(channel) => channel.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return productsSalesChannels.some((id) => salesChannelIds.includes(id))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a product.
|
* Creates a product.
|
||||||
* @param productObject - the product to create
|
* @param productObject - the product to create
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MedusaError } from "medusa-core-utils"
|
|||||||
|
|
||||||
import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
|
import { PublishableApiKeyRepository } from "../repositories/publishable-api-key"
|
||||||
import { FindConfig, Selector } from "../types/common"
|
import { FindConfig, Selector } from "../types/common"
|
||||||
import { PublishableApiKey } from "../models"
|
import { PublishableApiKey, SalesChannel } from "../models"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
import { buildQuery, isDefined, isString } from "../utils"
|
import { buildQuery, isDefined, isString } from "../utils"
|
||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
CreatePublishableApiKeyInput,
|
CreatePublishableApiKeyInput,
|
||||||
UpdatePublishableApiKeyInput,
|
UpdatePublishableApiKeyInput,
|
||||||
} from "../types/publishable-api-key"
|
} from "../types/publishable-api-key"
|
||||||
|
import { PublishableApiKeySalesChannelRepository } from "../repositories/publishable-api-key-sales-channel"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
|
|
||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
publishableApiKeyRepository: typeof PublishableApiKeyRepository
|
publishableApiKeyRepository: typeof PublishableApiKeyRepository
|
||||||
|
publishableApiKeySalesChannelRepository: typeof PublishableApiKeySalesChannelRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,17 +35,21 @@ class PublishableApiKeyService extends TransactionBaseService {
|
|||||||
|
|
||||||
protected readonly eventBusService_: EventBusService
|
protected readonly eventBusService_: EventBusService
|
||||||
protected readonly publishableApiKeyRepository_: typeof PublishableApiKeyRepository
|
protected readonly publishableApiKeyRepository_: typeof PublishableApiKeyRepository
|
||||||
|
protected readonly publishableApiKeySalesChannelRepository_: typeof PublishableApiKeySalesChannelRepository
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
manager,
|
manager,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
publishableApiKeyRepository,
|
publishableApiKeyRepository,
|
||||||
|
publishableApiKeySalesChannelRepository,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
|
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.eventBusService_ = eventBusService
|
this.eventBusService_ = eventBusService
|
||||||
this.publishableApiKeyRepository_ = publishableApiKeyRepository
|
this.publishableApiKeyRepository_ = publishableApiKeyRepository
|
||||||
|
this.publishableApiKeySalesChannelRepository_ =
|
||||||
|
publishableApiKeySalesChannelRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -247,6 +253,91 @@ class PublishableApiKeyService extends TransactionBaseService {
|
|||||||
const pubKey = await this.retrieve(publishableApiKeyId)
|
const pubKey = await this.retrieve(publishableApiKeyId)
|
||||||
return pubKey.revoked_by === null
|
return pubKey.revoked_by === null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate provided sales channels with the publishable api key.
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId
|
||||||
|
* @param salesChannelIds
|
||||||
|
*/
|
||||||
|
async addSalesChannels(
|
||||||
|
publishableApiKeyId: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<void | never> {
|
||||||
|
return await this.atomicPhase_(async (transactionManager) => {
|
||||||
|
const pubKeySalesChannelRepo = transactionManager.getCustomRepository(
|
||||||
|
this.publishableApiKeySalesChannelRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
await pubKeySalesChannelRepo.addSalesChannels(
|
||||||
|
publishableApiKeyId,
|
||||||
|
salesChannelIds
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove provided sales channels from the publishable api key scope.
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId
|
||||||
|
* @param salesChannelIds
|
||||||
|
*/
|
||||||
|
async removeSalesChannels(
|
||||||
|
publishableApiKeyId: string,
|
||||||
|
salesChannelIds: string[]
|
||||||
|
): Promise<void | never> {
|
||||||
|
return await this.atomicPhase_(async (transactionManager) => {
|
||||||
|
const pubKeySalesChannelRepo = transactionManager.getCustomRepository(
|
||||||
|
this.publishableApiKeySalesChannelRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
await pubKeySalesChannelRepo.removeSalesChannels(
|
||||||
|
publishableApiKeyId,
|
||||||
|
salesChannelIds
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List SalesChannels associated with the PublishableKey
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId - id of the key SalesChannels are listed for
|
||||||
|
*/
|
||||||
|
async listSalesChannels(
|
||||||
|
publishableApiKeyId: string
|
||||||
|
): Promise<SalesChannel[]> {
|
||||||
|
const manager = this.manager_
|
||||||
|
const pubKeySalesChannelRepo = manager.getCustomRepository(
|
||||||
|
this.publishableApiKeySalesChannelRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
return await pubKeySalesChannelRepo.findSalesChannels(publishableApiKeyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a map of resources ids that are withing the key's scope.
|
||||||
|
*
|
||||||
|
* @param publishableApiKeyId
|
||||||
|
*/
|
||||||
|
async getResourceScopes(
|
||||||
|
publishableApiKeyId: string
|
||||||
|
): Promise<{ sales_channel_id: string[] }> {
|
||||||
|
const manager = this.manager_
|
||||||
|
const pubKeySalesChannelRepo = manager.getCustomRepository(
|
||||||
|
this.publishableApiKeySalesChannelRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const salesChannels = await pubKeySalesChannelRepo.find({
|
||||||
|
select: ["sales_channel_id"],
|
||||||
|
where: { publishable_key_id: publishableApiKeyId },
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
sales_channel_id: salesChannels.map(
|
||||||
|
({ sales_channel_id }) => sales_channel_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PublishableApiKeyService
|
export default PublishableApiKeyService
|
||||||
Reference in new issue
Block a user