diff --git a/.changeset/silver-lemons-fly.md b/.changeset/silver-lemons-fly.md new file mode 100644 index 0000000000000..feb899e43e8e5 --- /dev/null +++ b/.changeset/silver-lemons-fly.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/types": patch +"@medusajs/medusa": patch +--- + +feat(core-flows,types,medusa): API to add promotions to campaign diff --git a/integration-tests/modules/__tests__/promotion/admin/campaigns.spec.ts b/integration-tests/modules/__tests__/promotion/admin/campaigns.spec.ts new file mode 100644 index 0000000000000..10ffcf7c5dab9 --- /dev/null +++ b/integration-tests/modules/__tests__/promotion/admin/campaigns.spec.ts @@ -0,0 +1,600 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { CampaignBudgetType, PromotionType } from "@medusajs/utils" +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { createAdminUser } from "../../../../helpers/create-admin-user" + +jest.setTimeout(50000) + +export const campaignData = { + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: new Date("01/01/2023").toISOString(), + ends_at: new Date("01/01/2024").toISOString(), + budget: { + type: CampaignBudgetType.SPEND, + limit: 1000, + used: 0, + }, +} + +export const campaignsData = [ + { + id: "campaign-id-1", + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: new Date("01/01/2023"), + ends_at: new Date("01/01/2024"), + budget: { + type: CampaignBudgetType.SPEND, + limit: 1000, + used: 0, + }, + }, + { + id: "campaign-id-2", + name: "campaign 2", + description: "test description", + currency: "USD", + campaign_identifier: "test-2", + starts_at: new Date("01/01/2023"), + ends_at: new Date("01/01/2024"), + budget: { + type: CampaignBudgetType.USAGE, + limit: 1000, + used: 0, + }, + }, +] + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Admin Campaigns API", () => { + let appContainer + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + appContainer = getContainer() + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders, appContainer) + }) + + const generatePromotionData = () => { + const code = Math.random().toString(36).substring(7) + + return { + code, + type: PromotionType.STANDARD, + is_automatic: true, + application_method: { + target_type: "items", + type: "fixed", + allocation: "each", + value: 100, + max_quantity: 100, + target_rules: [], + }, + rules: [], + } + } + + describe("GET /admin/campaigns", () => { + beforeEach(async () => { + await promotionModuleService.createCampaigns(campaignsData) + }) + + it("should get all campaigns and its count", async () => { + const response = await api.get(`/admin/campaigns`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.campaigns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + type: "spend", + limit: 1000, + used: 0, + raw_limit: { + precision: 20, + value: "1000", + }, + raw_used: { + precision: 20, + value: "0", + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }), + expect.objectContaining({ + id: expect.any(String), + name: "campaign 2", + description: "test description", + currency: "USD", + campaign_identifier: "test-2", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + type: "usage", + limit: 1000, + used: 0, + raw_limit: { + precision: 20, + value: "1000", + }, + raw_used: { + precision: 20, + value: "0", + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }), + ]) + ) + }) + + it("should support search on campaigns", async () => { + const response = await api.get( + `/admin/campaigns?q=ign%202`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaigns).toEqual([ + expect.objectContaining({ + name: "campaign 2", + }), + ]) + }) + + it("should get all campaigns and its count filtered", async () => { + const response = await api.get( + `/admin/campaigns?fields=name,created_at,budget.id`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.campaigns).toEqual( + expect.arrayContaining([ + { + id: expect.any(String), + name: "campaign 1", + created_at: expect.any(String), + budget: { + id: expect.any(String), + }, + }, + { + id: expect.any(String), + name: "campaign 2", + created_at: expect.any(String), + budget: { + id: expect.any(String), + }, + }, + ]) + ) + }) + }) + + describe("GET /admin/campaigns/:id", () => { + it("should throw an error if id does not exist", async () => { + const { response } = await api + .get(`/admin/campaigns/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(response.status).toEqual(404) + expect(response.data.message).toEqual( + "Campaign with id: does-not-exist was not found" + ) + }) + + it("should get the requested campaign", async () => { + const createdCampaign = await promotionModuleService.createCampaigns( + campaignData + ) + + const response = await api.get( + `/admin/campaigns/${createdCampaign.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual({ + id: expect.any(String), + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + type: "spend", + limit: 1000, + raw_limit: { + precision: 20, + value: "1000", + }, + raw_used: { + precision: 20, + value: "0", + }, + used: 0, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }) + }) + + it("should get the requested campaign with filtered fields and relations", async () => { + const createdCampaign = await promotionModuleService.createCampaigns( + campaignData + ) + + const response = await api.get( + `/admin/campaigns/${createdCampaign.id}?fields=name`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual({ + id: expect.any(String), + name: "campaign 1", + }) + }) + }) + + describe("POST /admin/campaigns", () => { + it("should throw an error if required params are not passed", async () => { + const { response } = await api + .post(`/admin/campaigns`, {}, adminHeaders) + .catch((e) => e) + + expect(response.status).toEqual(400) + // expect(response.data.message).toEqual( + // "name must be a string, name should not be empty" + // ) + }) + + it("should create a campaign successfully", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "TEST", + type: "standard", + }) + + const response = await api.post( + `/admin/campaigns?fields=*promotions`, + { + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024").toISOString(), + ends_at: new Date("01/01/2029").toISOString(), + budget: { + limit: 1000, + type: "usage", + }, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test", + campaign_identifier: "test", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: expect.objectContaining({ + limit: 1000, + type: "usage", + }), + }) + ) + }) + + it("should create 3 campaigns in parallel and have the context passed as argument when calling createCampaigns with different transactionId", async () => { + const parallelPromotion = await promotionModuleService.create({ + code: "PARALLEL", + type: "standard", + }) + + const spyCreateCampaigns = jest.spyOn( + promotionModuleService.constructor.prototype, + "createCampaigns" + ) + + const a = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_1", + campaign_identifier: "camp_1", + starts_at: new Date("01/01/2024").toISOString(), + ends_at: new Date("01/02/2024").toISOString(), + budget: { + limit: 1000, + type: "usage", + }, + }, + adminHeaders + ) + } + + const b = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_2", + campaign_identifier: "camp_2", + starts_at: new Date("01/02/2024").toISOString(), + ends_at: new Date("01/03/2029").toISOString(), + budget: { + limit: 500, + type: "usage", + }, + }, + adminHeaders + ) + } + + const c = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_3", + campaign_identifier: "camp_3", + starts_at: new Date("01/03/2024").toISOString(), + ends_at: new Date("01/04/2029").toISOString(), + budget: { + limit: 250, + type: "usage", + }, + }, + { + headers: { + ...adminHeaders.headers, + "x-request-id": "my-custom-request-id", + }, + } + ) + } + + await Promise.all([a(), b(), c()]) + + expect(spyCreateCampaigns).toHaveBeenCalledTimes(3) + expect(spyCreateCampaigns.mock.calls[0][1].__type).toBe( + "MedusaContext" + ) + + const distinctTransactionId = [ + ...new Set( + spyCreateCampaigns.mock.calls.map((call) => call[1].transactionId) + ), + ] + expect(distinctTransactionId).toHaveLength(3) + + const distinctRequestId = [ + ...new Set( + spyCreateCampaigns.mock.calls.map((call) => call[1].requestId) + ), + ] + + expect(distinctRequestId).toHaveLength(3) + expect(distinctRequestId).toContain("my-custom-request-id") + }) + }) + + describe("POST /admin/campaigns/:id", () => { + it("should throw an error if id does not exist", async () => { + const { response } = await api + .post(`/admin/campaigns/does-not-exist`, {}, adminHeaders) + .catch((e) => e) + + expect(response.status).toEqual(404) + expect(response.data.message).toEqual( + `Campaign with id "does-not-exist" not found` + ) + }) + + it("should update a campaign successfully", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "TEST", + type: "standard", + }) + + const createdCampaign = await promotionModuleService.createCampaigns({ + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024").toISOString(), + ends_at: new Date("01/01/2029").toISOString(), + budget: { + limit: 1000, + type: "usage", + used: 10, + }, + }) + + await promotionModuleService.addPromotionsToCampaign({ + id: createdCampaign.id, + promotion_ids: [createdPromotion.id], + }) + + const response = await api.post( + `/admin/campaigns/${createdCampaign.id}?fields=*promotions`, + { + name: "test-2", + campaign_identifier: "test-2", + budget: { + limit: 2000, + }, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test-2", + campaign_identifier: "test-2", + budget: expect.objectContaining({ + limit: 2000, + type: "usage", + used: 10, + }), + promotions: [ + expect.objectContaining({ + id: createdPromotion.id, + }), + ], + }) + ) + }) + }) + + describe("DELETE /admin/campaigns/:id", () => { + it("should delete campaign successfully", async () => { + const [createdCampaign] = + await promotionModuleService.createCampaigns([ + { + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024"), + ends_at: new Date("01/01/2025"), + }, + ]) + + const deleteRes = await api.delete( + `/admin/campaigns/${createdCampaign.id}`, + adminHeaders + ) + + expect(deleteRes.status).toEqual(200) + + const campaigns = await promotionModuleService.listCampaigns({ + id: [createdCampaign.id], + }) + + expect(campaigns.length).toEqual(0) + }) + }) + + describe("POST /admin/campaigns/:id/promotions", () => { + it("should add or remove promotions from campaign", async () => { + const campaign = ( + await api.post(`/admin/campaigns`, campaignData, adminHeaders) + ).data.campaign + + const promotion1 = ( + await api.post( + `/admin/promotions`, + generatePromotionData(), + adminHeaders + ) + ).data.promotion + + const promotion2 = ( + await api.post( + `/admin/promotions`, + generatePromotionData(), + adminHeaders + ) + ).data.promotion + + let response = await api.post( + `/admin/campaigns/${campaign.id}/promotions`, + { add: [promotion1.id, promotion2.id] }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual( + expect.objectContaining({ + id: expect.any(String), + }) + ) + + response = await api.get( + `/admin/promotions?campaign_id=${campaign.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotions).toHaveLength(2) + expect(response.data.promotions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: promotion1.id, + }), + expect.objectContaining({ + id: promotion2.id, + }), + ]) + ) + + await api.post( + `/admin/campaigns/${campaign.id}/promotions`, + { remove: [promotion1.id] }, + adminHeaders + ) + + response = await api.get( + `/admin/promotions?campaign_id=${campaign.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotions).toHaveLength(1) + expect(response.data.promotions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: promotion2.id, + }), + ]) + ) + }) + }) + }) + }, +}) diff --git a/integration-tests/modules/__tests__/promotion/admin/create-campaign.spec.ts b/integration-tests/modules/__tests__/promotion/admin/create-campaign.spec.ts deleted file mode 100644 index a1d5f1a086772..0000000000000 --- a/integration-tests/modules/__tests__/promotion/admin/create-campaign.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { IPromotionModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("POST /admin/campaigns", () => { - let appContainer - let promotionModuleService: IPromotionModuleService - - beforeAll(async () => { - appContainer = getContainer() - promotionModuleService = appContainer.resolve( - ModuleRegistrationName.PROMOTION - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should throw an error if required params are not passed", async () => { - const { response } = await api - .post(`/admin/campaigns`, {}, adminHeaders) - .catch((e) => e) - - expect(response.status).toEqual(400) - // expect(response.data.message).toEqual( - // "name must be a string, name should not be empty" - // ) - }) - - it("should create a campaign successfully", async () => { - const createdPromotion = await promotionModuleService.create({ - code: "TEST", - type: "standard", - }) - - const response = await api.post( - `/admin/campaigns?fields=*promotions`, - { - name: "test", - campaign_identifier: "test", - starts_at: new Date("01/01/2024").toISOString(), - ends_at: new Date("01/01/2029").toISOString(), - promotions: [{ id: createdPromotion.id }], - budget: { - limit: 1000, - type: "usage", - }, - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.campaign).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test", - campaign_identifier: "test", - starts_at: expect.any(String), - ends_at: expect.any(String), - budget: expect.objectContaining({ - limit: 1000, - type: "usage", - }), - promotions: [ - expect.objectContaining({ - id: createdPromotion.id, - }), - ], - }) - ) - }) - - it("should create 3 campaigns in parallel and have the context passed as argument when calling createCampaigns with different transactionId", async () => { - const parallelPromotion = await promotionModuleService.create({ - code: "PARALLEL", - type: "standard", - }) - - const spyCreateCampaigns = jest.spyOn( - promotionModuleService.constructor.prototype, - "createCampaigns" - ) - - const a = async () => { - return await api.post( - `/admin/campaigns`, - { - name: "camp_1", - campaign_identifier: "camp_1", - starts_at: new Date("01/01/2024").toISOString(), - ends_at: new Date("01/02/2024").toISOString(), - promotions: [{ id: parallelPromotion.id }], - budget: { - limit: 1000, - type: "usage", - }, - }, - adminHeaders - ) - } - - const b = async () => { - return await api.post( - `/admin/campaigns`, - { - name: "camp_2", - campaign_identifier: "camp_2", - starts_at: new Date("01/02/2024").toISOString(), - ends_at: new Date("01/03/2029").toISOString(), - promotions: [{ id: parallelPromotion.id }], - budget: { - limit: 500, - type: "usage", - }, - }, - adminHeaders - ) - } - - const c = async () => { - return await api.post( - `/admin/campaigns`, - { - name: "camp_3", - campaign_identifier: "camp_3", - starts_at: new Date("01/03/2024").toISOString(), - ends_at: new Date("01/04/2029").toISOString(), - promotions: [{ id: parallelPromotion.id }], - budget: { - limit: 250, - type: "usage", - }, - }, - { - headers: { - ...adminHeaders.headers, - "x-request-id": "my-custom-request-id", - }, - } - ) - } - - await Promise.all([a(), b(), c()]) - - expect(spyCreateCampaigns).toHaveBeenCalledTimes(3) - expect(spyCreateCampaigns.mock.calls[0][1].__type).toBe("MedusaContext") - - const distinctTransactionId = [ - ...new Set( - spyCreateCampaigns.mock.calls.map((call) => call[1].transactionId) - ), - ] - expect(distinctTransactionId).toHaveLength(3) - - const distinctRequestId = [ - ...new Set( - spyCreateCampaigns.mock.calls.map((call) => call[1].requestId) - ), - ] - - expect(distinctRequestId).toHaveLength(3) - expect(distinctRequestId).toContain("my-custom-request-id") - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/promotion/admin/delete-campaign.spec.ts b/integration-tests/modules/__tests__/promotion/admin/delete-campaign.spec.ts deleted file mode 100644 index bc1a65c7aba6c..0000000000000 --- a/integration-tests/modules/__tests__/promotion/admin/delete-campaign.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IPromotionModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("DELETE /admin/campaigns/:id", () => { - let appContainer - let promotionModuleService: IPromotionModuleService - - beforeAll(async () => { - appContainer = getContainer() - promotionModuleService = appContainer.resolve( - ModuleRegistrationName.PROMOTION - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should delete campaign successfully", async () => { - const [createdCampaign] = await promotionModuleService.createCampaigns([ - { - name: "test", - campaign_identifier: "test", - starts_at: new Date("01/01/2024"), - ends_at: new Date("01/01/2025"), - }, - ]) - - const deleteRes = await api.delete( - `/admin/campaigns/${createdCampaign.id}`, - adminHeaders - ) - - expect(deleteRes.status).toEqual(200) - - const campaigns = await promotionModuleService.listCampaigns({ - id: [createdCampaign.id], - }) - - expect(campaigns.length).toEqual(0) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/promotion/admin/list-campaigns.spec.ts b/integration-tests/modules/__tests__/promotion/admin/list-campaigns.spec.ts deleted file mode 100644 index c1f1eb7c8d213..0000000000000 --- a/integration-tests/modules/__tests__/promotion/admin/list-campaigns.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPromotionModuleService } from "@medusajs/types" -import { CampaignBudgetType } from "@medusajs/utils" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -export const campaignsData = [ - { - id: "campaign-id-1", - name: "campaign 1", - description: "test description", - currency: "USD", - campaign_identifier: "test-1", - starts_at: new Date("01/01/2023"), - ends_at: new Date("01/01/2024"), - budget: { - type: CampaignBudgetType.SPEND, - limit: 1000, - used: 0, - }, - }, - { - id: "campaign-id-2", - name: "campaign 2", - description: "test description", - currency: "USD", - campaign_identifier: "test-2", - starts_at: new Date("01/01/2023"), - ends_at: new Date("01/01/2024"), - budget: { - type: CampaignBudgetType.USAGE, - limit: 1000, - used: 0, - }, - }, -] - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("GET /admin/campaigns", () => { - let appContainer - let promotionModuleService: IPromotionModuleService - - beforeAll(async () => { - appContainer = getContainer() - promotionModuleService = appContainer.resolve( - ModuleRegistrationName.PROMOTION - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - await promotionModuleService.createCampaigns(campaignsData) - }) - - it("should get all campaigns and its count", async () => { - const response = await api.get(`/admin/campaigns`, adminHeaders) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(2) - expect(response.data.campaigns).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: "campaign 1", - description: "test description", - currency: "USD", - campaign_identifier: "test-1", - starts_at: expect.any(String), - ends_at: expect.any(String), - budget: { - id: expect.any(String), - type: "spend", - limit: 1000, - used: 0, - raw_limit: { - precision: 20, - value: "1000", - }, - raw_used: { - precision: 20, - value: "0", - }, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }), - expect.objectContaining({ - id: expect.any(String), - name: "campaign 2", - description: "test description", - currency: "USD", - campaign_identifier: "test-2", - starts_at: expect.any(String), - ends_at: expect.any(String), - budget: { - id: expect.any(String), - type: "usage", - limit: 1000, - used: 0, - raw_limit: { - precision: 20, - value: "1000", - }, - raw_used: { - precision: 20, - value: "0", - }, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }), - ]) - ) - }) - - it("should support search on campaigns", async () => { - const response = await api.get( - `/admin/campaigns?q=ign%202`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.campaigns).toEqual([ - expect.objectContaining({ - name: "campaign 2", - }), - ]) - }) - - it("should get all campaigns and its count filtered", async () => { - const response = await api.get( - `/admin/campaigns?fields=name,created_at,budget.id`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.count).toEqual(2) - expect(response.data.campaigns).toEqual( - expect.arrayContaining([ - { - id: expect.any(String), - name: "campaign 1", - created_at: expect.any(String), - budget: { - id: expect.any(String), - }, - }, - { - id: expect.any(String), - name: "campaign 2", - created_at: expect.any(String), - budget: { - id: expect.any(String), - }, - }, - ]) - ) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/promotion/admin/list-promotions.spec.ts b/integration-tests/modules/__tests__/promotion/admin/list-promotions.spec.ts index 94ca5b6cd1d83..fcb24aa5250c7 100644 --- a/integration-tests/modules/__tests__/promotion/admin/list-promotions.spec.ts +++ b/integration-tests/modules/__tests__/promotion/admin/list-promotions.spec.ts @@ -113,7 +113,7 @@ medusaIntegrationTestRunner({ ]) const response = await api.get( - `/admin/promotions?fields=code,created_at,application_method.id&expand=application_method`, + `/admin/promotions?fields=code,created_at,application_method.id`, adminHeaders ) diff --git a/integration-tests/modules/__tests__/promotion/admin/retrieve-campaign.spec.ts b/integration-tests/modules/__tests__/promotion/admin/retrieve-campaign.spec.ts deleted file mode 100644 index 4b63a0fb6909a..0000000000000 --- a/integration-tests/modules/__tests__/promotion/admin/retrieve-campaign.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IPromotionModuleService } from "@medusajs/types" -import { CampaignBudgetType } from "@medusajs/utils" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -export const campaignData = { - name: "campaign 1", - description: "test description", - currency: "USD", - campaign_identifier: "test-1", - starts_at: new Date("01/01/2023"), - ends_at: new Date("01/01/2024"), - budget: { - type: CampaignBudgetType.SPEND, - limit: 1000, - used: 0, - }, -} - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("GET /admin/campaigns", () => { - let appContainer - let promotionModuleService: IPromotionModuleService - - beforeAll(async () => { - appContainer = getContainer() - promotionModuleService = appContainer.resolve( - ModuleRegistrationName.PROMOTION - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should throw an error if id does not exist", async () => { - const { response } = await api - .get(`/admin/campaigns/does-not-exist`, adminHeaders) - .catch((e) => e) - - expect(response.status).toEqual(404) - expect(response.data.message).toEqual( - "Campaign with id: does-not-exist was not found" - ) - }) - - it("should get the requested campaign", async () => { - const createdCampaign = await promotionModuleService.createCampaigns( - campaignData - ) - - const response = await api.get( - `/admin/campaigns/${createdCampaign.id}`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.campaign).toEqual({ - id: expect.any(String), - name: "campaign 1", - description: "test description", - currency: "USD", - campaign_identifier: "test-1", - starts_at: expect.any(String), - ends_at: expect.any(String), - budget: { - id: expect.any(String), - type: "spend", - limit: 1000, - raw_limit: { - precision: 20, - value: "1000", - }, - raw_used: { - precision: 20, - value: "0", - }, - used: 0, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }) - }) - - it("should get the requested campaign with filtered fields and relations", async () => { - const createdCampaign = await promotionModuleService.createCampaigns( - campaignData - ) - - const response = await api.get( - `/admin/campaigns/${createdCampaign.id}?fields=name`, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.campaign).toEqual({ - id: expect.any(String), - name: "campaign 1", - }) - }) - }) - }, -}) diff --git a/integration-tests/modules/__tests__/promotion/admin/update-campaign.spec.ts b/integration-tests/modules/__tests__/promotion/admin/update-campaign.spec.ts deleted file mode 100644 index 318a88a00acae..0000000000000 --- a/integration-tests/modules/__tests__/promotion/admin/update-campaign.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { IPromotionModuleService } from "@medusajs/types" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAdminUser } from "../../../../helpers/create-admin-user" -import { medusaIntegrationTestRunner } from "medusa-test-utils" - -jest.setTimeout(50000) - -const env = { MEDUSA_FF_MEDUSA_V2: true } -const adminHeaders = { - headers: { "x-medusa-access-token": "test_token" }, -} - -medusaIntegrationTestRunner({ - env, - testSuite: ({ dbConnection, getContainer, api }) => { - describe("POST /admin/campaigns/:id", () => { - let appContainer - let promotionModuleService: IPromotionModuleService - - beforeAll(async () => { - appContainer = getContainer() - promotionModuleService = appContainer.resolve( - ModuleRegistrationName.PROMOTION - ) - }) - - beforeEach(async () => { - await createAdminUser(dbConnection, adminHeaders, appContainer) - }) - - it("should throw an error if id does not exist", async () => { - const { response } = await api - .post(`/admin/campaigns/does-not-exist`, {}, adminHeaders) - .catch((e) => e) - - expect(response.status).toEqual(404) - expect(response.data.message).toEqual( - `Campaign with id "does-not-exist" not found` - ) - }) - - it("should update a campaign successfully", async () => { - const createdPromotion = await promotionModuleService.create({ - code: "TEST", - type: "standard", - }) - - const createdPromotion2 = await promotionModuleService.create({ - code: "TEST_2", - type: "standard", - }) - - const createdCampaign = await promotionModuleService.createCampaigns({ - name: "test", - campaign_identifier: "test", - starts_at: new Date("01/01/2024").toISOString(), - ends_at: new Date("01/01/2029").toISOString(), - promotions: [{ id: createdPromotion.id }], - budget: { - limit: 1000, - type: "usage", - used: 10, - }, - }) - - const response = await api.post( - `/admin/campaigns/${createdCampaign.id}?fields=*promotions`, - { - name: "test-2", - campaign_identifier: "test-2", - budget: { - limit: 2000, - }, - promotions: [{ id: createdPromotion2.id }], - }, - adminHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.campaign).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test-2", - campaign_identifier: "test-2", - budget: expect.objectContaining({ - limit: 2000, - type: "usage", - used: 10, - }), - promotions: [ - expect.objectContaining({ - id: createdPromotion2.id, - }), - ], - }) - ) - }) - }) - }, -}) diff --git a/packages/core/core-flows/src/promotion/steps/add-campaign-promotions.ts b/packages/core/core-flows/src/promotion/steps/add-campaign-promotions.ts new file mode 100644 index 0000000000000..a175b5b15dd24 --- /dev/null +++ b/packages/core/core-flows/src/promotion/steps/add-campaign-promotions.ts @@ -0,0 +1,41 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService, LinkWorkflowInput } from "@medusajs/types" +import { StepResponse, WorkflowData, createStep } from "@medusajs/workflows-sdk" + +export const addCampaignPromotionsStepId = "add-campaign-promotions" +export const addCampaignPromotionsStep = createStep( + addCampaignPromotionsStepId, + async (input: WorkflowData, { container }) => { + const { id: campaignId, add: promotionIdsToAdd = [] } = input + + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + if (promotionIdsToAdd.length) { + await promotionModule.addPromotionsToCampaign({ + id: campaignId, + promotion_ids: promotionIdsToAdd, + }) + } + + return new StepResponse(null, input) + }, + async (data, { container }) => { + if (!data) { + return + } + + const { id: campaignId, add: promotionIdsToRemove = [] } = data + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + if (promotionIdsToRemove.length) { + await promotionModule.removePromotionsFromCampaign({ + id: campaignId, + promotion_ids: promotionIdsToRemove, + }) + } + } +) diff --git a/packages/core/core-flows/src/promotion/steps/index.ts b/packages/core/core-flows/src/promotion/steps/index.ts index f3b89f7a68a4e..632fd1c808ecb 100644 --- a/packages/core/core-flows/src/promotion/steps/index.ts +++ b/packages/core/core-flows/src/promotion/steps/index.ts @@ -1,8 +1,10 @@ +export * from "./add-campaign-promotions" export * from "./add-rules-to-promotions" export * from "./create-campaigns" export * from "./create-promotions" export * from "./delete-campaigns" export * from "./delete-promotions" +export * from "./remove-campaign-promotions" export * from "./remove-rules-from-promotions" export * from "./update-campaigns" export * from "./update-promotion-rules" diff --git a/packages/core/core-flows/src/promotion/steps/remove-campaign-promotions.ts b/packages/core/core-flows/src/promotion/steps/remove-campaign-promotions.ts new file mode 100644 index 0000000000000..fdcf242b1a609 --- /dev/null +++ b/packages/core/core-flows/src/promotion/steps/remove-campaign-promotions.ts @@ -0,0 +1,40 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService, LinkWorkflowInput } from "@medusajs/types" +import { StepResponse, WorkflowData, createStep } from "@medusajs/workflows-sdk" + +export const removeCampaignPromotionsStepId = "remove-campaign-promotions" +export const removeCampaignPromotionsStep = createStep( + removeCampaignPromotionsStepId, + async (input: WorkflowData, { container }) => { + const { id: campaignId, remove: promotionIdsToRemove = [] } = input + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + if (promotionIdsToRemove.length) { + await promotionModule.removePromotionsFromCampaign({ + id: campaignId, + promotion_ids: promotionIdsToRemove, + }) + } + + return new StepResponse(null, input) + }, + async (data, { container }) => { + if (!data) { + return + } + + const { id: campaignId, remove: promotionIdsToAdd = [] } = data + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + if (promotionIdsToAdd.length) { + await promotionModule.addPromotionsToCampaign({ + id: campaignId, + promotion_ids: promotionIdsToAdd, + }) + } + } +) diff --git a/packages/core/core-flows/src/promotion/workflows/add-or-remove-campaign-promotions.ts b/packages/core/core-flows/src/promotion/workflows/add-or-remove-campaign-promotions.ts new file mode 100644 index 0000000000000..adb34649c6395 --- /dev/null +++ b/packages/core/core-flows/src/promotion/workflows/add-or-remove-campaign-promotions.ts @@ -0,0 +1,16 @@ +import { LinkWorkflowInput } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { + addCampaignPromotionsStep, + removeCampaignPromotionsStep, +} from "../steps" + +export const addOrRemoveCampaignPromotionsWorkflowId = + "add-or-remove-campaign-promotions" +export const addOrRemoveCampaignPromotionsWorkflow = createWorkflow( + addOrRemoveCampaignPromotionsWorkflowId, + (input: WorkflowData): WorkflowData => { + addCampaignPromotionsStep(input) + removeCampaignPromotionsStep(input) + } +) diff --git a/packages/core/core-flows/src/promotion/workflows/index.ts b/packages/core/core-flows/src/promotion/workflows/index.ts index e90129f58cc2b..604b82679b24d 100644 --- a/packages/core/core-flows/src/promotion/workflows/index.ts +++ b/packages/core/core-flows/src/promotion/workflows/index.ts @@ -1,10 +1,11 @@ +export * from "./add-or-remove-campaign-promotions" export * from "./batch-promotion-rules" export * from "./create-campaigns" +export * from "./create-promotion-rules" export * from "./create-promotions" export * from "./delete-campaigns" +export * from "./delete-promotion-rules" export * from "./delete-promotions" export * from "./update-campaigns" export * from "./update-promotion-rules" -export * from "./delete-promotion-rules" -export * from "./create-promotion-rules" export * from "./update-promotions" diff --git a/packages/core/types/src/promotion/common/campaign.ts b/packages/core/types/src/promotion/common/campaign.ts index fa502c1b27945..9af0dec0d23a3 100644 --- a/packages/core/types/src/promotion/common/campaign.ts +++ b/packages/core/types/src/promotion/common/campaign.ts @@ -1,5 +1,6 @@ import { BaseFilterable } from "../../dal" import { CampaignBudgetDTO } from "./campaign-budget" +import { PromotionDTO } from "./promotion" /** * The campaign details. @@ -44,6 +45,11 @@ export interface CampaignDTO { * The associated campaign budget. */ budget?: CampaignBudgetDTO + + /** + * The associated promotions. + */ + promotions?: PromotionDTO[] } /** diff --git a/packages/core/types/src/promotion/common/promotion.ts b/packages/core/types/src/promotion/common/promotion.ts index 73bd193ee8336..e3d9723c60b50 100644 --- a/packages/core/types/src/promotion/common/promotion.ts +++ b/packages/core/types/src/promotion/common/promotion.ts @@ -133,7 +133,7 @@ export interface UpdatePromotionDTO { /** * The associated campaign's ID. */ - campaign_id?: string + campaign_id?: string | null } /** diff --git a/packages/core/types/src/promotion/mutations.ts b/packages/core/types/src/promotion/mutations.ts index ec9462ddf86b1..6370a87e4d409 100644 --- a/packages/core/types/src/promotion/mutations.ts +++ b/packages/core/types/src/promotion/mutations.ts @@ -1,4 +1,4 @@ -import { CampaignBudgetTypeValues, PromotionDTO } from "./common" +import { CampaignBudgetTypeValues } from "./common" /** * The campaign budget to be created. @@ -83,11 +83,6 @@ export interface CreateCampaignDTO { * The associated campaign budget. */ budget?: CreateCampaignBudgetDTO - - /** - * The promotions of the campaign. - */ - promotions?: Pick[] } /** @@ -133,9 +128,28 @@ export interface UpdateCampaignDTO { * The budget of the campaign. */ budget?: Omit +} + +export interface AddPromotionsToCampaignDTO { + /** + * The ID of the campaign. + */ + id: string + + /** + * Ids of promotions to add + */ + promotion_ids: string[] +} + +export interface RemovePromotionsFromCampaignDTO { + /** + * The ID of the campaign. + */ + id: string /** - * The promotions of the campaign. + * Ids of promotions to add */ - promotions?: Pick[] + promotion_ids: string[] } diff --git a/packages/core/types/src/promotion/service.ts b/packages/core/types/src/promotion/service.ts index 37855478ff248..d92e92a2d99dc 100644 --- a/packages/core/types/src/promotion/service.ts +++ b/packages/core/types/src/promotion/service.ts @@ -16,7 +16,12 @@ import { UpdatePromotionDTO, UpdatePromotionRuleDTO, } from "./common" -import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations" +import { + AddPromotionsToCampaignDTO, + CreateCampaignDTO, + RemovePromotionsFromCampaignDTO, + UpdateCampaignDTO, +} from "./mutations" /** * The main service interface for the Promotion Module. @@ -967,4 +972,14 @@ export interface IPromotionModuleService extends IModuleService { config?: RestoreReturn, sharedContext?: Context ): Promise | void> + + addPromotionsToCampaign( + data: AddPromotionsToCampaignDTO, + sharedContext?: Context + ): Promise<{ ids: string[] }> + + removePromotionsFromCampaign( + data: RemovePromotionsFromCampaignDTO, + sharedContext?: Context + ): Promise<{ ids: string[] }> } diff --git a/packages/medusa/src/api-v2/admin/campaigns/[id]/promotions/route.ts b/packages/medusa/src/api-v2/admin/campaigns/[id]/promotions/route.ts new file mode 100644 index 0000000000000..7fc71e316565a --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/[id]/promotions/route.ts @@ -0,0 +1,34 @@ +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" + +import { addOrRemoveCampaignPromotionsWorkflow } from "@medusajs/core-flows" +import { LinkMethodRequest } from "@medusajs/types/src" +import { refetchCampaign } from "../../helpers" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const { add, remove } = req.validatedBody + const { errors } = await addOrRemoveCampaignPromotionsWorkflow(req.scope).run( + { + input: { id, add, remove }, + throwOnError: false, + } + ) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const campaign = await refetchCampaign( + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ campaign }) +} diff --git a/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts b/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts index 6217dfd7b4b66..a83bfe4d37778 100644 --- a/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts @@ -1,14 +1,15 @@ -import * as QueryConfig from "./query-config" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { authenticate } from "../../../utils/authenticate-middleware" +import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformQuery } from "../../utils/validate-query" +import { createLinkBody } from "../../utils/validators" +import * as QueryConfig from "./query-config" import { AdminCreateCampaign, AdminGetCampaignParams, AdminGetCampaignsParams, AdminUpdateCampaign, } from "./validators" -import { validateAndTransformBody } from "../../utils/validate-body" export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -57,4 +58,15 @@ export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/campaigns/:id/promotions", + middlewares: [ + validateAndTransformBody(createLinkBody()), + validateAndTransformQuery( + AdminGetCampaignParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts index e52f4b03db0b8..d0bb738b7360c 100644 --- a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts @@ -1,7 +1,9 @@ -import * as QueryConfig from "./query-config" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" import { authenticate } from "../../../utils/authenticate-middleware" +import { validateAndTransformBody } from "../../utils/validate-body" import { validateAndTransformQuery } from "../../utils/validate-query" +import { createBatchBody } from "../../utils/validators" +import * as QueryConfig from "./query-config" import { AdminCreatePromotion, AdminCreatePromotionRule, @@ -13,8 +15,6 @@ import { AdminUpdatePromotion, AdminUpdatePromotionRule, } from "./validators" -import { validateAndTransformBody } from "../../utils/validate-body" -import { createBatchBody } from "../../utils/validators" export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ { diff --git a/packages/medusa/src/api-v2/admin/promotions/route.ts b/packages/medusa/src/api-v2/admin/promotions/route.ts index 17866457f22a2..e9731503a5335 100644 --- a/packages/medusa/src/api-v2/admin/promotions/route.ts +++ b/packages/medusa/src/api-v2/admin/promotions/route.ts @@ -1,17 +1,17 @@ import { createPromotionsWorkflow } from "@medusajs/core-flows" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "../../../types/routing" import { ContainerRegistrationKeys, remoteQueryObjectFromString, } from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { refetchPromotion } from "./helpers" import { AdminCreatePromotionType, AdminGetPromotionsParamsType, } from "./validators" -import { refetchPromotion } from "./helpers" export const GET = async ( req: AuthenticatedMedusaRequest, diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts index 17adcbc97a25d..37effa3e0aa43 100644 --- a/packages/medusa/src/api-v2/admin/promotions/validators.ts +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -24,17 +24,20 @@ export type AdminGetPromotionsParamsType = z.infer< export const AdminGetPromotionsParams = createFindParams({ limit: 50, offset: 0, -}).merge( - z.object({ - q: z.string().optional(), - code: z.union([z.string(), z.array(z.string())]).optional(), - created_at: createOperatorMap().optional(), - updated_at: createOperatorMap().optional(), - deleted_at: createOperatorMap().optional(), - $and: z.lazy(() => AdminGetPromotionsParams.array()).optional(), - $or: z.lazy(() => AdminGetPromotionsParams.array()).optional(), - }) -) +}) + .merge( + z.object({ + q: z.string().optional(), + code: z.union([z.string(), z.array(z.string())]).optional(), + campaign_id: z.union([z.string(), z.array(z.string())]).optional(), + created_at: createOperatorMap().optional(), + updated_at: createOperatorMap().optional(), + deleted_at: createOperatorMap().optional(), + $and: z.lazy(() => AdminGetPromotionsParams.array()).optional(), + $or: z.lazy(() => AdminGetPromotionsParams.array()).optional(), + }) + ) + .strict() export type AdminGetPromotionRuleParamsType = z.infer< typeof AdminGetPromotionRuleParams @@ -152,7 +155,6 @@ export const AdminCreateCampaign = z.object({ budget: CreateCampaignBudget.optional(), starts_at: z.coerce.date().optional(), ends_at: z.coerce.date().optional(), - promotions: z.array(z.object({ id: z.string() })).optional(), }) export type AdminCreatePromotionType = z.infer diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts index d6765daab5eb8..9905e642ccb98 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts @@ -152,43 +152,6 @@ moduleIntegrationTestRunner({ }) ) }) - - it("should create a basic campaign with promotions successfully", async () => { - await createPromotions(MikroOrmWrapper.forkManager()) - - const startsAt = new Date("01/01/2024") - const endsAt = new Date("01/01/2025") - const [createdCampaign] = await service.createCampaigns([ - { - name: "test", - campaign_identifier: "test", - starts_at: startsAt, - ends_at: endsAt, - promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }], - }, - ]) - - const campaign = await service.retrieveCampaign(createdCampaign.id, { - relations: ["promotions"], - }) - - expect(campaign).toEqual( - expect.objectContaining({ - name: "test", - campaign_identifier: "test", - starts_at: startsAt, - ends_at: endsAt, - promotions: [ - expect.objectContaining({ - id: "promotion-id-1", - }), - expect.objectContaining({ - id: "promotion-id-2", - }), - ], - }) - ) - }) }) describe("updateCampaigns", () => { @@ -251,66 +214,6 @@ moduleIntegrationTestRunner({ }) ) }) - - it("should update promotions of a campaign successfully", async () => { - await createCampaigns(MikroOrmWrapper.forkManager()) - await createPromotions(MikroOrmWrapper.forkManager()) - - const [updatedCampaign] = await service.updateCampaigns([ - { - id: "campaign-id-1", - description: "test description 1", - currency: "EUR", - campaign_identifier: "new", - starts_at: new Date("01/01/2024"), - ends_at: new Date("01/01/2025"), - promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }], - }, - ]) - - expect(updatedCampaign).toEqual( - expect.objectContaining({ - description: "test description 1", - currency: "EUR", - campaign_identifier: "new", - starts_at: new Date("01/01/2024"), - ends_at: new Date("01/01/2025"), - promotions: [ - expect.objectContaining({ - id: "promotion-id-1", - }), - expect.objectContaining({ - id: "promotion-id-2", - }), - ], - }) - ) - }) - - it("should remove promotions of the campaign successfully", async () => { - await createCampaigns(MikroOrmWrapper.forkManager()) - await createPromotions(MikroOrmWrapper.forkManager()) - - await service.updateCampaigns({ - id: "campaign-id-1", - promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }], - }) - - const updatedCampaign = await service.updateCampaigns({ - id: "campaign-id-1", - promotions: [{ id: "promotion-id-1" }], - }) - - expect(updatedCampaign).toEqual( - expect.objectContaining({ - promotions: [ - expect.objectContaining({ - id: "promotion-id-1", - }), - ], - }) - ) - }) }) describe("retrieveCampaign", () => { @@ -438,6 +341,77 @@ moduleIntegrationTestRunner({ expect(campaigns).toHaveLength(1) }) }) + + describe("addPromotionsToCampaign", () => { + beforeEach(async () => { + await createCampaigns(MikroOrmWrapper.forkManager()) + await createPromotions(MikroOrmWrapper.forkManager()) + + await service.addPromotionsToCampaign({ + id, + promotion_ids: ["promotion-id-1"], + }) + }) + + const id = "campaign-id-1" + + it("should add promotions to a campaign", async () => { + await service.addPromotionsToCampaign({ + id, + promotion_ids: ["promotion-id-2"], + }) + + const campaign = await service.retrieveCampaign(id, { + relations: ["promotions"], + }) + + expect(campaign.promotions).toHaveLength(2) + expect(campaign).toEqual( + expect.objectContaining({ + id, + promotions: expect.arrayContaining([ + expect.objectContaining({ id: "promotion-id-1" }), + expect.objectContaining({ id: "promotion-id-2" }), + ]), + }) + ) + }) + }) + + describe("removePromotionsFromCampaign", () => { + beforeEach(async () => { + await createCampaigns(MikroOrmWrapper.forkManager()) + await createPromotions(MikroOrmWrapper.forkManager()) + + await service.addPromotionsToCampaign({ + id, + promotion_ids: ["promotion-id-1", "promotion-id-2"], + }) + }) + + const id = "campaign-id-1" + + it("should remove promotions to a campaign", async () => { + await service.removePromotionsFromCampaign({ + id, + promotion_ids: ["promotion-id-1"], + }) + + const campaign = await service.retrieveCampaign(id, { + relations: ["promotions"], + }) + + expect(campaign.promotions).toHaveLength(1) + expect(campaign).toEqual( + expect.objectContaining({ + id, + promotions: expect.arrayContaining([ + expect.objectContaining({ id: "promotion-id-2" }), + ]), + }) + ) + }) + }) }) }, }) diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index 0bd32d73b0d24..9b9a951141270 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -539,7 +539,7 @@ moduleIntegrationTestRunner({ }, ]) - await service.updateCampaigns({ + const updated = await service.updateCampaigns({ id: "campaign-id-2", budget: { used: 1000 }, }) diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 03a3386741d84..a59c95eb1216d 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -6,9 +6,9 @@ import { CampaignBudgetType, PromotionType, } from "@medusajs/utils" +import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" import { createPromotions } from "../../../__fixtures__/promotion" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" jest.setTimeout(30000) @@ -918,6 +918,7 @@ moduleIntegrationTestRunner({ { id: "promotion-id-1", code: "PROMOTION_1", + campaign_id: null, campaign: null, is_automatic: false, type: "standard", @@ -929,6 +930,7 @@ moduleIntegrationTestRunner({ { id: "promotion-id-2", code: "PROMOTION_2", + campaign_id: null, campaign: null, is_automatic: false, type: "standard", diff --git a/packages/modules/promotion/src/models/campaign.ts b/packages/modules/promotion/src/models/campaign.ts index 6874398035729..bafa194a4a8f3 100644 --- a/packages/modules/promotion/src/models/campaign.ts +++ b/packages/modules/promotion/src/models/campaign.ts @@ -70,9 +70,7 @@ export default class Campaign { }) budget: CampaignBudget | null = null - @OneToMany(() => Promotion, (promotion) => promotion.campaign, { - orphanRemoval: true, - }) + @OneToMany(() => Promotion, (promotion) => promotion.campaign) promotions = new Collection(this) @Property({ diff --git a/packages/modules/promotion/src/models/promotion.ts b/packages/modules/promotion/src/models/promotion.ts index bee0fe91c34ae..2f53cae22ac6e 100644 --- a/packages/modules/promotion/src/models/promotion.ts +++ b/packages/modules/promotion/src/models/promotion.ts @@ -45,13 +45,17 @@ export default class Promotion { }) code: string - @Searchable() @ManyToOne(() => Campaign, { + columnType: "text", fieldName: "campaign_id", nullable: true, - cascade: ["soft-remove"] as any, + mapToPk: true, + onDelete: "set null", }) - campaign: Campaign | null = null + campaign_id: string | null = null + + @ManyToOne(() => Campaign, { persist: false }) + campaign: Campaign | null @Property({ columnType: "boolean", default: false }) is_automatic: boolean = false diff --git a/packages/modules/promotion/src/repositories/campaign.ts b/packages/modules/promotion/src/repositories/campaign.ts deleted file mode 100644 index afec080397274..0000000000000 --- a/packages/modules/promotion/src/repositories/campaign.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Context } from "@medusajs/types" -import { DALUtils } from "@medusajs/utils" -import { SqlEntityManager } from "@mikro-orm/postgresql" -import { Campaign, Promotion } from "@models" -import { CreateCampaignDTO, UpdateCampaignDTO } from "@types" - -export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory( - Campaign -) { - async create( - data: CreateCampaignDTO[], - context: Context = {} - ): Promise { - const manager = this.getActiveManager(context) - const promotionIdsToUpsert: string[] = [] - const campaignIdentifierPromotionsMap = new Map() - - data.forEach((campaignData) => { - const campaignPromotionIds = - campaignData.promotions?.map((p) => p.id) || [] - - promotionIdsToUpsert.push(...campaignPromotionIds) - - campaignIdentifierPromotionsMap.set( - campaignData.campaign_identifier, - campaignPromotionIds - ) - - delete campaignData.promotions - }) - - const existingPromotions = await manager.find(Promotion, { - id: promotionIdsToUpsert, - }) - - const existingPromotionsMap = new Map( - existingPromotions.map((promotion) => [promotion.id, promotion]) - ) - - const createdCampaigns = await super.create(data, context) - - for (const createdCampaign of createdCampaigns) { - const campaignPromotionIds = - campaignIdentifierPromotionsMap.get( - createdCampaign.campaign_identifier - ) || [] - - for (const campaignPromotionId of campaignPromotionIds) { - const promotion = existingPromotionsMap.get(campaignPromotionId) - - if (!promotion) { - continue - } - - createdCampaign.promotions.add(promotion) - } - } - - return createdCampaigns - } - - async update( - data: { entity: Campaign; update: UpdateCampaignDTO }[], - context: Context = {} - ): Promise { - const manager = this.getActiveManager(context) - const promotionIdsToUpsert: string[] = [] - const campaignIds: string[] = [] - const campaignPromotionIdsMap = new Map() - - data.forEach(({ update: campaignData }) => { - const campaignPromotionIds = campaignData.promotions?.map((p) => p.id) - - campaignIds.push(campaignData.id) - - if (campaignPromotionIds) { - promotionIdsToUpsert.push(...campaignPromotionIds) - campaignPromotionIdsMap.set(campaignData.id, campaignPromotionIds) - } - - delete campaignData.promotions - }) - - const existingCampaigns = await manager.find( - Campaign, - { id: campaignIds }, - { populate: ["promotions"] } - ) - - const promotionIds = existingCampaigns - .map((campaign) => campaign.promotions?.map((p) => p.id)) - .flat(1) - .concat(promotionIdsToUpsert) - - const existingPromotions = await manager.find(Promotion, { - id: promotionIds, - }) - - const existingCampaignsMap = new Map( - existingCampaigns.map((campaign) => [campaign.id, campaign]) - ) - - const existingPromotionsMap = new Map( - existingPromotions.map((promotion) => [promotion.id, promotion]) - ) - - const updatedCampaigns = await super.update(data, context) - - for (const updatedCampaign of updatedCampaigns) { - const upsertPromotionIds = campaignPromotionIdsMap.get(updatedCampaign.id) - - if (!upsertPromotionIds) { - continue - } - - const existingPromotionIds = ( - existingCampaignsMap.get(updatedCampaign.id)?.promotions || [] - ).map((p) => p.id) - - for (const existingPromotionId of existingPromotionIds) { - const promotion = existingPromotionsMap.get(existingPromotionId) - - if (!promotion) { - continue - } - - if (!upsertPromotionIds.includes(existingPromotionId)) { - updatedCampaign.promotions.remove(promotion) - } - } - - for (const promotionIdToAdd of upsertPromotionIds) { - const promotion = existingPromotionsMap.get(promotionIdToAdd) - - if (!promotion) { - continue - } - - if (existingPromotionIds.includes(promotionIdToAdd)) { - continue - } else { - updatedCampaign.promotions.add(promotion) - } - } - } - - return updatedCampaigns - } -} diff --git a/packages/modules/promotion/src/repositories/index.ts b/packages/modules/promotion/src/repositories/index.ts index db193bb79fe2d..147c9cc259fa4 100644 --- a/packages/modules/promotion/src/repositories/index.ts +++ b/packages/modules/promotion/src/repositories/index.ts @@ -1,2 +1 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" -export { CampaignRepository } from "./campaign" diff --git a/packages/modules/promotion/src/services/promotion-module.ts b/packages/modules/promotion/src/services/promotion-module.ts index a4c628ed2138d..bf6942eed98ce 100644 --- a/packages/modules/promotion/src/services/promotion-module.ts +++ b/packages/modules/promotion/src/services/promotion-module.ts @@ -528,7 +528,7 @@ export default class PromotionModuleService< promotionsData.push({ ...promotionData, - campaign: campaignId, + campaign_id: campaignId, }) } @@ -536,6 +536,7 @@ export default class PromotionModuleService< promotionsData, sharedContext ) + const promotionsToAdd: PromotionTypes.AddPromotionsToCampaignDTO[] = [] for (const promotion of createdPromotions) { const applMethodData = promotionCodeApplicationMethodDataMap.get( @@ -617,8 +618,25 @@ export default class PromotionModuleService< sharedContext ) - if (campaignsData.length) { - await this.createCampaigns(campaignsData, sharedContext) + const createdCampaigns = await this.createCampaigns( + campaignsData, + sharedContext + ) + + for (const campaignData of campaignsData) { + const promotions = campaignData.promotions + const campaign = createdCampaigns.find( + (c) => c.campaign_identifier === campaignData.campaign_identifier + ) + + if (!campaign || !promotions || !promotions.length) { + continue + } + + await this.addPromotionsToCampaign( + { id: campaign.id, promotion_ids: promotions.map((p) => p.id) }, + sharedContext + ) } for (const applicationMethod of createdApplicationMethods) { @@ -704,7 +722,7 @@ export default class PromotionModuleService< ...promotionData } of data) { if (campaignId) { - promotionsData.push({ ...promotionData, campaign: campaignId }) + promotionsData.push({ ...promotionData, campaign_id: campaignId }) } else { promotionsData.push(promotionData) } @@ -1110,19 +1128,7 @@ export default class PromotionModuleService< >() for (const createCampaignData of data) { - const { - budget: campaignBudgetData, - promotions, - ...campaignData - } = createCampaignData - - const promotionsToAdd = promotions - ? await this.list( - { id: promotions.map((p) => p.id) }, - { take: null }, - sharedContext - ) - : [] + const { budget: campaignBudgetData, ...campaignData } = createCampaignData if (campaignBudgetData) { campaignIdentifierBudgetMap.set( @@ -1133,7 +1139,6 @@ export default class PromotionModuleService< campaignsData.push({ ...campaignData, - promotions: promotionsToAdd, }) } @@ -1244,4 +1249,104 @@ export default class PromotionModuleService< return updatedCampaigns } + + @InjectManager("baseRepository_") + async addPromotionsToCampaign( + data: PromotionTypes.AddPromotionsToCampaignDTO, + sharedContext?: Context + ): Promise<{ ids: string[] }> { + const ids = await this.addPromotionsToCampaign_(data, sharedContext) + + return { ids } + } + + // TODO: + // - introduce currency_code to promotion + // - allow promotions to be queried by currency code + // - when the above is present, validate adding promotion to campaign based on currency code + @InjectTransactionManager("baseRepository_") + protected async addPromotionsToCampaign_( + data: PromotionTypes.AddPromotionsToCampaignDTO, + @MedusaContext() sharedContext: Context = {} + ) { + const { id, promotion_ids: promotionIds = [] } = data + + const campaign = await this.campaignService_.retrieve(id, {}, sharedContext) + const promotionsToAdd = await this.promotionService_.list( + { id: promotionIds, campaign_id: null }, + { take: null }, + sharedContext + ) + + const diff = arrayDifference( + promotionsToAdd.map((p) => p.id), + promotionIds + ) + + if (diff.length > 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Cannot add promotions (${diff.join( + "," + )}) to campaign. These promotions are either already part of a campaign or not found.` + ) + } + + await this.promotionService_.update( + promotionsToAdd.map((promotion) => ({ + id: promotion.id, + campaign_id: campaign.id, + })), + sharedContext + ) + + return promotionsToAdd.map((promo) => promo.id) + } + + @InjectManager("baseRepository_") + async removePromotionsFromCampaign( + data: PromotionTypes.AddPromotionsToCampaignDTO, + sharedContext?: Context + ): Promise<{ ids: string[] }> { + const ids = await this.removePromotionsFromCampaign_(data, sharedContext) + + return { ids } + } + + @InjectTransactionManager("baseRepository_") + protected async removePromotionsFromCampaign_( + data: PromotionTypes.AddPromotionsToCampaignDTO, + @MedusaContext() sharedContext: Context = {} + ) { + const { id, promotion_ids: promotionIds = [] } = data + + await this.campaignService_.retrieve(id, {}, sharedContext) + const promotionsToRemove = await this.promotionService_.list( + { id: promotionIds }, + { take: null }, + sharedContext + ) + + const diff = arrayDifference( + promotionsToRemove.map((p) => p.id), + promotionIds + ) + + if (diff.length > 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Promotions with ids (${diff.join(",")}) not found.` + ) + } + + await this.promotionService_.update( + promotionsToRemove.map((promotion) => ({ + id: promotion.id, + campaign_id: null, + })), + sharedContext + ) + + return promotionsToRemove.map((promo) => promo.id) + } } diff --git a/packages/modules/promotion/src/types/promotion.ts b/packages/modules/promotion/src/types/promotion.ts index ae63f9f36b434..c1c6fffd6ba24 100644 --- a/packages/modules/promotion/src/types/promotion.ts +++ b/packages/modules/promotion/src/types/promotion.ts @@ -4,7 +4,7 @@ export interface CreatePromotionDTO { code: string type: PromotionTypeValues is_automatic?: boolean - campaign?: string + campaign_id?: string } export interface UpdatePromotionDTO { @@ -12,5 +12,5 @@ export interface UpdatePromotionDTO { code?: string type?: PromotionTypeValues is_automatic?: boolean - campaign?: string + campaign_id?: string } diff --git a/www/apps/api-reference/app/_mdx/admin.mdx b/www/apps/api-reference/app/_mdx/v1/admin.mdx similarity index 98% rename from www/apps/api-reference/app/_mdx/admin.mdx rename to www/apps/api-reference/app/_mdx/v1/admin.mdx index f1cafbd04e01e..958a9d43b0322 100644 --- a/www/apps/api-reference/app/_mdx/admin.mdx +++ b/www/apps/api-reference/app/_mdx/v1/admin.mdx @@ -1,12 +1,9 @@ import { Feedback, CodeTabs, CodeTab } from "docs-ui" import SectionContainer from "@/components/Section/Container" import formatReportLink from "@/utils/format-report-link" -import VersionNote from "@/components/VersionNote" - - This API reference includes Medusa's Admin APIs, which are REST APIs exposed by the Medusa backend. They are typically used to perform admin functionalities or create an admin dashboard to access and manipulate your commerce store's data. All API Routes are prefixed with `/admin`. So, during development, the API Routes will be available under the path `http://localhost:9000/admin`. For production, replace `http://localhost:9000` with your Medusa backend URL. @@ -135,17 +132,6 @@ You can also pass it to client libraries: - - ### JWT Token Use a JWT token to send authenticated requests. Authentication state is managed by the client, which is ideal for Jamstack applications and mobile applications. diff --git a/www/apps/api-reference/app/_mdx/client-libraries.mdx b/www/apps/api-reference/app/_mdx/v1/client-libraries.mdx similarity index 100% rename from www/apps/api-reference/app/_mdx/client-libraries.mdx rename to www/apps/api-reference/app/_mdx/v1/client-libraries.mdx diff --git a/www/apps/api-reference/app/_mdx/store.mdx b/www/apps/api-reference/app/_mdx/v1/store.mdx similarity index 99% rename from www/apps/api-reference/app/_mdx/store.mdx rename to www/apps/api-reference/app/_mdx/v1/store.mdx index 862f45dd836c3..b5ef864b71e6d 100644 --- a/www/apps/api-reference/app/_mdx/store.mdx +++ b/www/apps/api-reference/app/_mdx/v1/store.mdx @@ -2,12 +2,9 @@ import { Feedback, CodeTabs, CodeTab } from "docs-ui" import SectionContainer from "@/components/Section/Container" import formatReportLink from "@/utils/format-report-link" -import VersionNote from "@/components/VersionNote" - - This API reference includes Medusa's Store APIs, which are REST APIs exposed by the Medusa backend. They are typically used to create a storefront for your commerce store, such as a webshop or a commerce mobile app. All API Routes are prefixed with `/store`. So, during development, the API Routes will be available under the path `http://localhost:9000/store`. For production, replace `http://localhost:9000` with your Medusa backend URL. diff --git a/www/apps/api-reference/app/_mdx/v2/admin.mdx b/www/apps/api-reference/app/_mdx/v2/admin.mdx new file mode 100644 index 0000000000000..44a23dc9430ab --- /dev/null +++ b/www/apps/api-reference/app/_mdx/v2/admin.mdx @@ -0,0 +1,478 @@ +import { Feedback, CodeTabs, CodeTab } from "docs-ui" +import SectionContainer from "@/components/Section/Container" +import formatReportLink from "@/utils/format-report-link" + + + + + +Medusa v2.0 is in development and not suitable for production +environments. As such, the API reference is incomplete and subject to +change, so please use it with caution. + + + +This API reference includes Medusa's Admin APIs, which are REST APIs exposed by the Medusa application. They are used to perform admin functionalities or create an admin dashboard to access and manipulate your commerce store's data. + +All API Routes are prefixed with `/admin`. So, during development, the API Routes will be available under the path `http://localhost:9000/admin`. For production, replace `http://localhost:9000` with your Medusa application URL. + + + + + + + +## Authentication + +There are three ways to send authenticated requests to the Medusa server: Using an admin user's API token, using a JWT token in a bearer authorization header, or using a cookie session ID. + +### Bearer Authorization with JWT Tokens + +Use a JWT token in a request's bearer authorization header to send authenticated requests. Authentication state is managed by the client, which is ideal for Jamstack applications and mobile applications. + +#### How to Obtain the JWT Token + +{/* TODO add correct link to auth route */} + +JWT tokens are obtained by sending a request to the authentication route passing it the user's email and password in the request body. + +For example: + +```bash +curl -X POST '{backend_url}/auth/admin/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "user@example.com", + "password": "supersecret" +}' +``` + +If authenticated successfully, an object is returned in the response with the property `token` being the JWT token. + +#### How to Use the JWT Token + +Pass the JWT token in the authorization bearer header: + + +```bash +Authorization: Bearer {jwt_token} +``` + +--- + +### API Token + +Use a user's API Token to send authenticated requests. + + + +This authentication method relies on using another authentication method first, as you must be an authenticated user to create an API token. + + + +#### How to Create an API Token for a User + +Use the [Create API Key API Route](#api-keys_postapikeys) to create an API token: + +```bash +curl --location 'localhost:9000/admin/api-keys' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer {jwt_token}' \ +--data '{ + "title": "my token", + "type": "secret" +}' +``` + +{/* TODO add a link to the API key object */} + +An `api_key` object is returned in the response. You need its `token` property. + +#### How to Use the API Token + + +The API token can be used by providing it in a basic authorization header: + +```bash +Authorization: Basic {api_key_token} +``` + +--- + +### Cookie Session ID + +When you authenticate a user and create a cookie session ID for them, the cookie session ID is passed automatically when sending the request from the browser, or with tools like Postman. + +### How to Obtain the Cookie Session + +To obtain a cookie session ID, you must have a [JWT token for bearer authentication](#bearer-authorization-with-jwt-tokens). + +{/* TODO add a link to the session authentication route. */} + +Then, send a request to the session authentication API route. To view the cookie session ID, pass the `-v` option to the `curl` command: + +```bash +curl -v -X POST '{backend_url}/auth/session' \ +--header 'Authorization: Bearer {jwt_token}' +``` + + +The headers will be logged in the terminal as well as the response. You +should find in the headers a Cookie header similar to this: + + +```bash +Set-Cookie: connect.sid=s%3A2Bu8BkaP9JUfHu9rG59G16Ma0QZf6Gj1.WT549XqX37PN8n0OecqnMCq798eLjZC5IT7yiDCBHPM; +``` + +#### How to Use the Cookie Session ID in cURL + +Copy the value after `connect.sid` (without the `;` at the end) and pass +it as a cookie in subsequent requests as the following: + + +```bash +curl '{backend_url}/admin/products' \ +-H 'Cookie: connect.sid={sid}' +``` + + +Where `{sid}` is the value of `connect.sid` that you copied. + +#### Including Credentials in the Fetch API + +If you're sending requests using JavaScript's Fetch API, you must pass the `credentials` option +with the value `include` to all the requests you're sending. For example: + +```js +fetch(`/admin/products`, { + credentials: "include", +}) +``` + + + + + + + +## HTTP Compression + +If you've enabled HTTP Compression in your Medusa configurations, and you +want to disable it for some requests, you can pass the `x-no-compression` +header in your requests: + +```bash +x-no-compression: true +``` + + + + + + + +## Select Fields and Relations + +Many API Routes accept a `fields` query that allows you to select which fields and relations should be returned in a record. + + + + +When you pass the `fields` query, only specified fields and relations, along with the `id`, are retrieved in the result. + + + +### Select Multiple Fields + +Separate the fields and relations you want to select with a comma. + +For example: + +```bash +curl 'localhost:9000/admin/products?fields=title,handle' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns only the `title` and `handle` fields of a product. + +### Select Relations + +To select a relation, pass to `fields` the relation name prefixed by `.*`. For example: + +```bash +curl 'localhost:9000/admin/products?fields=variants.*' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns the variants of each product. + +### Select Fields in a Relation + +The `.*` suffix selects all fields of the relation's data model. + +To select a specific field, change the `*` to the field's name. + +To specify multiple fields, pass each of the fields with the `.` format, separated by a comma. + +For example: + +```bash +curl 'localhost:9000/admin/products?fields=variants.title,variants.sku' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns the variants of each product, but the variants only have their `id`, `title`, and `sku` fields. The `id` is always included. + + + + + +## Query Parameter Types + + +This section covers how to pass some common data types as query parameters. + + +### Strings + + +You can pass a string value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/admin/products?title=Shirt" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +If the string has any characters other than letters and numbers, you must +encode them. + + +For example, if the string has spaces, you can encode the space with `+` or +`%20`: + + +```bash +curl "http://localhost:9000/admin/products?title=Blue%20Shirt" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +You can use tools like [this one](https://www.urlencoder.org/) to learn how +a value can be encoded. + + +### Integers + + +You can pass an integer value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/admin/products?offset=1" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +### Boolean + + +You can pass a boolean value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/admin/products?is_giftcard=true" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +### Date and DateTime + + +You can pass a date value in the form `=`. The date +must be in the format `YYYY-MM-DD`. + + +For example: + + +```bash +curl -g "http://localhost:9000/admin/products?created_at[lt]=2023-02-17" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +You can also pass the time using the format `YYYY-MM-DDTHH:MM:SSZ`. Please +note that the `T` and `Z` here are fixed. + + +For example: + + +```bash +curl -g "http://localhost:9000/admin/products?created_at[lt]=2023-02-17T07:22:30Z" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +### Array + + +Each array value must be passed as a separate query parameter in the form +`[]=`. You can also specify the index of each +parameter in the brackets `[0]=`. + + +For example: + + +```bash +curl -g "http://localhost:9000/admin/products?sales_channel_id[]=sc_01GPGVB42PZ7N3YQEP2WDM7PC7&sales_channel_id[]=sc_234PGVB42PZ7N3YQEP2WDM7PC7" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +Note that the `-g` parameter passed to `curl` disables errors being thrown +for using the brackets. Read more +[here](https://curl.se/docs/manpage.html#-g). + + +### Object + + +Object parameters must be passed as separate query parameters in the form +`[]=`. + + +For example: + + +```bash +curl -g "http://localhost:9000/admin/products?created_at[lt]=2023-02-17&created_at[gt]=2022-09-17" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + + + + + + +## Pagination + +### Query Parameters + + +In listing API Routes, such as list customers or list products, you can control the pagination using the query parameters `limit` and `offset`. + + +`limit` is used to specify the maximum number of items to be returned in the response. `offset` is used to specify how many items to skip before returning the resulting records. + + +Use the `offset` query parameter to change between pages. For example, if the limit is `50`, at page `1` the offset should be `0`; at page `2` the offset should be `50`, and so on. + + +For example: + +```bash +curl "http://localhost:9000/admin/products?limit=5" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +### Response Fields + + +In the response of listing API Routes, aside from the records retrieved, +there are three pagination-related fields returned: + + +- `limit`: the maximum number of items that can be returned in the response. +- `offset`: the number of items that were skipped before the records in the result. +- `count`: the total number of available items of this data model. It can be used to determine how many pages are there. + + +For example, if the `count` is `100` and the `limit` is `50`, divide the +`count` by the `limit` to get the number of pages: `100/50 = 2 pages`. + + +### Sort Order + + +The `order` field (available on API Routes that support pagination) allows you to +sort the retrieved items by a field of that item. + +For example, pass the query parameter `order=created_at` to sort products by their `created_at` field: + +```bash +curl "http://localhost:9000/admin/products?order=created_at" \ +-H 'Authorization: Bearer {jwt_token}' +``` + +By default, the sort direction is ascending. To change it to +descending, pass a dash (`-`) before the field name. + +For example: + +```bash +curl "http://localhost:9000/admin/products?order=-created_at" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +This sorts the products by their `created_at` field in the descending order. + + + + \ No newline at end of file diff --git a/www/apps/api-reference/app/_mdx/v2/client-libraries.mdx b/www/apps/api-reference/app/_mdx/v2/client-libraries.mdx new file mode 100644 index 0000000000000..b85d8aaea3255 --- /dev/null +++ b/www/apps/api-reference/app/_mdx/v2/client-libraries.mdx @@ -0,0 +1,20 @@ +import Space from "@/components/Space" +import DownloadFull from "@/components/DownloadFull" + +### Just Getting Started? + +Check out the [quickstart guide](https://docs.medusajs.com/create-medusa-app). + + + + + +Support for v2 API Routes is coming soon in Medusa JS Client and Medusa React. + + + +### Download Full Reference + +Download this reference as an OpenApi YAML file. You can import this file to tools like Postman and start sending requests directly to your Medusa backend. + + \ No newline at end of file diff --git a/www/apps/api-reference/app/_mdx/v2/store.mdx b/www/apps/api-reference/app/_mdx/v2/store.mdx new file mode 100644 index 0000000000000..277eab62e9974 --- /dev/null +++ b/www/apps/api-reference/app/_mdx/v2/store.mdx @@ -0,0 +1,495 @@ + +import { Feedback, CodeTabs, CodeTab } from "docs-ui" +import SectionContainer from "@/components/Section/Container" +import formatReportLink from "@/utils/format-report-link" + + + + + +Medusa v2.0 is in development and not suitable for production +environments. As such, the API reference is incomplete and subject to +change, so please use it with caution. + + + +This API reference includes Medusa's Store APIs, which are REST APIs exposed by the Medusa application. They are used to create a storefront for your commerce store, such as a webshop or a commerce mobile app. + +All API Routes are prefixed with `/store`. So, during development, the API Routes will be available under the path `http://localhost:9000/store`. For production, replace `http://localhost:9000` with your Medusa application URL. + + + + + + + +## Authentication + +There are two ways to send authenticated requests to the Medusa application: Using a JWT token or using a Cookie Session ID. + +### Bearer Authorization with JWT Tokens + +Use a JWT token in a request's bearer authorization header to send authenticated requests. Authentication state is managed by the client, which is ideal for Jamstack applications and mobile applications. + +#### How to Obtain the JWT Token + +{/* TODO add correct link to auth route */} + +JWT tokens are obtained by sending a request to the authentication route passing it the customer's email and password in the request body. + +For example: + +```bash +curl -X POST '{backend_url}/auth/store/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "user@example.com", + "password": "supersecret" +}' +``` + + + +{/* TODO add link to implementing login with google guide. */} + +Alternatively, you can use the `google` provider instead of `emailpass`. + + + +If authenticated successfully, an object is returned in the response with the property `token` being the JWT token. + +#### How to Use the JWT Token + +Pass the JWT token in the authorization bearer header: + + +```bash +Authorization: Bearer {jwt_token} +``` + +--- + +### Cookie Session ID + +When you authenticate a customer and create a cookie session ID for them, the cookie session ID is passed automatically when sending the request from the browser, or with tools like Postman. + +### How to Obtain the Cookie Session + +To obtain a cookie session ID, you must have a [JWT token for bearer authentication](#bearer-authorization-with-jwt-tokens). + +{/* TODO add a link to the session authentication route. */} + +Then, send a request to the session authentication API route. To view the cookie session ID, pass the `-v` option to the `curl` command: + +```bash +curl -v -X POST '{backend_url}/auth/session' \ +--header 'Authorization: Bearer {jwt_token}' +``` + + +The headers will be logged in the terminal as well as the response. You +should find in the headers a Cookie header similar to this: + + +```bash +Set-Cookie: connect.sid=s%3A2Bu8BkaP9JUfHu9rG59G16Ma0QZf6Gj1.WT549XqX37PN8n0OecqnMCq798eLjZC5IT7yiDCBHPM; +``` + +#### How to Use the Cookie Session ID in cURL + +Copy the value after `connect.sid` (without the `;` at the end) and pass +it as a cookie in subsequent requests as the following: + + +```bash +curl '{backend_url}/store/products' \ +-H 'Cookie: connect.sid={sid}' +``` + + +Where `{sid}` is the value of `connect.sid` that you copied. + +#### Including Credentials in the Fetch API + +If you're sending requests using JavaScript's Fetch API, you must pass the `credentials` option +with the value `include` to all the requests you're sending. For example: + +```js +fetch(`/store/products`, { + credentials: "include", +}) +``` + + + + + + + +## Publishable API Key + +Publishable API Keys allow you to send a request with a pre-defined scope. You can associate the +publishable API key with one or more resources, such as sales channels, then include the publishable +API key in the header of your requests. + +The Medusa application will infer the scope of the current +request based on the publishable API key. At the moment, publishable API keys only work with sales channels. + +It's highly recommended to create a publishable API key and pass it in the header of all your requests to the +store APIs. + +{/* TODO add link to the v2 guide */} + +{/* You can learn more about publishable API keys and how to use them in [this documentation](https://docs.medusajs.com/development/publishable-api-keys/). */} + +### How to Create a Publishable API Key + +{/* TODO add v2 links */} + +Create a publishable API key either using the admin REST APIs, or using the Medusa Admin. + +### How to Use a Publishable API Key + +You can pass the publishable API key in the header `x-publishable-api-key` in all your requests to the store APIs: + +```bash +x-publishable-api-key: {your_publishable_api_key} +``` + + + + + + + +## HTTP Compression + +If you've enabled HTTP Compression in your Medusa configurations, and you +want to disable it for some requests, you can pass the `x-no-compression` +header in your requests: + +```bash +x-no-compression: true +``` + + + + + + + +## Select Fields and Relations + +Many API Routes accept a `fields` query that allows you to select which fields and relations should be returned in a record. + + + + +When you pass the `fields` query, only specified fields and relations, along with the `id`, are retrieved in the result. + + + +### Select Multiple Fields + +Separate the fields and relations you want to select with a comma. + +For example: + +```bash +curl 'localhost:9000/store/products?fields=title,handle' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns only the `title` and `handle` fields of a product. + +### Select Relations + +To select a relation, pass to `fields` the relation name prefixed by `.*`. For example: + +```bash +curl 'localhost:9000/store/products?fields=variants.*' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns the variants of each product. + +### Select Fields in a Relation + +The `.*` suffix selects all fields of the relation's data model. + +To select a specific field, change the `*` to the field's name. + +To specify multiple fields, pass each of the fields with the `.` format, separated by a comma. + +For example: + +```bash +curl 'localhost:9000/store/products?fields=variants.title,variants.sku' \ +-H 'Authorization: Bearer {jwt_token}' +``` + +This returns the variants of each product, but the variants only have their `id`, `title`, and `sku` fields. The `id` is always included. + + + + + + + +## Query Parameter Types + + +This section covers how to pass some common data types as query parameters. +This is useful if you're sending requests to the API Routes and not using +our JS Client. For example, when using cURL or Postman. + + +### Strings + + +You can pass a string value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/store/products?title=Shirt" +``` + + +If the string has any characters other than letters and numbers, you must +encode them. + + +For example, if the string has spaces, you can encode the space with `+` or +`%20`: + + +```bash +curl "http://localhost:9000/store/products?title=Blue%20Shirt" +``` + + +You can use tools like [this one](https://www.urlencoder.org/) to learn how +a value can be encoded. + +### Integers + +You can pass an integer value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/store/products?offset=1" +``` + + +### Boolean + + +You can pass a boolean value in the form of `=`. + + +For example: + + +```bash +curl "http://localhost:9000/store/products?is_giftcard=true" +``` + + +### Date and DateTime + + +You can pass a date value in the form `=`. The date +must be in the format `YYYY-MM-DD`. + + +For example: + + +```bash +curl -g "http://localhost:9000/store/products?created_at[lt]=2023-02-17" +``` + + +You can also pass the time using the format `YYYY-MM-DDTHH:MM:SSZ`. Please +note that the `T` and `Z` here are fixed. + + +For example: + + +```bash +curl -g "http://localhost:9000/store/products?created_at[lt]=2023-02-17T07:22:30Z" +``` + + +### Array + + +Each array value must be passed as a separate query parameter in the form +`[]=`. You can also specify the index of each +parameter in the brackets `[0]=`. + + +For example: + + +```bash +curl -g "http://localhost:9000/store/products?sales_channel_id[]=sc_01GPGVB42PZ7N3YQEP2WDM7PC7&sales_channel_id[]=sc_234PGVB42PZ7N3YQEP2WDM7PC7" +``` + + +Note that the `-g` parameter passed to `curl` disables errors being thrown +for using the brackets. Read more +[here](https://curl.se/docs/manpage.html#-g). + + +### Object + + +Object parameters must be passed as separate query parameters in the form +`[]=`. + + +For example: + + +```bash +curl -g "http://localhost:9000/store/products?created_at[lt]=2023-02-17&created_at[gt]=2022-09-17" +``` + + + + + + + +## Pagination + + +### Query Parameters + + +In listing API Routes, such as list products, you can control the pagination using the query parameters `limit` and `offset`. + + +`limit` is used to specify the maximum number of items to be returned in the response. `offset` is used to specify how many items to skip before returning the resulting records. + + +Use the `offset` query parameter to change between pages. For example, if the limit is `50`, at page `1` the offset should be `0`; at page `2` the offset should be `50`, and so on. + + +For example: + +```bash +curl "http://localhost:9000/store/products?limit=5" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +### Response Fields + + +In the response of listing API Routes, aside from the records retrieved, +there are three pagination-related fields returned: + + +- `limit`: the maximum number of items that can be returned in the response. +- `offset`: the number of items that were skipped before the records in the result. +- `count`: the total number of available items of this data model. It can be used to determine how many pages are there. + + +For example, if the `count` is `100` and the `limit` is `50`, divide the +`count` by the `limit` to get the number of pages: `100/50 = 2 pages`. + + +### Sort Order + + +The `order` field (available on API Routes that support pagination) allows you to +sort the retrieved items by a field of that item. + +For example, pass the query parameter `order=created_at` to sort products by their `created_at` field: + +```bash +curl "http://localhost:9000/store/products?order=created_at" \ +-H 'Authorization: Bearer {jwt_token}' +``` + +By default, the sort direction is ascending. To change it to +descending, pass a dash (`-`) before the field name. + +For example: + +```bash +curl "http://localhost:9000/store/products?order=-created_at" \ +-H 'Authorization: Bearer {jwt_token}' +``` + + +This sorts the products by their `created_at` field in the descending order. + + + + \ No newline at end of file diff --git a/www/apps/api-reference/app/api/[area]/page.tsx b/www/apps/api-reference/app/api/[area]/page.tsx index 512589b374e77..d8dba1ece4097 100644 --- a/www/apps/api-reference/app/api/[area]/page.tsx +++ b/www/apps/api-reference/app/api/[area]/page.tsx @@ -1,7 +1,7 @@ import AreaProvider from "@/providers/area" -import AdminDescription from "../../_mdx/admin.mdx" -import StoreDescription from "../../_mdx/store.mdx" -import ClientLibraries from "../../_mdx/client-libraries.mdx" +import AdminContentV1 from "../../_mdx/v1/admin.mdx" +import StoreContentV1 from "../../_mdx/v1/store.mdx" +import ClientLibrariesV1 from "../../_mdx/v1/client-libraries.mdx" import Section from "@/components/Section" import Tags from "@/components/Tags" import type { Area } from "@/types/openapi" @@ -25,11 +25,11 @@ const ReferencePage = async ({ params: { area } }: ReferencePageProps) => { mainContent={
- {area.includes("admin") && } - {area.includes("store") && } + {area.includes("admin") && } + {area.includes("store") && }
} - codeContent={} + codeContent={} className="flex-col-reverse" /> diff --git a/www/apps/api-reference/app/api/[area]/v2/opengraph-image.jpg b/www/apps/api-reference/app/api/[area]/v2/opengraph-image.jpg new file mode 100644 index 0000000000000..c56693dc37634 Binary files /dev/null and b/www/apps/api-reference/app/api/[area]/v2/opengraph-image.jpg differ diff --git a/www/apps/api-reference/app/api/[area]/v2/page.tsx b/www/apps/api-reference/app/api/[area]/v2/page.tsx new file mode 100644 index 0000000000000..e042634ea4c51 --- /dev/null +++ b/www/apps/api-reference/app/api/[area]/v2/page.tsx @@ -0,0 +1,62 @@ +import AreaProvider from "@/providers/area" +import AdminContentV2 from "../../../_mdx/v2/admin.mdx" +import StoreContentV2 from "../../../_mdx/v2/store.mdx" +import ClientLibrariesV2 from "../../../_mdx/v2/client-libraries.mdx" +import Section from "@/components/Section" +import Tags from "@/components/Tags" +import type { Area } from "@/types/openapi" +import DividedLayout from "@/layouts/Divided" +import { capitalize } from "docs-ui" +import PageTitleProvider from "@/providers/page-title" +import PageHeading from "@/components/PageHeading" + +type ReferencePageProps = { + params: { + area: Area + } +} + +const ReferencePage = async ({ params: { area } }: ReferencePageProps) => { + return ( + + + + + + {area.includes("admin") && } + {area.includes("store") && } + + } + codeContent={} + className="flex-col-reverse" + /> + + + + ) +} + +export default ReferencePage + +export function generateMetadata({ params: { area } }: ReferencePageProps) { + return { + title: `Medusa ${capitalize(area)} API Reference`, + description: `REST API reference for the Medusa ${area} API. This reference includes code snippets and examples for Medusa JS Client and cURL.`, + metadataBase: process.env.NEXT_PUBLIC_BASE_URL, + } +} + +export const dynamicParams = false + +export async function generateStaticParams() { + return [ + { + area: "admin", + }, + { + area: "store", + }, + ] +} diff --git a/www/apps/api-reference/app/api/[area]/v2/twitter-image.jpg b/www/apps/api-reference/app/api/[area]/v2/twitter-image.jpg new file mode 100644 index 0000000000000..c56693dc37634 Binary files /dev/null and b/www/apps/api-reference/app/api/[area]/v2/twitter-image.jpg differ diff --git a/www/apps/api-reference/components/Navbar/index.tsx b/www/apps/api-reference/components/Navbar/index.tsx index 572d435981ad5..af9c540220e4a 100644 --- a/www/apps/api-reference/components/Navbar/index.tsx +++ b/www/apps/api-reference/components/Navbar/index.tsx @@ -11,11 +11,13 @@ import { useMemo } from "react" import { config } from "../../config" import { usePathname } from "next/navigation" import VersionSwitcher from "../VersionSwitcher" +import { useVersion } from "../../providers/version" const Navbar = () => { const { setMobileSidebarOpen, mobileSidebarOpen } = useSidebar() const pathname = usePathname() const { isLoading } = usePageLoading() + const { isVersioningEnabled } = useVersion() const navbarItems = useMemo( () => @@ -38,9 +40,7 @@ const Navbar = () => { mobileSidebarOpen, }} additionalActionsBefore={ - <> - {process.env.NEXT_PUBLIC_VERSIONING === "true" && } - + <>{isVersioningEnabled && } } additionalActionsAfter={} isLoading={isLoading} diff --git a/www/apps/api-reference/components/PageHeading/index.tsx b/www/apps/api-reference/components/PageHeading/index.tsx index d878d79aa1034..5af3c61c244d2 100644 --- a/www/apps/api-reference/components/PageHeading/index.tsx +++ b/www/apps/api-reference/components/PageHeading/index.tsx @@ -10,10 +10,9 @@ type PageHeadingProps = { const PageHeading = ({ className }: PageHeadingProps) => { const { area } = useArea() - const { version } = useVersion() + const { version, isVersioningEnabled } = useVersion() - const versionText = - process.env.NEXT_PUBLIC_VERSIONING === "true" ? ` V${version}` : "" + const versionText = isVersioningEnabled ? ` V${version}` : "" return (

diff --git a/www/apps/api-reference/components/VersionNote/index.tsx b/www/apps/api-reference/components/VersionNote/index.tsx deleted file mode 100644 index 1ecf6d40ed9ba..0000000000000 --- a/www/apps/api-reference/components/VersionNote/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client" - -import { Note } from "docs-ui" -import { useVersion } from "../../providers/version" - -const VersionNote = () => { - const { version } = useVersion() - - return ( - <> - {version === "2" && ( - - Medusa v2.0 is in development and not suitable for production - environments. As such, the API reference is incomplete and subject to - change, so please use it with caution. - - )} - - ) -} - -export default VersionNote diff --git a/www/apps/api-reference/components/VersionSwitcher/index.tsx b/www/apps/api-reference/components/VersionSwitcher/index.tsx index f1a9be56de8ef..29dc2fd3c7baf 100644 --- a/www/apps/api-reference/components/VersionSwitcher/index.tsx +++ b/www/apps/api-reference/components/VersionSwitcher/index.tsx @@ -1,11 +1,13 @@ "use client" import { Toggle } from "docs-ui" -import { useVersion } from "../../providers/version" import clsx from "clsx" +import { usePathname } from "next/navigation" +import { useVersion } from "../../providers/version" const VersionSwitcher = () => { - const { version, changeVersion } = useVersion() + const pathname = usePathname() + const { version } = useVersion() return (
@@ -19,7 +21,14 @@ const VersionSwitcher = () => { changeVersion(checked ? "2" : "1")} + onCheckedChange={(checked) => { + let newPath = pathname.replace("/v2", "") + if (checked) { + newPath += `/v2` + } + + location.href = location.href.replace(pathname, newPath) + }} /> void + isVersioningEnabled: boolean } const VersionContext = createContext(null) @@ -18,44 +17,18 @@ type VersionProviderProps = { const VersionProvider = ({ children }: VersionProviderProps) => { const pathname = usePathname() - const [version, setVersion] = useState("1") - const isBrowser = useIsBrowser() - const changeVersion = (version: Version) => { - if (!isBrowser || process.env.NEXT_PUBLIC_VERSIONING !== "true") { - return - } - localStorage.setItem("api-version", version) - - location.href = `${location.href.substring( - 0, - location.href.indexOf(location.pathname) - )}${pathname}` - } - - useEffect(() => { - if (!isBrowser || process.env.NEXT_PUBLIC_VERSIONING !== "true") { - return - } - // try to load from localstorage - const versionInLocalStorage = localStorage.getItem("api-version") as Version - if (versionInLocalStorage) { - setVersion(versionInLocalStorage) - } - }, [isBrowser]) - - useEffect(() => { - if (!isBrowser || process.env.NEXT_PUBLIC_VERSIONING !== "true") { - return - } - const versionInLocalStorage = localStorage.getItem("api-version") as Version - if (version !== versionInLocalStorage) { - localStorage.setItem("api-version", version) - } - }, [version, isBrowser]) + const version = useMemo(() => { + return pathname.includes("v2") ? "2" : "1" + }, [pathname]) return ( - + {children} ) diff --git a/www/utils/packages/docblock-generator/src/classes/helpers/formatter.ts b/www/utils/packages/docblock-generator/src/classes/helpers/formatter.ts index 929a22ebe64e9..7a18c1362cac7 100644 --- a/www/utils/packages/docblock-generator/src/classes/helpers/formatter.ts +++ b/www/utils/packages/docblock-generator/src/classes/helpers/formatter.ts @@ -106,7 +106,10 @@ class Formatter { this.eslintConfig = ( await import( - path.relative(dirname(), path.join(this.cwd, ".eslintrc.js")) + path.relative( + dirname(import.meta.url), + path.join(this.cwd, ".eslintrc.js") + ) ) ).default as Linter.Config diff --git a/www/utils/packages/docblock-generator/src/classes/kinds/default.ts b/www/utils/packages/docblock-generator/src/classes/kinds/default.ts index e20d864b14dcd..1a502cc8c5740 100644 --- a/www/utils/packages/docblock-generator/src/classes/kinds/default.ts +++ b/www/utils/packages/docblock-generator/src/classes/kinds/default.ts @@ -474,7 +474,7 @@ class DefaultKindGenerator { } if ( - symbolType.symbol.valueDeclaration && + symbolType.symbol?.valueDeclaration && "heritageClauses" in symbolType.symbol.valueDeclaration ) { return this.isEntity({ diff --git a/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts b/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts index 78818f6d810e3..50ce7c4ab45b7 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-git-changes.ts @@ -40,7 +40,7 @@ export default async function runGitChanges({ ...options, }) - oasGenerator.run() + await oasGenerator.run() } console.log(`Finished generating docs for ${files.length} files.`) diff --git a/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts b/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts index a88034d4138ee..1ed2cd2332ca9 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-git-commit.ts @@ -48,7 +48,7 @@ export default async function ( ...options, }) - oasGenerator.run() + await oasGenerator.run() } console.log(`Finished generating docs for ${filteredFiles.length} files.`) diff --git a/www/utils/packages/docblock-generator/src/commands/run-release.ts b/www/utils/packages/docblock-generator/src/commands/run-release.ts index 62df087beae24..5af53a24c138d 100644 --- a/www/utils/packages/docblock-generator/src/commands/run-release.ts +++ b/www/utils/packages/docblock-generator/src/commands/run-release.ts @@ -46,7 +46,7 @@ export default async function ({ type, tag, ...options }: CommonCliOptions) { ...options, }) - oasGenerator.run() + await oasGenerator.run() } console.log(`Finished generating docs for ${filteredFiles.length} files.`) diff --git a/www/utils/packages/docblock-generator/src/utils/dirname.ts b/www/utils/packages/docblock-generator/src/utils/dirname.ts index fbdb8f6a90589..4e4a89bd72173 100644 --- a/www/utils/packages/docblock-generator/src/utils/dirname.ts +++ b/www/utils/packages/docblock-generator/src/utils/dirname.ts @@ -1,8 +1,8 @@ import path from "path" import { fileURLToPath } from "url" -export default function dirname() { - const __filename = fileURLToPath(import.meta.url) +export default function dirname(fileUrl: string) { + const __filename = fileURLToPath(fileUrl) return path.dirname(__filename) } diff --git a/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts b/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts index 2d8ac2af00b8d..b5040b0ffc581 100644 --- a/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts +++ b/www/utils/packages/docblock-generator/src/utils/get-monorepo-root.ts @@ -10,6 +10,6 @@ import dirname from "./dirname.js" export default function getMonorepoRoot() { return ( process.env.MONOREPO_ROOT_PATH || - path.join(dirname(), "..", "..", "..", "..", "..", "..") + path.join(dirname(import.meta.url), "..", "..", "..", "..", "..", "..") ) }