From 10c67ecd7460fd37da94d8280958abd5c3df02a3 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 29 Jan 2024 15:35:59 +0100 Subject: [PATCH 1/4] chore(core-flows): reorganize folder structure for promotion workflows (#6243) workflows folder structure: ``` - src/ - promotion/ - workflows/ - create-promotion.ts - steps/ - prepare-create-promotion-data.ts ``` RESOLVES CORE-1688 --- packages/core-flows/src/definition/index.ts | 1 - packages/core-flows/src/index.ts | 1 + packages/core-flows/src/promotion/index.ts | 2 ++ .../{handlers/promotion => promotion/steps}/create-campaigns.ts | 0 .../promotion => promotion/steps}/create-promotions.ts | 0 .../{handlers/promotion => promotion/steps}/delete-campaigns.ts | 0 .../promotion => promotion/steps}/delete-promotions.ts | 0 .../src/{definition/promotion => promotion/steps}/index.ts | 0 .../{handlers/promotion => promotion/steps}/update-campaigns.ts | 0 .../promotion => promotion/steps}/update-promotions.ts | 0 .../promotion => promotion/workflows}/create-campaigns.ts | 2 +- .../promotion => promotion/workflows}/create-promotions.ts | 2 +- .../promotion => promotion/workflows}/delete-campaigns.ts | 2 +- .../promotion => promotion/workflows}/delete-promotions.ts | 2 +- .../src/{handlers/promotion => promotion/workflows}/index.ts | 0 .../promotion => promotion/workflows}/update-campaigns.ts | 2 +- .../promotion => promotion/workflows}/update-promotions.ts | 2 +- 17 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 packages/core-flows/src/promotion/index.ts rename packages/core-flows/src/{handlers/promotion => promotion/steps}/create-campaigns.ts (100%) rename packages/core-flows/src/{handlers/promotion => promotion/steps}/create-promotions.ts (100%) rename packages/core-flows/src/{handlers/promotion => promotion/steps}/delete-campaigns.ts (100%) rename packages/core-flows/src/{handlers/promotion => promotion/steps}/delete-promotions.ts (100%) rename packages/core-flows/src/{definition/promotion => promotion/steps}/index.ts (100%) rename packages/core-flows/src/{handlers/promotion => promotion/steps}/update-campaigns.ts (100%) rename packages/core-flows/src/{handlers/promotion => promotion/steps}/update-promotions.ts (100%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/create-campaigns.ts (88%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/create-promotions.ts (88%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/delete-campaigns.ts (85%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/delete-promotions.ts (85%) rename packages/core-flows/src/{handlers/promotion => promotion/workflows}/index.ts (100%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/update-campaigns.ts (88%) rename packages/core-flows/src/{definition/promotion => promotion/workflows}/update-promotions.ts (88%) diff --git a/packages/core-flows/src/definition/index.ts b/packages/core-flows/src/definition/index.ts index 2eef7cc60dd2b..9200339f2bd50 100644 --- a/packages/core-flows/src/definition/index.ts +++ b/packages/core-flows/src/definition/index.ts @@ -2,4 +2,3 @@ export * from "./cart" export * from "./inventory" export * from "./price-list" export * from "./product" -export * from "./promotion" diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index 844b249bef9e7..b6819d03ff15c 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -1,3 +1,4 @@ export * from "./definition" export * from "./definitions" export * as Handlers from "./handlers" +export * from "./promotion" diff --git a/packages/core-flows/src/promotion/index.ts b/packages/core-flows/src/promotion/index.ts new file mode 100644 index 0000000000000..68de82c9f92da --- /dev/null +++ b/packages/core-flows/src/promotion/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/handlers/promotion/create-campaigns.ts b/packages/core-flows/src/promotion/steps/create-campaigns.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/create-campaigns.ts rename to packages/core-flows/src/promotion/steps/create-campaigns.ts diff --git a/packages/core-flows/src/handlers/promotion/create-promotions.ts b/packages/core-flows/src/promotion/steps/create-promotions.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/create-promotions.ts rename to packages/core-flows/src/promotion/steps/create-promotions.ts diff --git a/packages/core-flows/src/handlers/promotion/delete-campaigns.ts b/packages/core-flows/src/promotion/steps/delete-campaigns.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/delete-campaigns.ts rename to packages/core-flows/src/promotion/steps/delete-campaigns.ts diff --git a/packages/core-flows/src/handlers/promotion/delete-promotions.ts b/packages/core-flows/src/promotion/steps/delete-promotions.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/delete-promotions.ts rename to packages/core-flows/src/promotion/steps/delete-promotions.ts diff --git a/packages/core-flows/src/definition/promotion/index.ts b/packages/core-flows/src/promotion/steps/index.ts similarity index 100% rename from packages/core-flows/src/definition/promotion/index.ts rename to packages/core-flows/src/promotion/steps/index.ts diff --git a/packages/core-flows/src/handlers/promotion/update-campaigns.ts b/packages/core-flows/src/promotion/steps/update-campaigns.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/update-campaigns.ts rename to packages/core-flows/src/promotion/steps/update-campaigns.ts diff --git a/packages/core-flows/src/handlers/promotion/update-promotions.ts b/packages/core-flows/src/promotion/steps/update-promotions.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/update-promotions.ts rename to packages/core-flows/src/promotion/steps/update-promotions.ts diff --git a/packages/core-flows/src/definition/promotion/create-campaigns.ts b/packages/core-flows/src/promotion/workflows/create-campaigns.ts similarity index 88% rename from packages/core-flows/src/definition/promotion/create-campaigns.ts rename to packages/core-flows/src/promotion/workflows/create-campaigns.ts index af5bc0cd81722..d3644754471fe 100644 --- a/packages/core-flows/src/definition/promotion/create-campaigns.ts +++ b/packages/core-flows/src/promotion/workflows/create-campaigns.ts @@ -1,6 +1,6 @@ import { CampaignDTO, CreateCampaignDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { createCampaignsStep } from "../../handlers/promotion" +import { createCampaignsStep } from "../steps" type WorkflowInput = { campaignsData: CreateCampaignDTO[] } diff --git a/packages/core-flows/src/definition/promotion/create-promotions.ts b/packages/core-flows/src/promotion/workflows/create-promotions.ts similarity index 88% rename from packages/core-flows/src/definition/promotion/create-promotions.ts rename to packages/core-flows/src/promotion/workflows/create-promotions.ts index 227be13dd0127..a19dce1015f8d 100644 --- a/packages/core-flows/src/definition/promotion/create-promotions.ts +++ b/packages/core-flows/src/promotion/workflows/create-promotions.ts @@ -1,6 +1,6 @@ import { CreatePromotionDTO, PromotionDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { createPromotionsStep } from "../../handlers/promotion" +import { createPromotionsStep } from "../steps" type WorkflowInput = { promotionsData: CreatePromotionDTO[] } diff --git a/packages/core-flows/src/definition/promotion/delete-campaigns.ts b/packages/core-flows/src/promotion/workflows/delete-campaigns.ts similarity index 85% rename from packages/core-flows/src/definition/promotion/delete-campaigns.ts rename to packages/core-flows/src/promotion/workflows/delete-campaigns.ts index 578e95e89bde9..a6c14f4e045d7 100644 --- a/packages/core-flows/src/definition/promotion/delete-campaigns.ts +++ b/packages/core-flows/src/promotion/workflows/delete-campaigns.ts @@ -1,5 +1,5 @@ import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk" -import { deleteCampaignsStep } from "../../handlers/promotion" +import { deleteCampaignsStep } from "../steps" type WorkflowInput = { ids: string[] } diff --git a/packages/core-flows/src/definition/promotion/delete-promotions.ts b/packages/core-flows/src/promotion/workflows/delete-promotions.ts similarity index 85% rename from packages/core-flows/src/definition/promotion/delete-promotions.ts rename to packages/core-flows/src/promotion/workflows/delete-promotions.ts index ab4794d4ac0f5..7223afaa8a919 100644 --- a/packages/core-flows/src/definition/promotion/delete-promotions.ts +++ b/packages/core-flows/src/promotion/workflows/delete-promotions.ts @@ -1,5 +1,5 @@ import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk" -import { deletePromotionsStep } from "../../handlers/promotion" +import { deletePromotionsStep } from "../steps" type WorkflowInput = { ids: string[] } diff --git a/packages/core-flows/src/handlers/promotion/index.ts b/packages/core-flows/src/promotion/workflows/index.ts similarity index 100% rename from packages/core-flows/src/handlers/promotion/index.ts rename to packages/core-flows/src/promotion/workflows/index.ts diff --git a/packages/core-flows/src/definition/promotion/update-campaigns.ts b/packages/core-flows/src/promotion/workflows/update-campaigns.ts similarity index 88% rename from packages/core-flows/src/definition/promotion/update-campaigns.ts rename to packages/core-flows/src/promotion/workflows/update-campaigns.ts index d517c1f0804a4..dac3086857bf5 100644 --- a/packages/core-flows/src/definition/promotion/update-campaigns.ts +++ b/packages/core-flows/src/promotion/workflows/update-campaigns.ts @@ -1,6 +1,6 @@ import { CampaignDTO, UpdateCampaignDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { updateCampaignsStep } from "../../handlers/promotion" +import { updateCampaignsStep } from "../steps" type WorkflowInput = { campaignsData: UpdateCampaignDTO[] } diff --git a/packages/core-flows/src/definition/promotion/update-promotions.ts b/packages/core-flows/src/promotion/workflows/update-promotions.ts similarity index 88% rename from packages/core-flows/src/definition/promotion/update-promotions.ts rename to packages/core-flows/src/promotion/workflows/update-promotions.ts index 157c3ca81ec98..dcd44846cb0f2 100644 --- a/packages/core-flows/src/definition/promotion/update-promotions.ts +++ b/packages/core-flows/src/promotion/workflows/update-promotions.ts @@ -1,6 +1,6 @@ import { PromotionDTO, UpdatePromotionDTO } from "@medusajs/types" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" -import { updatePromotionsStep } from "../../handlers/promotion" +import { updatePromotionsStep } from "../steps" type WorkflowInput = { promotionsData: UpdatePromotionDTO[] } From 8ec093dc071d0f3ea13fdd02dfcb9b3e6899d7cf Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 29 Jan 2024 23:38:31 +0800 Subject: [PATCH 2/4] fix(authentication): remove providers loader (#6257) **What** - remove file leftover from renaming authentication -> auth --- .../authentication/src/loaders/providers.ts | 71 ------------------- 1 file changed, 71 deletions(-) delete mode 100644 packages/authentication/src/loaders/providers.ts diff --git a/packages/authentication/src/loaders/providers.ts b/packages/authentication/src/loaders/providers.ts deleted file mode 100644 index 4f7c11048efcb..0000000000000 --- a/packages/authentication/src/loaders/providers.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as defaultProviders from "@providers" - -import { - asClass, - AwilixContainer, - ClassOrFunctionReturning, - Constructor, - Resolver, -} from "awilix" -import { - AuthModuleProviderConfig, - AuthProviderScope, - LoaderOptions, - ModulesSdkTypes, -} from "@medusajs/types" - -type AuthModuleProviders = { - providers: AuthModuleProviderConfig[] -} - -export default async ({ - container, - options, -}: LoaderOptions< - ( - | ModulesSdkTypes.ModuleServiceInitializeOptions - | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions - ) & - AuthModuleProviders ->): Promise => { - const providerMap = new Map( - options?.providers?.map((provider) => [provider.name, provider.scopes]) ?? - [] - ) - // if(options?.providers?.length) { - // TODO: implement plugin provider registration - // } - - const providersToLoad = Object.values(defaultProviders) - - for (const provider of providersToLoad) { - container.register({ - [`auth_provider_${provider.PROVIDER}`]: asClass( - provider as Constructor - ) - .singleton() - .inject(() => ({ scopes: providerMap.get(provider.PROVIDER) ?? {} })), - }) - } - - container.register({ - [`auth_providers`]: asArray(providersToLoad, providerMap), - }) -} - -function asArray( - resolvers: (ClassOrFunctionReturning | Resolver)[], - providerScopeMap: Map> -): { resolve: (container: AwilixContainer) => unknown[] } { - return { - resolve: (container: AwilixContainer) => - resolvers.map((resolver) => - asClass(resolver as Constructor) - .inject(() => ({ - // @ts-ignore - scopes: providerScopeMap.get(resolver.PROVIDER) ?? {}, - })) - .resolve(container) - ), - } -} From 90cff0777fd351771f3713bf84f4c327c64d276c Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:00:45 +0100 Subject: [PATCH 3/4] fix(medusa): add support for `order` field on GET /admin/orders (#6258) **What** - Adds support for `order` field on `GET /admin/orders` --- .changeset/little-carrots-serve.md | 6 ++++++ .../api/__tests__/admin/order/order.js | 17 +++++++++++++++++ .../src/lib/models/AdminGetOrdersParams.ts | 4 ++++ .../src/api/routes/admin/orders/list-orders.ts | 8 ++++++++ 4 files changed, 35 insertions(+) create mode 100644 .changeset/little-carrots-serve.md diff --git a/.changeset/little-carrots-serve.md b/.changeset/little-carrots-serve.md new file mode 100644 index 0000000000000..d3dfa094f025d --- /dev/null +++ b/.changeset/little-carrots-serve.md @@ -0,0 +1,6 @@ +--- +"@medusajs/client-types": patch +"@medusajs/medusa": patch +--- + +fix(medusa): Adds support for ordering GET /admin/orders diff --git a/integration-tests/api/__tests__/admin/order/order.js b/integration-tests/api/__tests__/admin/order/order.js index 6975ae5c52774..bf4502d0145e8 100644 --- a/integration-tests/api/__tests__/admin/order/order.js +++ b/integration-tests/api/__tests__/admin/order/order.js @@ -78,6 +78,23 @@ describe("/admin/orders", () => { }) expect(response.status).toEqual(200) }) + + it("gets orders ordered by display_id", async () => { + const api = useApi() + + const response = await api + .get("/admin/orders?order=display_id", adminReqConfig) + .catch((err) => { + console.log(err) + }) + expect(response.status).toEqual(200) + + const sortedOrders = response.data.orders.sort((a, b) => { + return a.display_id - b.display_id + }) + + expect(response.data.orders).toEqual(sortedOrders) + }) }) describe("POST /admin/orders/:id", () => { diff --git a/packages/generated/client-types/src/lib/models/AdminGetOrdersParams.ts b/packages/generated/client-types/src/lib/models/AdminGetOrdersParams.ts index 93261dcd51be2..b7c70eb467a38 100644 --- a/packages/generated/client-types/src/lib/models/AdminGetOrdersParams.ts +++ b/packages/generated/client-types/src/lib/models/AdminGetOrdersParams.ts @@ -155,4 +155,8 @@ export interface AdminGetOrdersParams { * Comma-separated fields that should be included in the returned order. */ fields?: string + /** + * A order field to sort-order the retrieved orders by. + */ + order?: string } diff --git a/packages/medusa/src/api/routes/admin/orders/list-orders.ts b/packages/medusa/src/api/routes/admin/orders/list-orders.ts index af7cb3860caa6..684365eb03aad 100644 --- a/packages/medusa/src/api/routes/admin/orders/list-orders.ts +++ b/packages/medusa/src/api/routes/admin/orders/list-orders.ts @@ -152,6 +152,7 @@ import { cleanResponseData } from "../../../../utils/clean-response-data" * - (query) limit=50 {integer} Limit the number of orders returned. * - (query) expand {string} Comma-separated relations that should be expanded in the returned order. * - (query) fields {string} Comma-separated fields that should be included in the returned order. + * - (query) order {string} A order field to sort-order the retrieved orders by. * x-codegen: * method: list * queryParams: AdminGetOrdersParams @@ -277,4 +278,11 @@ export class AdminGetOrdersParams extends AdminListOrdersSelector { @IsString() @IsOptional() fields?: string + + /** + * The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`. + */ + @IsOptional() + @IsString() + order?: string } From 87390704be5f3e82be08e1d8665102c29d357910 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Mon, 29 Jan 2024 17:56:39 +0100 Subject: [PATCH 4/4] feat(customer): list customer (#6206) **What** - GET /admin/customers - GET /admin/customer-groups --- .../admin/list-customer-groups.spec.ts | 63 ++++++++ .../customer/admin/list-customers.spec.ts | 119 ++++++++++++++ integration-tests/plugins/medusa-config.js | 5 + integration-tests/plugins/package.json | 1 + packages/customer/tsconfig.json | 2 +- .../admin/customer-groups/middlewares.ts | 52 +++++++ .../admin/customer-groups/query-config.ts | 21 +++ .../src/api-v2/admin/customer-groups/route.ts | 24 +++ .../admin/customer-groups/validators.ts | 114 ++++++++++++++ .../src/api-v2/admin/customers/middlewares.ts | 46 ++++++ .../api-v2/admin/customers/query-config.ts | 29 ++++ .../src/api-v2/admin/customers/route.ts | 41 +++++ .../src/api-v2/admin/customers/validators.ts | 146 ++++++++++++++++++ packages/medusa/src/api-v2/middlewares.ts | 4 + .../src/types/validators/operator-map.ts | 67 ++++++++ packages/types/src/dal/index.ts | 4 +- yarn.lock | 3 +- 17 files changed, 737 insertions(+), 4 deletions(-) create mode 100644 integration-tests/plugins/__tests__/customer/admin/list-customer-groups.spec.ts create mode 100644 integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts create mode 100644 packages/medusa/src/api-v2/admin/customer-groups/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/customer-groups/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/customer-groups/route.ts create mode 100644 packages/medusa/src/api-v2/admin/customer-groups/validators.ts create mode 100644 packages/medusa/src/api-v2/admin/customers/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/customers/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/customers/route.ts create mode 100644 packages/medusa/src/api-v2/admin/customers/validators.ts create mode 100644 packages/medusa/src/types/validators/operator-map.ts diff --git a/integration-tests/plugins/__tests__/customer/admin/list-customer-groups.spec.ts b/integration-tests/plugins/__tests__/customer/admin/list-customer-groups.spec.ts new file mode 100644 index 0000000000000..538caa7cbf5e7 --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/list-customer-groups.spec.ts @@ -0,0 +1,63 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/customer-groups", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + 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 get all customer groups and its count", async () => { + await customerModuleService.createCustomerGroup({ + name: "Test", + }) + + const api = useApi() as any + const response = await api.get(`/admin/customer-groups`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.groups).toEqual([ + expect.objectContaining({ + id: expect.any(String), + name: "Test", + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts b/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts new file mode 100644 index 0000000000000..a5c0c47d541aa --- /dev/null +++ b/integration-tests/plugins/__tests__/customer/admin/list-customers.spec.ts @@ -0,0 +1,119 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } 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" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/customers", () => { + let dbConnection + let appContainer + let shutdownServer + let customerModuleService: ICustomerModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + }) + + 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 get all customers and its count", async () => { + await customerModuleService.create([ + { + first_name: "Test", + last_name: "Test", + email: "test@me.com", + }, + ]) + + const api = useApi() as any + const response = await api.get(`/admin/customers`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customers).toEqual([ + expect.objectContaining({ + id: expect.any(String), + first_name: "Test", + last_name: "Test", + email: "test@me.com", + }), + ]) + }) + + it("should filter customers by last name", async () => { + await customerModuleService.create([ + { + first_name: "Jane", + last_name: "Doe", + email: "jane@me.com", + }, + { + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }, + { + first_name: "LeBron", + last_name: "James", + email: "lebron@me.com", + }, + { + first_name: "John", + last_name: "Silver", + email: "johns@me.com", + }, + ]) + + const api = useApi() as any + const response = await api.get( + `/admin/customers?last_name=Doe`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.customers).toContainEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "Jane", + last_name: "Doe", + email: "jane@me.com", + }) + ) + expect(response.data.customers).toContainEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "John", + last_name: "Doe", + email: "john@me.com", + }) + ) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index 843f7264edfed..2088ea4544e5c 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -71,6 +71,11 @@ module.exports = { resources: "shared", resolve: "@medusajs/promotion", }, + [Modules.CUSTOMER]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/customer", + }, [Modules.SALES_CHANNEL]: { scope: "internal", resources: "shared", diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index b2b3a47a4b93e..74a9ec209c1f1 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@medusajs/cache-inmemory": "workspace:*", + "@medusajs/customer": "workspace:^", "@medusajs/event-bus-local": "workspace:*", "@medusajs/inventory": "workspace:^", "@medusajs/medusa": "workspace:*", diff --git a/packages/customer/tsconfig.json b/packages/customer/tsconfig.json index 6143fb1ef307f..4b79cd603235c 100644 --- a/packages/customer/tsconfig.json +++ b/packages/customer/tsconfig.json @@ -23,7 +23,7 @@ "@models": ["./src/models"], "@services": ["./src/services"], "@repositories": ["./src/repositories"], - "@types": ["./src/types"], + "@types": ["./src/types"] } }, "include": ["src"], diff --git a/packages/medusa/src/api-v2/admin/customer-groups/middlewares.ts b/packages/medusa/src/api-v2/admin/customer-groups/middlewares.ts new file mode 100644 index 0000000000000..d1ca416a9fc5b --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customer-groups/middlewares.ts @@ -0,0 +1,52 @@ +import { MedusaV2Flag } from "@medusajs/utils" + +import { + isFeatureFlagEnabled, + transformBody, + transformQuery, +} from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetCustomerGroupsParams, + AdminGetCustomerGroupsGroupParams, + AdminPostCustomerGroupsReq, + AdminPostCustomerGroupsGroupReq, +} from "./validators" + +export const adminCustomerGroupRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/customer-groups*", + middlewares: [isFeatureFlagEnabled(MedusaV2Flag.key)], + }, + { + method: ["GET"], + matcher: "/admin/customer-groups", + middlewares: [ + transformQuery( + AdminGetCustomerGroupsParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/customer-groups/:id", + middlewares: [ + transformQuery( + AdminGetCustomerGroupsGroupParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/customer-groups", + middlewares: [transformBody(AdminPostCustomerGroupsReq)], + }, + { + method: ["POST"], + matcher: "/admin/customer-groups/:id", + middlewares: [transformBody(AdminPostCustomerGroupsGroupReq)], + }, +] diff --git a/packages/medusa/src/api-v2/admin/customer-groups/query-config.ts b/packages/medusa/src/api-v2/admin/customer-groups/query-config.ts new file mode 100644 index 0000000000000..db9aac969824b --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customer-groups/query-config.ts @@ -0,0 +1,21 @@ +export const defaultAdminCustomerGroupRelations = [] +export const allowedAdminCustomerGroupRelations = ["customers"] +export const defaultAdminCustomerGroupFields = [ + "id", + "name", + "created_at", + "updated_at", + "deleted_at", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminCustomerGroupFields, + defaultRelations: defaultAdminCustomerGroupRelations, + allowedRelations: allowedAdminCustomerGroupRelations, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/customer-groups/route.ts b/packages/medusa/src/api-v2/admin/customer-groups/route.ts new file mode 100644 index 0000000000000..03cbdd4728e6c --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customer-groups/route.ts @@ -0,0 +1,24 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const customerModuleService = req.scope.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const [groups, count] = + await customerModuleService.listAndCountCustomerGroups( + req.filterableFields, + req.listConfig + ) + + const { offset, limit } = req.validatedQuery + + res.json({ + count, + groups, + offset, + limit, + }) +} diff --git a/packages/medusa/src/api-v2/admin/customer-groups/validators.ts b/packages/medusa/src/api-v2/admin/customer-groups/validators.ts new file mode 100644 index 0000000000000..a0f5a24283262 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customer-groups/validators.ts @@ -0,0 +1,114 @@ +import { OperatorMap } from "@medusajs/types" +import { Type } from "class-transformer" +import { + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" +import { OperatorMapValidator } from "../../../types/validators/operator-map" + +export class AdminGetCustomerGroupsGroupParams extends FindParams {} + +class FilterableCustomerPropsValidator { + @IsOptional() + @IsString({ each: true }) + id?: string | string[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => OperatorMapValidator) + email?: string | string[] | OperatorMap + + @IsOptional() + @IsString({ each: true }) + default_billing_address_id?: string | string[] | null + + @IsOptional() + @IsString({ each: true }) + default_shipping_address_id?: string | string[] | null + + @IsOptional() + @IsString({ each: true }) + company_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + first_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + last_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + created_by?: string | string[] | null + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap +} + +export class AdminGetCustomerGroupsParams extends extendedFindParamsMixin({ + limit: 100, + offset: 0, +}) { + @IsOptional() + @IsString({ each: true }) + id?: string | string[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => OperatorMapValidator) + name?: string | OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => FilterableCustomerPropsValidator) + customers?: FilterableCustomerPropsValidator | string | string[] + + @IsOptional() + @IsString({ each: true }) + created_by?: string | string[] | null + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap + + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetCustomerGroupsParams) + $and?: AdminGetCustomerGroupsParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetCustomerGroupsParams) + $or?: AdminGetCustomerGroupsParams[] +} + +export class AdminPostCustomerGroupsReq { + @IsNotEmpty() + @IsString() + name: string +} + +export class AdminPostCustomerGroupsGroupReq { + @IsNotEmpty() + @IsString() + @IsOptional() + name?: string +} diff --git a/packages/medusa/src/api-v2/admin/customers/middlewares.ts b/packages/medusa/src/api-v2/admin/customers/middlewares.ts new file mode 100644 index 0000000000000..962b1a4529ccf --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/middlewares.ts @@ -0,0 +1,46 @@ +import { MedusaV2Flag } from "@medusajs/utils" + +import { + isFeatureFlagEnabled, + transformBody, + transformQuery, +} from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetCustomersParams, + AdminGetCustomersCustomerParams, + AdminPostCustomersCustomerReq, +} from "./validators" + +export const adminCustomerRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/customers*", + middlewares: [isFeatureFlagEnabled(MedusaV2Flag.key)], + }, + { + method: ["GET"], + matcher: "/admin/customers", + middlewares: [ + transformQuery( + AdminGetCustomersParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/customers/:id", + middlewares: [ + transformQuery( + AdminGetCustomersCustomerParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/customers/:id", + middlewares: [transformBody(AdminPostCustomersCustomerReq)], + }, +] diff --git a/packages/medusa/src/api-v2/admin/customers/query-config.ts b/packages/medusa/src/api-v2/admin/customers/query-config.ts new file mode 100644 index 0000000000000..3702bea7656db --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/query-config.ts @@ -0,0 +1,29 @@ +export const defaultAdminCustomerRelations = [] +export const allowedAdminCustomerRelations = [ + "groups", + "default_shipping_address", + "default_billing_address", + "addresses", +] +export const defaultAdminCustomerFields = [ + "id", + "company_name", + "first_name", + "last_name", + "email", + "created_at", + "updated_at", + "deleted_at", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminCustomerFields, + defaultRelations: defaultAdminCustomerRelations, + allowedRelations: allowedAdminCustomerRelations, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/customers/route.ts b/packages/medusa/src/api-v2/admin/customers/route.ts new file mode 100644 index 0000000000000..8be7d305362b7 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/route.ts @@ -0,0 +1,41 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICustomerModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const customerModuleService = req.scope.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const [customers, count] = await customerModuleService.listAndCount( + req.filterableFields, + req.listConfig + ) + + const { offset, limit } = req.validatedQuery + + // TODO: Replace with remote query + //const remoteQuery = req.scope.resolve("remoteQuery") + + //const variables = { + // filters: req.filterableFields, + // order: req.listConfig.order, + // skip: req.listConfig.skip, + // take: req.listConfig.take, + //} + + //const query = remoteQueryObjectFromString({ + // entryPoint: "customer", + // variables, + // fields: [...req.listConfig.select!, ...req.listConfig.relations!], + //}) + + //const results = await remoteQuery(query) + + res.json({ + count, + customers, + offset, + limit, + }) +} diff --git a/packages/medusa/src/api-v2/admin/customers/validators.ts b/packages/medusa/src/api-v2/admin/customers/validators.ts new file mode 100644 index 0000000000000..4f1fda5ca0816 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/customers/validators.ts @@ -0,0 +1,146 @@ +import { OperatorMap } from "@medusajs/types" +import { Transform, Type } from "class-transformer" +import { + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" +import { OperatorMapValidator } from "../../../types/validators/operator-map" +import { IsType } from "../../../utils" + +export class AdminGetCustomersCustomerParams extends FindParams {} + +export class AdminGetCustomersParams extends extendedFindParamsMixin({ + limit: 100, + offset: 0, +}) { + @IsOptional() + @IsString({ each: true }) + id?: string | string[] + + @IsOptional() + @IsType([String, [String], OperatorMapValidator]) + email?: string | string[] | OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => FilterableCustomerGroupPropsValidator) + groups?: FilterableCustomerGroupPropsValidator | string | string[] + + @IsOptional() + @IsString({ each: true }) + default_billing_address_id?: string | string[] | null + + @IsOptional() + @IsString({ each: true }) + default_shipping_address_id?: string | string[] | null + + @IsOptional() + @IsString({ each: true }) + company_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + first_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsType([String, [String], OperatorMapValidator]) + @Transform(({ value }) => (value === "null" ? null : value)) + last_name?: string | string[] | OperatorMap | null + + @IsOptional() + @IsString({ each: true }) + created_by?: string | string[] | null + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap + + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetCustomersParams) + $and?: AdminGetCustomersParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetCustomersParams) + $or?: AdminGetCustomersParams[] +} + +class FilterableCustomerGroupPropsValidator { + @IsOptional() + @IsString({ each: true }) + id?: string | string[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => OperatorMapValidator) + name?: string | OperatorMap + + @IsOptional() + @IsString({ each: true }) + created_by?: string | string[] | null + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap +} + +export class AdminPostCustomersReq { + @IsNotEmpty() + @IsString() + @IsOptional() + company_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + first_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + last_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + email?: string +} + +export class AdminPostCustomersCustomerReq { + @IsNotEmpty() + @IsString() + @IsOptional() + company_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + first_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + last_name?: string + + @IsNotEmpty() + @IsString() + @IsOptional() + email?: string +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 9c6ec41da5857..59b5884fed05b 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,9 +1,13 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" +import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" +import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" export const config: MiddlewaresConfig = { routes: [ + ...adminCustomerGroupRoutesMiddlewares, + ...adminCustomerRoutesMiddlewares, ...adminPromotionRoutesMiddlewares, ...adminCampaignRoutesMiddlewares, ], diff --git a/packages/medusa/src/types/validators/operator-map.ts b/packages/medusa/src/types/validators/operator-map.ts new file mode 100644 index 0000000000000..82f50cfc73e11 --- /dev/null +++ b/packages/medusa/src/types/validators/operator-map.ts @@ -0,0 +1,67 @@ +import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator" + +export class OperatorMapValidator { + @IsOptional() + @IsString() + $eq?: string | string[] + + @IsOptional() + @IsString() + $ne?: string + + @IsOptional() + @IsArray() + $in?: string[] + + @IsOptional() + @IsArray() + $nin?: string[] + + @IsOptional() + @IsString() + $like?: string + + @IsOptional() + @IsString() + $re?: string + + @IsOptional() + @IsString() + $ilike?: string + + @IsOptional() + @IsString() + $fulltext?: string + + @IsOptional() + @IsArray() + $overlap?: string[] + + @IsOptional() + @IsString() + $contains?: string + + @IsOptional() + @IsArray() + $contained?: string[] + + @IsOptional() + @IsBoolean() + $exists?: boolean + + @IsOptional() + @IsString() + $gt?: string + + @IsOptional() + @IsString() + $gte?: string + + @IsOptional() + @IsString() + $lt?: string + + @IsOptional() + @IsString() + $lte?: string +} diff --git a/packages/types/src/dal/index.ts b/packages/types/src/dal/index.ts index 32531476c647c..341a50dbee5fa 100644 --- a/packages/types/src/dal/index.ts +++ b/packages/types/src/dal/index.ts @@ -1,10 +1,10 @@ import { Dictionary, FilterQuery, Order } from "./utils" -export { FilterQuery } from "./utils" +export { FilterQuery, OperatorMap } from "./utils" /** * @interface - * + * * An object used to allow specifying flexible queries with and/or conditions. */ export interface BaseFilterable { diff --git a/yarn.lock b/yarn.lock index 575604621c425..7a53985a90a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8009,7 +8009,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/customer@workspace:packages/customer": +"@medusajs/customer@workspace:^, @medusajs/customer@workspace:packages/customer": version: 0.0.0-use.local resolution: "@medusajs/customer@workspace:packages/customer" dependencies: @@ -31378,6 +31378,7 @@ __metadata: "@babel/core": ^7.12.10 "@babel/node": ^7.12.10 "@medusajs/cache-inmemory": "workspace:*" + "@medusajs/customer": "workspace:^" "@medusajs/event-bus-local": "workspace:*" "@medusajs/inventory": "workspace:^" "@medusajs/medusa": "workspace:*"