From 99045848fd3e863359c7878d9bc05271ed083a0e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 22 Jan 2024 14:06:49 +0100 Subject: [PATCH] feat(medusa,types,core-flows,utils): added delete endpoints for campaigns and promotions (#6152) what: adds delete endpoints for: - campaigns (RESOLVES CORE-1686) - promotions (RESOLVES CORE-1680) --- .changeset/early-meals-brake.md | 8 + .../promotion/admin/delete-campaign.spec.ts | 72 +++++++ .../promotion/admin/delete-promotion.spec.ts | 73 +++++++ .../definition/promotion/create-promotions.ts | 1 - .../definition/promotion/delete-campaigns.ts | 12 ++ .../definition/promotion/delete-promotions.ts | 12 ++ .../src/definition/promotion/index.ts | 2 + .../handlers/promotion/delete-campaigns.ts | 28 +++ .../handlers/promotion/delete-promotions.ts | 28 +++ .../src/handlers/promotion/index.ts | 2 + .../src/api-v2/admin/campaigns/[id]/route.ts | 27 ++- .../src/api-v2/admin/promotions/[id]/route.ts | 27 ++- .../promotion-module/campaign.spec.ts | 68 +++++- .../promotion-module/promotion.spec.ts | 50 ++++- .../.snapshot-medusa-promotion.json | 36 +++- ...17090706.ts => Migration20240122070028.ts} | 8 +- .../src/models/application-method.ts | 12 +- .../promotion/src/models/campaign-budget.ts | 15 +- packages/promotion/src/models/campaign.ts | 24 +-- .../src/models/promotion-rule-value.ts | 25 ++- .../promotion/src/models/promotion-rule.ts | 12 +- packages/promotion/src/models/promotion.ts | 15 +- .../src/services/promotion-module.ts | 196 ++++++++++++++++-- packages/types/src/promotion/service.ts | 26 ++- .../src/dal/mikro-orm/mikro-orm-repository.ts | 4 +- 25 files changed, 699 insertions(+), 84 deletions(-) create mode 100644 .changeset/early-meals-brake.md create mode 100644 integration-tests/plugins/__tests__/promotion/admin/delete-campaign.spec.ts create mode 100644 integration-tests/plugins/__tests__/promotion/admin/delete-promotion.spec.ts create mode 100644 packages/core-flows/src/definition/promotion/delete-campaigns.ts create mode 100644 packages/core-flows/src/definition/promotion/delete-promotions.ts create mode 100644 packages/core-flows/src/handlers/promotion/delete-campaigns.ts create mode 100644 packages/core-flows/src/handlers/promotion/delete-promotions.ts rename packages/promotion/src/migrations/{Migration20240117090706.ts => Migration20240122070028.ts} (93%) diff --git a/.changeset/early-meals-brake.md b/.changeset/early-meals-brake.md new file mode 100644 index 0000000000..813c69941c --- /dev/null +++ b/.changeset/early-meals-brake.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/core-flows": patch +"@medusajs/utils": patch +--- + +feat(medusa,types,core-flows,utils): added delete endpoints for campaigns and promotions diff --git a/integration-tests/plugins/__tests__/promotion/admin/delete-campaign.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/delete-campaign.spec.ts new file mode 100644 index 0000000000..47c215129e --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/delete-campaign.spec.ts @@ -0,0 +1,72 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("DELETE /admin/campaigns/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + 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 api = useApi() as any + 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/plugins/__tests__/promotion/admin/delete-promotion.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/delete-promotion.spec.ts new file mode 100644 index 0000000000..2c66f0ab0f --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/delete-promotion.spec.ts @@ -0,0 +1,73 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("DELETE /admin/promotions/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete promotion successfully", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "TEST", + type: "standard", + application_method: { + type: "fixed", + target_type: "order", + value: "100", + }, + }) + + const api = useApi() as any + const deleteRes = await api.delete( + `/admin/promotions/${createdPromotion.id}`, + adminHeaders + ) + + expect(deleteRes.status).toEqual(200) + + const promotions = await promotionModuleService.list({ + id: [createdPromotion.id], + }) + + expect(promotions.length).toEqual(0) + }) +}) diff --git a/packages/core-flows/src/definition/promotion/create-promotions.ts b/packages/core-flows/src/definition/promotion/create-promotions.ts index 68deb30e73..227be13dd0 100644 --- a/packages/core-flows/src/definition/promotion/create-promotions.ts +++ b/packages/core-flows/src/definition/promotion/create-promotions.ts @@ -3,7 +3,6 @@ import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { createPromotionsStep } from "../../handlers/promotion" type WorkflowInput = { promotionsData: CreatePromotionDTO[] } -type WorkflowOutput = PromotionDTO[] export const createPromotionsWorkflowId = "create-promotions" export const createPromotionsWorkflow = createWorkflow( diff --git a/packages/core-flows/src/definition/promotion/delete-campaigns.ts b/packages/core-flows/src/definition/promotion/delete-campaigns.ts new file mode 100644 index 0000000000..578e95e89b --- /dev/null +++ b/packages/core-flows/src/definition/promotion/delete-campaigns.ts @@ -0,0 +1,12 @@ +import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk" +import { deleteCampaignsStep } from "../../handlers/promotion" + +type WorkflowInput = { ids: string[] } + +export const deleteCampaignsWorkflowId = "delete-campaigns" +export const deleteCampaignsWorkflow = createWorkflow( + deleteCampaignsWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteCampaignsStep(input.ids) + } +) diff --git a/packages/core-flows/src/definition/promotion/delete-promotions.ts b/packages/core-flows/src/definition/promotion/delete-promotions.ts new file mode 100644 index 0000000000..ab4794d4ac --- /dev/null +++ b/packages/core-flows/src/definition/promotion/delete-promotions.ts @@ -0,0 +1,12 @@ +import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk" +import { deletePromotionsStep } from "../../handlers/promotion" + +type WorkflowInput = { ids: string[] } + +export const deletePromotionsWorkflowId = "delete-promotions" +export const deletePromotionsWorkflow = createWorkflow( + deletePromotionsWorkflowId, + (input: WorkflowData): WorkflowData => { + return deletePromotionsStep(input.ids) + } +) diff --git a/packages/core-flows/src/definition/promotion/index.ts b/packages/core-flows/src/definition/promotion/index.ts index 92e4c5a638..5e81ec9e63 100644 --- a/packages/core-flows/src/definition/promotion/index.ts +++ b/packages/core-flows/src/definition/promotion/index.ts @@ -1,4 +1,6 @@ export * from "./create-campaigns" export * from "./create-promotions" +export * from "./delete-campaigns" +export * from "./delete-promotions" export * from "./update-campaigns" export * from "./update-promotions" diff --git a/packages/core-flows/src/handlers/promotion/delete-campaigns.ts b/packages/core-flows/src/handlers/promotion/delete-campaigns.ts new file mode 100644 index 0000000000..c0b7f20f59 --- /dev/null +++ b/packages/core-flows/src/handlers/promotion/delete-campaigns.ts @@ -0,0 +1,28 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deleteCampaignsStepId = "delete-campaigns" +export const deleteCampaignsStep = createStep( + deleteCampaignsStepId, + async (ids: string[], { container }) => { + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + await promotionModule.softDeleteCampaigns(ids) + + return new StepResponse(void 0, ids) + }, + async (idsToRestore, { container }) => { + if (!idsToRestore?.length) { + return + } + + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + await promotionModule.restoreCampaigns(idsToRestore) + } +) diff --git a/packages/core-flows/src/handlers/promotion/delete-promotions.ts b/packages/core-flows/src/handlers/promotion/delete-promotions.ts new file mode 100644 index 0000000000..ddcb66aa69 --- /dev/null +++ b/packages/core-flows/src/handlers/promotion/delete-promotions.ts @@ -0,0 +1,28 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deletePromotionsStepId = "delete-promotions" +export const deletePromotionsStep = createStep( + deletePromotionsStepId, + async (ids: string[], { container }) => { + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + await promotionModule.softDelete(ids) + + return new StepResponse(void 0, ids) + }, + async (idsToRestore, { container }) => { + if (!idsToRestore?.length) { + return + } + + const promotionModule = container.resolve( + ModuleRegistrationName.PROMOTION + ) + + await promotionModule.restore(idsToRestore) + } +) diff --git a/packages/core-flows/src/handlers/promotion/index.ts b/packages/core-flows/src/handlers/promotion/index.ts index 92e4c5a638..5e81ec9e63 100644 --- a/packages/core-flows/src/handlers/promotion/index.ts +++ b/packages/core-flows/src/handlers/promotion/index.ts @@ -1,4 +1,6 @@ export * from "./create-campaigns" export * from "./create-promotions" +export * from "./delete-campaigns" +export * from "./delete-promotions" export * from "./update-campaigns" export * from "./update-promotions" diff --git a/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts b/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts index 5515a0f8e3..a9d3a62434 100644 --- a/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts @@ -1,4 +1,7 @@ -import { updateCampaignsWorkflow } from "@medusajs/core-flows" +import { + deleteCampaignsWorkflow, + updateCampaignsWorkflow, +} from "@medusajs/core-flows" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" @@ -39,3 +42,25 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { res.status(200).json({ campaign: result[0] }) } + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.id + const manager = req.scope.resolve("manager") + const deleteCampaigns = deleteCampaignsWorkflow(req.scope) + + const { errors } = await deleteCampaigns.run({ + input: { ids: [id] }, + context: { manager }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "campaign", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts b/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts index 388d697ea5..d22211287e 100644 --- a/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts @@ -1,4 +1,7 @@ -import { updatePromotionsWorkflow } from "@medusajs/core-flows" +import { + deletePromotionsWorkflow, + updatePromotionsWorkflow, +} from "@medusajs/core-flows" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" @@ -36,3 +39,25 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { res.status(200).json({ promotion: result[0] }) } + +export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { + const id = req.params.id + const manager = req.scope.resolve("manager") + const deletePromotions = deletePromotionsWorkflow(req.scope) + + const { errors } = await deletePromotions.run({ + input: { ids: [id] }, + context: { manager }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "promotion", + deleted: true, + }) +} diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts index b54c7158fd..43d1b3b06d 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts @@ -385,20 +385,70 @@ describe("Promotion Module Service: Campaigns", () => { }) describe("deleteCampaigns", () => { - beforeEach(async () => { - await createCampaigns(repositoryManager) - }) - - const id = "campaign-id-1" - it("should delete the campaigns given an id successfully", async () => { - await service.deleteCampaigns([id]) + const [createdCampaign] = await service.createCampaigns([ + { + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024"), + ends_at: new Date("01/01/2025"), + }, + ]) - const campaigns = await service.list({ - id: [id], + await service.deleteCampaigns([createdCampaign.id]) + + const campaigns = await service.listCampaigns( + { + id: [createdCampaign.id], + }, + { withDeleted: true } + ) + + expect(campaigns).toHaveLength(0) + }) + }) + + describe("softDeleteCampaigns", () => { + it("should soft delete the campaigns given an id successfully", async () => { + const [createdCampaign] = await service.createCampaigns([ + { + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024"), + ends_at: new Date("01/01/2025"), + }, + ]) + + await service.softDeleteCampaigns([createdCampaign.id]) + + const campaigns = await service.listCampaigns({ + id: [createdCampaign.id], }) expect(campaigns).toHaveLength(0) }) }) + + describe("restoreCampaigns", () => { + it("should restore the campaigns given an id successfully", async () => { + const [createdCampaign] = await service.createCampaigns([ + { + name: "test", + campaign_identifier: "test", + starts_at: new Date("01/01/2024"), + ends_at: new Date("01/01/2025"), + }, + ]) + + await service.softDeleteCampaigns([createdCampaign.id]) + + let campaigns = await service.listCampaigns({ id: [createdCampaign.id] }) + + expect(campaigns).toHaveLength(0) + await service.restoreCampaigns([createdCampaign.id]) + + campaigns = await service.listCampaigns({ id: [createdCampaign.id] }) + expect(campaigns).toHaveLength(1) + }) + }) }) diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 1a07483b8b..9982b25e06 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -762,23 +762,61 @@ describe("Promotion Service", () => { }) describe("delete", () => { - beforeEach(async () => { - await createPromotions(repositoryManager) + it("should soft delete the promotions given an id successfully", async () => { + const createdPromotion = await service.create({ + code: "TEST", + type: "standard", + }) + + await service.delete([createdPromotion.id]) + + const promotions = await service.list( + { + id: [createdPromotion.id], + }, + { withDeleted: true } + ) + + expect(promotions).toHaveLength(0) }) + }) - const id = "promotion-id-1" + describe("softDelete", () => { + it("should soft delete the promotions given an id successfully", async () => { + const createdPromotion = await service.create({ + code: "TEST", + type: "standard", + }) - it("should delete the promotions given an id successfully", async () => { - await service.delete([id]) + await service.softDelete([createdPromotion.id]) const promotions = await service.list({ - id: [id], + id: [createdPromotion.id], }) expect(promotions).toHaveLength(0) }) }) + describe("restore", () => { + it("should restore the promotions given an id successfully", async () => { + const createdPromotion = await service.create({ + code: "TEST", + type: "standard", + }) + + await service.softDelete([createdPromotion.id]) + + let promotions = await service.list({ id: [createdPromotion.id] }) + + expect(promotions).toHaveLength(0) + await service.restore([createdPromotion.id]) + + promotions = await service.list({ id: [createdPromotion.id] }) + expect(promotions).toHaveLength(1) + }) + }) + describe("addPromotionRules", () => { let promotion diff --git a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json index 5fb5f45305..bdf7a42841 100644 --- a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json +++ b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json @@ -365,8 +365,7 @@ "localTableName": "public.promotion", "referencedColumnNames": ["id"], "referencedTableName": "public.campaign", - "deleteRule": "set null", - "updateRule": "cascade" + "deleteRule": "set null" } } }, @@ -518,6 +517,7 @@ "localTableName": "public.application_method", "referencedColumnNames": ["id"], "referencedTableName": "public.promotion", + "deleteRule": "cascade", "updateRule": "cascade" } } @@ -758,6 +758,38 @@ "primary": false, "nullable": false, "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "promotion_rule_value", diff --git a/packages/promotion/src/migrations/Migration20240117090706.ts b/packages/promotion/src/migrations/Migration20240122070028.ts similarity index 93% rename from packages/promotion/src/migrations/Migration20240117090706.ts rename to packages/promotion/src/migrations/Migration20240122070028.ts index 09b3edd6a6..aad49ce928 100644 --- a/packages/promotion/src/migrations/Migration20240117090706.ts +++ b/packages/promotion/src/migrations/Migration20240122070028.ts @@ -1,6 +1,6 @@ import { Migration } from "@mikro-orm/migrations" -export class Migration20240117090706 extends Migration { +export class Migration20240122070028 extends Migration { async up(): Promise { this.addSql( 'create table "campaign" ("id" text not null, "name" text not null, "description" text null, "currency" text null, "campaign_identifier" text not null, "starts_at" timestamptz null, "ends_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "campaign_pkey" primary key ("id"));' @@ -63,7 +63,7 @@ export class Migration20240117090706 extends Migration { ) this.addSql( - 'create table "promotion_rule_value" ("id" text not null, "promotion_rule_id" text not null, "value" text not null, constraint "promotion_rule_value_pkey" primary key ("id"));' + 'create table "promotion_rule_value" ("id" text not null, "promotion_rule_id" text not null, "value" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_rule_value_pkey" primary key ("id"));' ) this.addSql( 'create index "IDX_promotion_rule_promotion_rule_value_id" on "promotion_rule_value" ("promotion_rule_id");' @@ -74,11 +74,11 @@ export class Migration20240117090706 extends Migration { ) this.addSql( - 'alter table "promotion" add constraint "promotion_campaign_id_foreign" foreign key ("campaign_id") references "campaign" ("id") on update cascade on delete set null;' + 'alter table "promotion" add constraint "promotion_campaign_id_foreign" foreign key ("campaign_id") references "campaign" ("id") on delete set null;' ) this.addSql( - 'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;' + 'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade on delete cascade;' ) this.addSql( diff --git a/packages/promotion/src/models/application-method.ts b/packages/promotion/src/models/application-method.ts index 57931ba0c5..1f28b377d4 100644 --- a/packages/promotion/src/models/application-method.ts +++ b/packages/promotion/src/models/application-method.ts @@ -4,12 +4,13 @@ import { ApplicationMethodTypeValues, DAL, } from "@medusajs/types" -import { PromotionUtils, generateEntityId } from "@medusajs/utils" +import { DALUtils, PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Collection, Entity, Enum, + Filter, Index, ManyToMany, OnInit, @@ -25,10 +26,10 @@ type OptionalFields = | "value" | "max_quantity" | "allocation" - | "deleted_at" - | DAL.EntityDateColumns + | DAL.SoftDeletableEntityDateColumns -@Entity() +@Entity({ tableName: "application_method" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class ApplicationMethod { [OptionalProps]?: OptionalFields @@ -58,6 +59,7 @@ export default class ApplicationMethod { @OneToOne({ entity: () => Promotion, + onDelete: "cascade", }) promotion: Promotion @@ -84,7 +86,7 @@ export default class ApplicationMethod { updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/models/campaign-budget.ts b/packages/promotion/src/models/campaign-budget.ts index ee0c447842..45e37d46c8 100644 --- a/packages/promotion/src/models/campaign-budget.ts +++ b/packages/promotion/src/models/campaign-budget.ts @@ -1,9 +1,10 @@ import { CampaignBudgetTypeValues, DAL } from "@medusajs/types" -import { PromotionUtils, generateEntityId } from "@medusajs/utils" +import { DALUtils, PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Entity, Enum, + Filter, Index, OnInit, OneToOne, @@ -17,10 +18,10 @@ type OptionalFields = | "description" | "limit" | "used" - | "deleted_at" - | DAL.EntityDateColumns + | DAL.SoftDeletableEntityDateColumns -@Entity() +@Entity({ tableName: "campaign_budget" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class CampaignBudget { [OptionalProps]?: OptionalFields @@ -34,7 +35,7 @@ export default class CampaignBudget { @OneToOne({ entity: () => Campaign, }) - campaign?: Campaign + campaign?: Campaign | null @Property({ columnType: "numeric", @@ -42,7 +43,7 @@ export default class CampaignBudget { serializer: Number, default: null, }) - limit: number | null + limit?: number | null @Property({ columnType: "numeric", @@ -67,7 +68,7 @@ export default class CampaignBudget { updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/models/campaign.ts b/packages/promotion/src/models/campaign.ts index c721348e81..81cffce1e4 100644 --- a/packages/promotion/src/models/campaign.ts +++ b/packages/promotion/src/models/campaign.ts @@ -1,9 +1,10 @@ import { DAL } from "@medusajs/types" -import { generateEntityId } from "@medusajs/utils" +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Collection, Entity, + Filter, OnInit, OneToMany, OneToOne, @@ -15,17 +16,16 @@ import { import CampaignBudget from "./campaign-budget" import Promotion from "./promotion" +type OptionalRelations = "budget" type OptionalFields = | "description" | "currency" | "starts_at" | "ends_at" - | "deleted_at" - | DAL.EntityDateColumns + | DAL.SoftDeletableEntityDateColumns -type OptionalRelations = "budget" - -@Entity() +@Entity({ tableName: "campaign" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class Campaign { [OptionalProps]?: OptionalFields | OptionalRelations @@ -36,10 +36,10 @@ export default class Campaign { name: string @Property({ columnType: "text", nullable: true }) - description: string | null + description?: string | null @Property({ columnType: "text", nullable: true }) - currency: string | null + currency?: string | null @Property({ columnType: "text" }) @Unique({ @@ -52,13 +52,13 @@ export default class Campaign { columnType: "timestamptz", nullable: true, }) - starts_at: Date | null + starts_at?: Date | null @Property({ columnType: "timestamptz", nullable: true, }) - ends_at: Date | null + ends_at?: Date | null @OneToOne({ entity: () => CampaignBudget, @@ -66,7 +66,7 @@ export default class Campaign { cascade: ["soft-remove"] as any, nullable: true, }) - budget: CampaignBudget | null + budget?: CampaignBudget | null @OneToMany(() => Promotion, (promotion) => promotion.campaign, { orphanRemoval: true, @@ -89,7 +89,7 @@ export default class Campaign { updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/models/promotion-rule-value.ts b/packages/promotion/src/models/promotion-rule-value.ts index 64c9cb6eab..3709382ddc 100644 --- a/packages/promotion/src/models/promotion-rule-value.ts +++ b/packages/promotion/src/models/promotion-rule-value.ts @@ -1,16 +1,17 @@ +import { DALUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Entity, + Filter, ManyToOne, OnInit, PrimaryKey, Property, } from "@mikro-orm/core" - -import { generateEntityId } from "@medusajs/utils" import PromotionRule from "./promotion-rule" -@Entity() +@Entity({ tableName: "promotion_rule_value" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class PromotionRuleValue { @PrimaryKey({ columnType: "text" }) id!: string @@ -25,6 +26,24 @@ export default class PromotionRuleValue { @Property({ columnType: "text" }) value: string + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "prorulval") diff --git a/packages/promotion/src/models/promotion-rule.ts b/packages/promotion/src/models/promotion-rule.ts index 8241f8a6e7..c603648325 100644 --- a/packages/promotion/src/models/promotion-rule.ts +++ b/packages/promotion/src/models/promotion-rule.ts @@ -1,11 +1,12 @@ import { DAL, PromotionRuleOperatorValues } from "@medusajs/types" -import { PromotionUtils, generateEntityId } from "@medusajs/utils" +import { DALUtils, PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, Collection, Entity, Enum, + Filter, Index, ManyToMany, OnInit, @@ -18,10 +19,11 @@ import ApplicationMethod from "./application-method" import Promotion from "./promotion" import PromotionRuleValue from "./promotion-rule-value" -type OptionalFields = "description" | "deleted_at" | DAL.EntityDateColumns +type OptionalFields = "description" | DAL.SoftDeletableEntityDateColumns type OptionalRelations = "values" | "promotions" -@Entity() +@Entity({ tableName: "promotion_rule" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class PromotionRule { [OptionalProps]?: OptionalFields | OptionalRelations @@ -29,7 +31,7 @@ export default class PromotionRule { id!: string @Property({ columnType: "text", nullable: true }) - description: string | null + description?: string | null @Index({ name: "IDX_promotion_rule_attribute" }) @Property({ columnType: "text" }) @@ -69,7 +71,7 @@ export default class PromotionRule { updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/models/promotion.ts b/packages/promotion/src/models/promotion.ts index 6327e35b77..ace7ee88ec 100644 --- a/packages/promotion/src/models/promotion.ts +++ b/packages/promotion/src/models/promotion.ts @@ -1,10 +1,11 @@ import { DAL, PromotionType } from "@medusajs/types" -import { PromotionUtils, generateEntityId } from "@medusajs/utils" +import { DALUtils, PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, Collection, Entity, Enum, + Filter, Index, ManyToMany, ManyToOne, @@ -19,10 +20,11 @@ import ApplicationMethod from "./application-method" import Campaign from "./campaign" import PromotionRule from "./promotion-rule" -type OptionalFields = "is_automatic" | "deleted_at" | DAL.EntityDateColumns +type OptionalFields = "is_automatic" | DAL.SoftDeletableEntityDateColumns type OptionalRelations = "application_method" | "campaign" -@Entity() +@Entity({ tableName: "promotion" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class Promotion { [OptionalProps]?: OptionalFields | OptionalRelations @@ -41,11 +43,12 @@ export default class Promotion { joinColumn: "campaign", fieldName: "campaign_id", nullable: true, + cascade: ["soft-remove"] as any, }) - campaign: Campaign + campaign?: Campaign | null @Property({ columnType: "boolean", default: false }) - is_automatic?: boolean = false + is_automatic: boolean = false @Index({ name: "IDX_promotion_type" }) @Enum(() => PromotionUtils.PromotionType) @@ -81,7 +84,7 @@ export default class Promotion { updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index bbdbc5fc10..e73b75ef49 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -5,6 +5,8 @@ import { InternalModuleDeclaration, ModuleJoinerConfig, PromotionTypes, + RestoreReturn, + SoftDeleteReturn, } from "@medusajs/types" import { ApplicationMethodTargetType, @@ -14,8 +16,16 @@ import { MedusaContext, MedusaError, isString, + mapObjectTo, } from "@medusajs/utils" -import { ApplicationMethod, Promotion } from "@models" +import { + ApplicationMethod, + Campaign, + CampaignBudget, + Promotion, + PromotionRule, + PromotionRuleValue, +} from "@models" import { ApplicationMethodService, CampaignBudgetService, @@ -42,29 +52,37 @@ import { validateApplicationMethodAttributes, validatePromotionRuleAttributes, } from "@utils" -import { joinerConfig } from "../joiner-config" +import { + LinkableKeys, + entityNameToLinkableKeysMap, + joinerConfig, +} from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - promotionService: PromotionService - applicationMethodService: ApplicationMethodService - promotionRuleService: PromotionRuleService - promotionRuleValueService: PromotionRuleValueService - campaignService: CampaignService - campaignBudgetService: CampaignBudgetService + promotionService: PromotionService + applicationMethodService: ApplicationMethodService + promotionRuleService: PromotionRuleService + promotionRuleValueService: PromotionRuleValueService + campaignService: CampaignService + campaignBudgetService: CampaignBudgetService } export default class PromotionModuleService< - TPromotion extends Promotion = Promotion + TPromotion extends Promotion = Promotion, + TPromotionRule extends PromotionRule = PromotionRule, + TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue, + TCampaign extends Campaign = Campaign, + TCampaignBudget extends CampaignBudget = CampaignBudget > implements PromotionTypes.IPromotionModuleService { protected baseRepository_: DAL.RepositoryService - protected promotionService_: PromotionService + protected promotionService_: PromotionService protected applicationMethodService_: ApplicationMethodService - protected promotionRuleService_: PromotionRuleService - protected promotionRuleValueService_: PromotionRuleValueService - protected campaignService_: CampaignService - protected campaignBudgetService_: CampaignBudgetService + protected promotionRuleService_: PromotionRuleService + protected promotionRuleValueService_: PromotionRuleValueService + protected campaignService_: CampaignService + protected campaignBudgetService_: CampaignBudgetService constructor( { @@ -809,12 +827,84 @@ export default class PromotionModuleService< @InjectTransactionManager("baseRepository_") async delete( - ids: string | string[], + ids: string[] | string, @MedusaContext() sharedContext: Context = {} ): Promise { - const promotionIds = Array.isArray(ids) ? ids : [ids] + const idsToDelete = Array.isArray(ids) ? ids : [ids] - await this.promotionService_.delete(promotionIds, sharedContext) + await this.promotionService_.delete(idsToDelete, sharedContext) + } + + @InjectManager("baseRepository_") + async softDelete< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + ids: string | string[], + { returnLinkableKeys }: SoftDeleteReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const idsToDelete = Array.isArray(ids) ? ids : [ids] + let [_, cascadedEntitiesMap] = await this.softDelete_( + idsToDelete, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + @InjectTransactionManager("baseRepository_") + protected async softDelete_( + promotionIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TPromotion[], Record]> { + return await this.promotionService_.softDelete(promotionIds, sharedContext) + } + + @InjectManager("baseRepository_") + async restore< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + ids: string | string[], + { returnLinkableKeys }: RestoreReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const idsToRestore = Array.isArray(ids) ? ids : [ids] + const [_, cascadedEntitiesMap] = await this.restore_( + idsToRestore, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + @InjectTransactionManager("baseRepository_") + async restore_( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TPromotion[], Record]> { + return await this.promotionService_.restore(ids, sharedContext) } @InjectManager("baseRepository_") @@ -1149,8 +1239,76 @@ export default class PromotionModuleService< ids: string | string[], @MedusaContext() sharedContext: Context = {} ): Promise { - const campaignIds = Array.isArray(ids) ? ids : [ids] + const idsToDelete = Array.isArray(ids) ? ids : [ids] - await this.promotionService_.delete(campaignIds, sharedContext) + await this.campaignService_.delete(idsToDelete, sharedContext) + } + + @InjectManager("baseRepository_") + async softDeleteCampaigns( + ids: string | string[], + { returnLinkableKeys }: SoftDeleteReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const idsToDelete = Array.isArray(ids) ? ids : [ids] + let [_, cascadedEntitiesMap] = await this.softDeleteCampaigns_( + idsToDelete, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + @InjectTransactionManager("baseRepository_") + protected async softDeleteCampaigns_( + campaignIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TCampaign[], Record]> { + return await this.campaignService_.softDelete(campaignIds, sharedContext) + } + + @InjectManager("baseRepository_") + async restoreCampaigns< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + ids: string | string[], + { returnLinkableKeys }: RestoreReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const idsToRestore = Array.isArray(ids) ? ids : [ids] + const [_, cascadedEntitiesMap] = await this.restoreCampaigns_( + idsToRestore, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + @InjectTransactionManager("baseRepository_") + async restoreCampaigns_( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TCampaign[], Record]> { + return await this.campaignService_.restore(ids, sharedContext) } } diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index 1e2e0e2e77..d968133f15 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -1,4 +1,5 @@ import { FindConfig } from "../common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { @@ -13,7 +14,6 @@ import { RemovePromotionRuleDTO, UpdatePromotionDTO, } from "./common" - import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations" export interface IPromotionModuleService extends IModuleService { @@ -66,6 +66,18 @@ export interface IPromotionModuleService extends IModuleService { delete(ids: string[], sharedContext?: Context): Promise delete(ids: string, sharedContext?: Context): Promise + softDelete( + promotionIds: string | string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restore( + promotionIds: string | string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + addPromotionRules( promotionId: string, rulesData: CreatePromotionRuleDTO[], @@ -130,4 +142,16 @@ export interface IPromotionModuleService extends IModuleService { deleteCampaigns(ids: string[], sharedContext?: Context): Promise deleteCampaigns(ids: string, sharedContext?: Context): Promise + + softDeleteCampaigns( + campaignIds: string | string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restoreCampaigns( + campaignIds: string | string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> } diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 5cd91f6a0f..714e943f44 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -17,9 +17,9 @@ import { EntityName, FilterQuery as MikroFilterQuery, } from "@mikro-orm/core/typings" -import { isString, MedusaError } from "../../common" +import { MedusaError, isString } from "../../common" import { MedusaContext } from "../../decorators" -import { buildQuery, InjectTransactionManager } from "../../modules-sdk" +import { InjectTransactionManager, buildQuery } from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, transactionWrapper,