From d60f3adc0329bc2ee36fdc6f452739eda0c4fd43 Mon Sep 17 00:00:00 2001 From: Stevche Radevski Date: Wed, 28 Feb 2024 11:08:11 +0100 Subject: [PATCH] feat: Add basic endpoints and workflows for Store module (#6515) --- .../__tests__/store/admin/store.spec.ts | 81 +++++++++++++++++++ integration-tests/plugins/medusa-config.js | 5 ++ integration-tests/plugins/package.json | 1 + packages/core-flows/src/index.ts | 1 + packages/core-flows/src/store/index.ts | 2 + .../src/store/steps/create-stores.ts | 34 ++++++++ .../src/store/steps/delete-stores.ts | 27 +++++++ packages/core-flows/src/store/steps/index.ts | 3 + .../src/store/steps/update-stores.ts | 51 ++++++++++++ .../src/store/workflows/create-stores.ts | 13 +++ .../src/store/workflows/delete-stores.ts | 12 +++ .../core-flows/src/store/workflows/index.ts | 3 + .../src/store/workflows/update-stores.ts | 18 +++++ .../src/api-v2/admin/stores/[id]/route.ts | 36 +++++++++ .../src/api-v2/admin/stores/middlewares.ts | 42 ++++++++++ .../src/api-v2/admin/stores/query-config.ts | 22 +++++ .../medusa/src/api-v2/admin/stores/route.ts | 27 +++++++ .../src/api-v2/admin/stores/validators.ts | 61 ++++++++++++++ packages/medusa/src/api-v2/middlewares.ts | 2 + .../migrations/.snapshot-medusa-store.json | 32 ++++++++ .../migrations/InitialSetup20240226130829.ts | 9 --- .../migrations/InitialSetup20240227075933.ts | 48 +++++++++++ packages/store/src/models/store.ts | 35 +++++++- packages/types/src/store/mutations/store.ts | 2 +- packages/types/src/store/service.ts | 39 +++++++++ yarn.lock | 3 +- 26 files changed, 596 insertions(+), 13 deletions(-) create mode 100644 integration-tests/plugins/__tests__/store/admin/store.spec.ts create mode 100644 packages/core-flows/src/store/index.ts create mode 100644 packages/core-flows/src/store/steps/create-stores.ts create mode 100644 packages/core-flows/src/store/steps/delete-stores.ts create mode 100644 packages/core-flows/src/store/steps/index.ts create mode 100644 packages/core-flows/src/store/steps/update-stores.ts create mode 100644 packages/core-flows/src/store/workflows/create-stores.ts create mode 100644 packages/core-flows/src/store/workflows/delete-stores.ts create mode 100644 packages/core-flows/src/store/workflows/index.ts create mode 100644 packages/core-flows/src/store/workflows/update-stores.ts create mode 100644 packages/medusa/src/api-v2/admin/stores/[id]/route.ts create mode 100644 packages/medusa/src/api-v2/admin/stores/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/stores/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/stores/route.ts create mode 100644 packages/medusa/src/api-v2/admin/stores/validators.ts delete mode 100644 packages/store/src/migrations/InitialSetup20240226130829.ts create mode 100644 packages/store/src/migrations/InitialSetup20240227075933.ts diff --git a/integration-tests/plugins/__tests__/store/admin/store.spec.ts b/integration-tests/plugins/__tests__/store/admin/store.spec.ts new file mode 100644 index 0000000000..1491e45d4c --- /dev/null +++ b/integration-tests/plugins/__tests__/store/admin/store.spec.ts @@ -0,0 +1,81 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IStoreModuleService } 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 { DataSource } from "typeorm" +import { createAdminUser } from "../../../helpers/create-admin-user" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("Store - Admin", () => { + let dbConnection: DataSource + let appContainer + let shutdownServer + let service: IStoreModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + service = appContainer.resolve(ModuleRegistrationName.STORE) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await createAdminUser(dbConnection, adminHeaders) + + const existingStores = await service.list({}) + await service.delete(existingStores.map((s) => s.id)) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should correctly implement the entire lifecycle of a store", async () => { + const api = useApi() as any + const createdStore = await service.create({ name: "Test store" }) + + expect(createdStore).toEqual( + expect.objectContaining({ + id: createdStore.id, + name: "Test store", + }) + ) + + const updated = await api.post( + `/admin/stores/${createdStore.id}`, + { + name: "Updated store", + }, + adminHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.store).toEqual( + expect.objectContaining({ + id: createdStore.id, + name: "Updated store", + }) + ) + + await service.delete(createdStore.id) + const listedStores = await api.get(`/admin/stores`, adminHeaders) + expect(listedStores.data.stores).toHaveLength(0) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index ad5f2dd3bb..5084890f1f 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -118,5 +118,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/api-key", }, + [Modules.STORE]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/store", + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index f29fa0f22e..0ce8a04699 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -21,6 +21,7 @@ "@medusajs/product": "workspace:^", "@medusajs/promotion": "workspace:^", "@medusajs/region": "workspace:^", + "@medusajs/store": "workspace:^", "@medusajs/user": "workspace:^", "@medusajs/utils": "workspace:^", "@medusajs/workflow-engine-inmemory": "workspace:*", diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index aa4e2ef320..7e1c10d923 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -8,3 +8,4 @@ export * from "./promotion" export * from "./region" export * from "./user" export * from "./api-key" +export * from "./store" diff --git a/packages/core-flows/src/store/index.ts b/packages/core-flows/src/store/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core-flows/src/store/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/store/steps/create-stores.ts b/packages/core-flows/src/store/steps/create-stores.ts new file mode 100644 index 0000000000..96ec94c405 --- /dev/null +++ b/packages/core-flows/src/store/steps/create-stores.ts @@ -0,0 +1,34 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CreateStoreDTO, IStoreModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type CreateStoresStepInput = { + stores: CreateStoreDTO[] +} + +export const createStoresStepId = "create-stores" +export const createStoresStep = createStep( + createStoresStepId, + async (data: CreateStoresStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.STORE + ) + + const created = await service.create(data.stores) + return new StepResponse( + created, + created.map((store) => store.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.STORE + ) + + await service.delete(createdIds) + } +) diff --git a/packages/core-flows/src/store/steps/delete-stores.ts b/packages/core-flows/src/store/steps/delete-stores.ts new file mode 100644 index 0000000000..42f42b600e --- /dev/null +++ b/packages/core-flows/src/store/steps/delete-stores.ts @@ -0,0 +1,27 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IStoreModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const deleteStoresStepId = "delete-stores" +export const deleteStoresStep = createStep( + deleteStoresStepId, + async (ids: string[], { container }) => { + const storeModule = container.resolve( + ModuleRegistrationName.STORE + ) + + await storeModule.softDelete(ids) + return new StepResponse(void 0, ids) + }, + async (idsToRestore, { container }) => { + if (!idsToRestore?.length) { + return + } + + const storeModule = container.resolve( + ModuleRegistrationName.STORE + ) + + await storeModule.restore(idsToRestore) + } +) diff --git a/packages/core-flows/src/store/steps/index.ts b/packages/core-flows/src/store/steps/index.ts new file mode 100644 index 0000000000..4005611611 --- /dev/null +++ b/packages/core-flows/src/store/steps/index.ts @@ -0,0 +1,3 @@ +export * from "./create-stores" +export * from "./delete-stores" +export * from "./update-stores" diff --git a/packages/core-flows/src/store/steps/update-stores.ts b/packages/core-flows/src/store/steps/update-stores.ts new file mode 100644 index 0000000000..a5164e560a --- /dev/null +++ b/packages/core-flows/src/store/steps/update-stores.ts @@ -0,0 +1,51 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FilterableStoreProps, + IStoreModuleService, + UpdateStoreDTO, +} from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type UpdateStoresStepInput = { + selector: FilterableStoreProps + update: UpdateStoreDTO +} + +export const updateStoresStepId = "update-stores" +export const updateStoresStep = createStep( + updateStoresStepId, + async (data: UpdateStoresStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.STORE + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + + const prevData = await service.list(data.selector, { + select: selects, + relations, + }) + + const stores = await service.update(data.selector, data.update) + return new StepResponse(stores, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.STORE + ) + + await service.upsert( + prevData.map((r) => ({ + ...r, + metadata: r.metadata || undefined, + })) + ) + } +) diff --git a/packages/core-flows/src/store/workflows/create-stores.ts b/packages/core-flows/src/store/workflows/create-stores.ts new file mode 100644 index 0000000000..d4458dabdf --- /dev/null +++ b/packages/core-flows/src/store/workflows/create-stores.ts @@ -0,0 +1,13 @@ +import { StoreDTO, CreateStoreDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createStoresStep } from "../steps" + +type WorkflowInput = { stores: CreateStoreDTO[] } + +export const createStoresWorkflowId = "create-stores" +export const createStoresWorkflow = createWorkflow( + createStoresWorkflowId, + (input: WorkflowData): WorkflowData => { + return createStoresStep(input) + } +) diff --git a/packages/core-flows/src/store/workflows/delete-stores.ts b/packages/core-flows/src/store/workflows/delete-stores.ts new file mode 100644 index 0000000000..14d5a3c49d --- /dev/null +++ b/packages/core-flows/src/store/workflows/delete-stores.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteStoresStep } from "../steps" + +type WorkflowInput = { ids: string[] } + +export const deleteStoresWorkflowId = "delete-stores" +export const deleteStoresWorkflow = createWorkflow( + deleteStoresWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteStoresStep(input.ids) + } +) diff --git a/packages/core-flows/src/store/workflows/index.ts b/packages/core-flows/src/store/workflows/index.ts new file mode 100644 index 0000000000..4005611611 --- /dev/null +++ b/packages/core-flows/src/store/workflows/index.ts @@ -0,0 +1,3 @@ +export * from "./create-stores" +export * from "./delete-stores" +export * from "./update-stores" diff --git a/packages/core-flows/src/store/workflows/update-stores.ts b/packages/core-flows/src/store/workflows/update-stores.ts new file mode 100644 index 0000000000..d5c08693eb --- /dev/null +++ b/packages/core-flows/src/store/workflows/update-stores.ts @@ -0,0 +1,18 @@ +import { StoreDTO, FilterableStoreProps, UpdateStoreDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateStoresStep } from "../steps" + +type UpdateStoresStepInput = { + selector: FilterableStoreProps + update: UpdateStoreDTO +} + +type WorkflowInput = UpdateStoresStepInput + +export const updateStoresWorkflowId = "update-stores" +export const updateStoresWorkflow = createWorkflow( + updateStoresWorkflowId, + (input: WorkflowData): WorkflowData => { + return updateStoresStep(input) + } +) diff --git a/packages/medusa/src/api-v2/admin/stores/[id]/route.ts b/packages/medusa/src/api-v2/admin/stores/[id]/route.ts new file mode 100644 index 0000000000..a5bd5d7010 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stores/[id]/route.ts @@ -0,0 +1,36 @@ +import { updateStoresWorkflow } from "@medusajs/core-flows" +import { UpdateStoreDTO } from "@medusajs/types" +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { defaultAdminStoreFields } from "../query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const variables = { id: req.params.id } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "store", + variables, + fields: defaultAdminStoreFields, + }) + + const [store] = await remoteQuery(queryObject) + res.status(200).json({ store }) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { result, errors } = await updateStoresWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + update: req.validatedBody as UpdateStoreDTO, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ store: result[0] }) +} diff --git a/packages/medusa/src/api-v2/admin/stores/middlewares.ts b/packages/medusa/src/api-v2/admin/stores/middlewares.ts new file mode 100644 index 0000000000..5091f0690b --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stores/middlewares.ts @@ -0,0 +1,42 @@ +import { transformBody, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" +import * as QueryConfig from "./query-config" +import { + AdminGetStoresParams, + AdminGetStoresStoreParams, + AdminPostStoresStoreReq, +} from "./validators" + +export const adminStoreRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["ALL"], + matcher: "/admin/stores*", + middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], + }, + { + method: ["GET"], + matcher: "/admin/stores", + middlewares: [ + transformQuery( + AdminGetStoresParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/stores/:id", + middlewares: [ + transformQuery( + AdminGetStoresStoreParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/stores/:id", + middlewares: [transformBody(AdminPostStoresStoreReq)], + }, +] diff --git a/packages/medusa/src/api-v2/admin/stores/query-config.ts b/packages/medusa/src/api-v2/admin/stores/query-config.ts new file mode 100644 index 0000000000..9140c2f213 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stores/query-config.ts @@ -0,0 +1,22 @@ +export const defaultAdminStoreRelations = [] +export const allowedAdminStoreRelations = [] +export const defaultAdminStoreFields = [ + "id", + "name", + "default_sales_channel_id", + "default_region_id", + "default_location_id", + "metadata", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminStoreFields, + defaultRelations: defaultAdminStoreRelations, + allowedRelations: allowedAdminStoreRelations, + isList: false, +} + +export const listTransformQueryConfig = { + defaultLimit: 20, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/stores/route.ts b/packages/medusa/src/api-v2/admin/stores/route.ts new file mode 100644 index 0000000000..0c63f37b9d --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stores/route.ts @@ -0,0 +1,27 @@ +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" +import { defaultAdminStoreFields } from "./query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "store", + variables: { + filters: req.filterableFields, + order: req.listConfig.order, + skip: req.listConfig.skip, + take: req.listConfig.take, + }, + fields: defaultAdminStoreFields, + }) + + const { rows: stores, metadata } = await remoteQuery(queryObject) + + res.json({ + stores, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} diff --git a/packages/medusa/src/api-v2/admin/stores/validators.ts b/packages/medusa/src/api-v2/admin/stores/validators.ts new file mode 100644 index 0000000000..39784fe71d --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stores/validators.ts @@ -0,0 +1,61 @@ +import { Type } from "class-transformer" +import { IsObject, IsOptional, IsString, ValidateNested } from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" + +export class AdminGetStoresStoreParams extends FindParams {} +/** + * Parameters used to filter and configure the pagination of the retrieved api keys. + */ +export class AdminGetStoresParams extends extendedFindParamsMixin({ + limit: 50, + offset: 0, +}) { + /** + * Search parameter for api keys. + */ + @IsString({ each: true }) + @IsOptional() + id?: string | string[] + + /** + * Filter by title + */ + @IsString({ each: true }) + @IsOptional() + name?: string | string[] + + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetStoresParams) + $and?: AdminGetStoresParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetStoresParams) + $or?: AdminGetStoresParams[] +} + +export class AdminPostStoresStoreReq { + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @IsString() + default_sales_channel_id?: string + + @IsOptional() + @IsString() + default_region_id?: string + + @IsOptional() + @IsString() + default_location_id?: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminDeleteStoresStoreReq {} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 11b048d0e9..43c2f55675 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -6,6 +6,7 @@ import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" +import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares" import { adminUserRoutesMiddlewares } from "./admin/users/middlewares" import { adminWorkflowsExecutionsMiddlewares } from "./admin/workflows-executions/middlewares" import { authRoutesMiddlewares } from "./auth/middlewares" @@ -31,5 +32,6 @@ export const config: MiddlewaresConfig = { ...adminInviteRoutesMiddlewares, ...adminApiKeyRoutesMiddlewares, ...hooksRoutesMiddlewares, + ...adminStoreRoutesMiddlewares, ], } diff --git a/packages/store/src/migrations/.snapshot-medusa-store.json b/packages/store/src/migrations/.snapshot-medusa-store.json index cf797f7239..072267583f 100644 --- a/packages/store/src/migrations/.snapshot-medusa-store.json +++ b/packages/store/src/migrations/.snapshot-medusa-store.json @@ -22,6 +22,7 @@ "autoincrement": false, "primary": false, "nullable": false, + "default": "'Medusa Store'", "mappedType": "text" }, "default_sales_channel_id": { @@ -70,11 +71,42 @@ "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": "store", "schema": "public", "indexes": [ + { + "keyName": "IDX_store_deleted_at", + "columnNames": [ + "deleted_at" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_store_deleted_at\" ON \"store\" (deleted_at) WHERE deleted_at IS NOT NULL" + }, { "keyName": "store_pkey", "columnNames": [ diff --git a/packages/store/src/migrations/InitialSetup20240226130829.ts b/packages/store/src/migrations/InitialSetup20240226130829.ts deleted file mode 100644 index 121cf6ad86..0000000000 --- a/packages/store/src/migrations/InitialSetup20240226130829.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class InitialSetup20240226130829 extends Migration { - async up(): Promise { - this.addSql( - 'create table if not exists "store" ("id" text not null, "name" text not null, "default_sales_channel_id" text null, "default_region_id" text null, "default_location_id" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), constraint "store_pkey" primary key ("id"));' - ) - } -} diff --git a/packages/store/src/migrations/InitialSetup20240227075933.ts b/packages/store/src/migrations/InitialSetup20240227075933.ts new file mode 100644 index 0000000000..ca30f4ac80 --- /dev/null +++ b/packages/store/src/migrations/InitialSetup20240227075933.ts @@ -0,0 +1,48 @@ +import { Migration } from "@mikro-orm/migrations" + +export class InitialSetup20240226130829 extends Migration { + async up(): Promise { + // TODO: The migration needs to take care of moving data before dropping columns, among other things + const storeTables = await this.execute( + "select * from information_schema.tables where table_name = 'store' and table_schema = 'public'" + ) + + if (storeTables.length > 0) { + this.addSql(`alter table "store" alter column "id" TYPE text;`) + this.addSql(`alter table "store" alter column "name" TYPE text;`) + this.addSql( + `alter table "store" alter column "name" SET DEFAULT 'Medusa Store';` + ) + this.addSql( + `alter table "store" alter column "default_sales_channel_id" TYPE text;` + ) + this.addSql( + `alter table "store" alter column "default_location_id" TYPE text;` + ) + + this.addSql(`alter table "store" add column "default_region_id" text;`) + this.addSql( + `alter table "store" add column "deleted_at" timestamptz null;` + ) + + this.addSql( + 'create index if not exists "IDX_store_deleted_at" on "store" (deleted_at) where deleted_at is not null;' + ) + + // this.addSql(`alter table "store" drop column "default_currency_code";`) + // this.addSql(`alter table "store" drop column "swap_link_template";`) + // this.addSql(`alter table "store" drop column "payment_link_template";`) + // this.addSql(`alter table "store" drop column "invite_link_template";`) + } else { + this.addSql(`create table if not exists "store" + ("id" text not null, "name" text not null default \'Medusa Store\', + "default_sales_channel_id" text null, "default_region_id" text null, "default_location_id" text null, + "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, + constraint "store_pkey" primary key ("id"));`) + + this.addSql( + 'create index if not exists "IDX_store_deleted_at" on "store" (deleted_at) where deleted_at is not null;' + ) + } + } +} diff --git a/packages/store/src/models/store.ts b/packages/store/src/models/store.ts index 04e8f7d6b4..d3d48ef17f 100644 --- a/packages/store/src/models/store.ts +++ b/packages/store/src/models/store.ts @@ -1,4 +1,10 @@ -import { generateEntityId } from "@medusajs/utils" +import { + DALUtils, + createPsqlIndexStatementHelper, + generateEntityId, +} from "@medusajs/utils" + +import { DAL } from "@medusajs/types" import { BeforeCreate, @@ -6,14 +12,27 @@ import { OnInit, PrimaryKey, Property, + Filter, + OptionalProps, } from "@mikro-orm/core" +type StoreOptionalProps = DAL.SoftDeletableEntityDateColumns + +const StoreDeletedAtIndex = createPsqlIndexStatementHelper({ + tableName: "store", + columns: "deleted_at", + where: "deleted_at IS NOT NULL", +}) + @Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class Store { + [OptionalProps]?: StoreOptionalProps + @PrimaryKey({ columnType: "text" }) id: string - @Property({ columnType: "text" }) + @Property({ columnType: "text", default: "Medusa Store" }) name: string @Property({ columnType: "text", nullable: true }) @@ -35,6 +54,18 @@ export default class Store { }) created_at: Date + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @StoreDeletedAtIndex.MikroORMIndex() + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "store") diff --git a/packages/types/src/store/mutations/store.ts b/packages/types/src/store/mutations/store.ts index dfb47ba985..59b6475252 100644 --- a/packages/types/src/store/mutations/store.ts +++ b/packages/types/src/store/mutations/store.ts @@ -1,5 +1,5 @@ export interface CreateStoreDTO { - name: string + name?: string default_sales_channel_id?: string default_region_id?: string default_location_id?: string diff --git a/packages/types/src/store/service.ts b/packages/types/src/store/service.ts index c2c2ebc973..c7212c9d0d 100644 --- a/packages/types/src/store/service.ts +++ b/packages/types/src/store/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 { FilterableStoreProps, StoreDTO } from "./common" @@ -210,4 +211,42 @@ export interface IStoreModuleService extends IModuleService { config?: FindConfig, sharedContext?: Context ): Promise<[StoreDTO[], number]> + + /** + * This method soft deletes stores by their IDs. + * + * @param {string[]} storeIds - The list of IDs of stores to soft delete. + * @param {SoftDeleteReturn} config - An object that is used to specify an entity's related entities that should be soft-deleted when the main entity is soft-deleted. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise>} Resolves successfully when the stores are soft-deleted. + * + * @example + * {example-code} + */ + softDelete( + storeIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method restores soft deleted stores by their IDs. + * + * @param {string[]} storeIds - The list of IDs of stores to restore. + * @param {RestoreReturn} config - Configurations determining which relations to restore along with each of the stores. You can pass to its `returnLinkableKeys` + * property any of the stores's relation attribute names, such as `currency`. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise>} An object that includes the IDs of related records that were restored, such as the ID of associated currency. + * The object's keys are the ID attribute names of the stores entity's relations, such as `currency_code`, + * and its value is an array of strings, each being the ID of the record associated with the store through this relation, + * such as the IDs of associated currency. + * + * @example + * {example-code} + */ + restore( + storeIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> } diff --git a/yarn.lock b/yarn.lock index 55e7e10386..773632e6a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8752,7 +8752,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/store@workspace:packages/store": +"@medusajs/store@workspace:^, @medusajs/store@workspace:packages/store": version: 0.0.0-use.local resolution: "@medusajs/store@workspace:packages/store" dependencies: @@ -31719,6 +31719,7 @@ __metadata: "@medusajs/product": "workspace:^" "@medusajs/promotion": "workspace:^" "@medusajs/region": "workspace:^" + "@medusajs/store": "workspace:^" "@medusajs/types": "workspace:^" "@medusajs/user": "workspace:^" "@medusajs/utils": "workspace:^"