diff --git a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap index ac4ab2e39e..38b5e2546c 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap @@ -1,5 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`sales channels GET /admin/sales-channels should list the sales channel 1`] = ` +Object { + "count": 2, + "limit": 20, + "offset": 0, + "sales_channels": ArrayContaining [ + Object { + "created_at": Any, + "deleted_at": null, + "description": "test description", + "id": Any, + "is_disabled": false, + "name": "test name", + "updated_at": Any, + }, + Object { + "created_at": Any, + "deleted_at": null, + "description": "test description 2", + "id": Any, + "is_disabled": false, + "name": "test name 2", + "updated_at": Any, + }, + ], +} +`; + +exports[`sales channels GET /admin/sales-channels should list the sales channel using free text search 1`] = ` +Object { + "count": 1, + "limit": 20, + "offset": 0, + "sales_channels": ArrayContaining [ + Object { + "created_at": Any, + "deleted_at": null, + "description": "test description 2", + "id": Any, + "is_disabled": false, + "name": "test name 2", + "updated_at": Any, + }, + ], +} +`; + +exports[`sales channels GET /admin/sales-channels should list the sales channel using properties filters 1`] = ` +Object { + "count": 1, + "limit": 20, + "offset": 0, + "sales_channels": ArrayContaining [ + Object { + "created_at": Any, + "deleted_at": null, + "description": "test description", + "id": Any, + "is_disabled": false, + "name": "test name", + "updated_at": Any, + }, + ], +} +`; + exports[`sales channels DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = ` Object { "deleted": true, diff --git a/integration-tests/api/__tests__/admin/order.js b/integration-tests/api/__tests__/admin/order.js index 93727e05d8..a90b607546 100644 --- a/integration-tests/api/__tests__/admin/order.js +++ b/integration-tests/api/__tests__/admin/order.js @@ -1521,7 +1521,7 @@ describe("/admin/orders", () => { expect(response.status).toEqual(200) expect(response.data.count).toEqual(2) - expect(response.data.orders).toEqual([ + expect(response.data.orders).toEqual(expect.arrayContaining([ expect.objectContaining({ id: "test-order", shipping_address: expect.objectContaining({ first_name: "lebron" }), @@ -1530,7 +1530,7 @@ describe("/admin/orders", () => { id: "discount-order", shipping_address: expect.objectContaining({ first_name: "lebron" }), }), - ]) + ])) }) it("successfully lists orders with greater than", async () => { diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index df6dcc5551..a95cf9ba1d 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -86,6 +86,124 @@ describe("sales channels", () => { }) }) + describe("GET /admin/sales-channels", () => { + let salesChannel1, salesChannel2 + + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + salesChannel1 = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + salesChannel2 = await simpleSalesChannelFactory(dbConnection, { + name: "test name 2", + description: "test description 2", + }) + } catch (e) { + console.error(e) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should list the sales channel", async () => { + const api = useApi() + const response = await api.get( + `/admin/sales-channels/`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe(2) + expect(response.data).toMatchSnapshot({ + count: 2, + limit: 20, + offset: 0, + sales_channels: expect.arrayContaining([ + { + id: expect.any(String), + name: salesChannel1.name, + description: salesChannel1.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + { + id: expect.any(String), + name: salesChannel2.name, + description: salesChannel2.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ]) + }) + }) + + it("should list the sales channel using free text search", async () => { + const api = useApi() + const response = await api.get( + `/admin/sales-channels/?q=2`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe(1) + expect(response.data).toMatchSnapshot({ + count: 1, + limit: 20, + offset: 0, + sales_channels: expect.arrayContaining([ + { + id: expect.any(String), + name: salesChannel2.name, + description: salesChannel2.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ]) + }) + }) + + it("should list the sales channel using properties filters", async () => { + const api = useApi() + const response = await api.get( + `/admin/sales-channels/?name=test+name`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe(1) + expect(response.data).toMatchSnapshot({ + count: 1, + limit: 20, + offset: 0, + sales_channels: expect.arrayContaining([ + { + id: expect.any(String), + name: salesChannel1.name, + description: salesChannel1.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ]) + }) + }) + }) + describe("POST /admin/sales-channels/:id", () => { let sc diff --git a/packages/medusa-js/src/resources/admin/sales-channels.ts b/packages/medusa-js/src/resources/admin/sales-channels.ts index 81f98cff3b..bfc735e62d 100644 --- a/packages/medusa-js/src/resources/admin/sales-channels.ts +++ b/packages/medusa-js/src/resources/admin/sales-channels.ts @@ -1,11 +1,14 @@ import { + AdminGetSalesChannelsParams, AdminPostSalesChannelsReq, AdminSalesChannelsRes, AdminPostSalesChannelsSalesChannelReq, AdminSalesChannelsDeleteRes, + AdminSalesChannelsListRes, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" +import qs from "qs" class AdminSalesChannelsResource extends BaseResource { /** retrieve a sales channel @@ -49,11 +52,26 @@ class AdminSalesChannelsResource extends BaseResource { return this.client.request("POST", path, payload, {}, customHeaders) } - /* list( - query?: any, + /** + * Retrieve a list of sales channels + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * @description Retrieve a list of sales channels + * @returns the list of sales channel as well as the pagination properties + */ + list( + query?: AdminGetSalesChannelsParams, customHeaders: Record = {} - ): ResponsePromise { - }*/ + ): ResponsePromise { + let path = `/admin/sales-channels` + + if (query) { + const queryString = qs.stringify(query) + path += `?${queryString}` + } + + return this.client.request("GET", path, {}, {}, customHeaders) + } /** * Delete a sales channel diff --git a/packages/medusa-react/mocks/data/fixtures.json b/packages/medusa-react/mocks/data/fixtures.json index 950da43985..ce795f566d 100644 --- a/packages/medusa-react/mocks/data/fixtures.json +++ b/packages/medusa-react/mocks/data/fixtures.json @@ -1270,6 +1270,15 @@ "updated_at": "2022-07-05T15:16:01.959Z", "created_at": "2022-07-05T15:16:01.959Z", "deleted_at": null - } + }, + "sales_channels": [{ + "id": "sc_01F0YES4R67TXXC1QBQ8P54A8Y", + "name": "sales channel 1 name", + "description": "sales channel 1 description", + "is_disabled": false, + "updated_at": "2022-07-05T15:16:01.959Z", + "created_at": "2022-07-05T15:16:01.959Z", + "deleted_at": null + }] } } diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index b7521cf37d..38a5b7ce6f 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1682,6 +1682,18 @@ export const adminHandlers = [ ) }), + rest.get("/admin/sales-channels", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + count: 1, + limit: 20, + offset: 20, + sales_channels: fixtures.get("sales_channels"), + }) + ) + }), + rest.post("/admin/sales-channels/:id", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts b/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts index c6a1f7e28c..22d2100e75 100644 --- a/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts +++ b/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts @@ -1,4 +1,8 @@ -import { AdminSalesChannelsRes } from "@medusajs/medusa" +import { + AdminSalesChannelsRes, + AdminSalesChannelsListRes, + AdminGetSalesChannelsParams, +} from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useQuery } from "react-query" import { useMedusa } from "../../../contexts" @@ -36,21 +40,26 @@ export const useAdminSalesChannel = ( return { ...data, ...rest } as const } -/* +/** + * retrieve a list of sales channels + * @experimental This feature is under development and may change in the future. + * To use this feature please enable feature flag `sales_channels` in your medusa backend project. + * @description Retrieve a list of sales channel + * @returns a list of sales channel as well as the pagination properties + */ export const useAdminSalesChannels = ( query?: AdminGetSalesChannelsParams, options?: UseQueryOptionsWrapper< - Response, + Response, Error, ReturnType > ) => { const { client } = useMedusa() const { data, ...rest } = useQuery( - adminProductKeys.list(query), + adminSalesChannelsKeys.list(query), () => client.admin.salesChannels.list(query), options ) return { ...data, ...rest } as const } -*/ diff --git a/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts b/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts index 35bc5b29f8..e4ae746327 100644 --- a/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts +++ b/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts @@ -1,10 +1,10 @@ -import { useAdminSalesChannel } from "../../../../src" +import { useAdminSalesChannel, useAdminSalesChannels } from "../../../../src" import { renderHook } from "@testing-library/react-hooks" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" describe("useAdminSalesChannel hook", () => { - test("returns a product", async () => { + test("returns a sales channel", async () => { const salesChannel = fixtures.get("sales_channel") const { result, waitFor } = renderHook( () => useAdminSalesChannel(salesChannel.id), @@ -19,3 +19,17 @@ describe("useAdminSalesChannel hook", () => { expect(result.current.sales_channel).toEqual(salesChannel) }) }) + +describe("useAdminSalesChannels hook", () => { + test("returns a list of sales channels", async () => { + const salesChannels = fixtures.get("sales_channels") + const { result, waitFor } = renderHook(() => useAdminSalesChannels(), { + wrapper: createWrapper(), + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.sales_channels).toEqual(salesChannels) + }) +}) diff --git a/packages/medusa/src/api/middlewares/transform-query.ts b/packages/medusa/src/api/middlewares/transform-query.ts index 142c2f55d7..ce134d847a 100644 --- a/packages/medusa/src/api/middlewares/transform-query.ts +++ b/packages/medusa/src/api/middlewares/transform-query.ts @@ -7,9 +7,10 @@ import { prepareListQuery, prepareRetrieveQuery, } from "../../utils/get-query-config" -import { BaseEntity } from "../../interfaces/models/base-entity" +import { BaseEntity } from "../../interfaces" import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common" import { omit } from "lodash" +import { removeUndefinedProperties } from "../../utils" export function transformQuery< T extends RequestQueryFields, @@ -36,6 +37,7 @@ export function transformQuery< "fields", "order", ]) + req.filterableFields = removeUndefinedProperties(req.filterableFields) if (queryConfig?.isList) { req.listConfig = prepareListQuery( diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts index 437119ffc5..c45804f689 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts @@ -1,11 +1,7 @@ import { Type } from "class-transformer" import { IsNumber, IsOptional, IsString } from "class-validator" -import omit from "lodash/omit" -import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" -import { FindConfig } from "../../../../types/common" import { FilterablePriceListProps } from "../../../../types/price-list" -import { validator } from "../../../../utils/validator" import { Request } from "express" /** * @oas [get] /price-lists diff --git a/packages/medusa/src/api/routes/admin/sales-channels/__tests__/list-sales-channels.js b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/list-sales-channels.js new file mode 100644 index 0000000000..4b5fdcb589 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/list-sales-channels.js @@ -0,0 +1,55 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel" + +describe("GET /admin/sales-channels/", () => { + describe("successfully list the sales channel", () => { + let subject + + beforeAll(async () => { + subject = await request( + "GET", + `/admin/sales-channels`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: ["sales_channels"], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls the listAndCount method from the sales channel service", () => { + expect(SalesChannelServiceMock.listAndCount).toHaveBeenCalledTimes(1) + expect(SalesChannelServiceMock.listAndCount).toHaveBeenCalledWith( + {}, + { + order: { created_at: "DESC" }, + relations: [], + skip: 0, + take: 20 + } + ) + }) + + it("returns the expected sales channel", () => { + expect(subject.body).toEqual({ + sales_channels: [{ + id: IdMap.getId("sales_channel_1"), + name: "sales channel 1 name", + description: "sales channel 1 description", + is_disabled: false, + }], + offset: 0, + limit: 20, + count: 1, + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts b/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts index dd18fb9c9f..c8220f94d7 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express" -import { IsObject, IsOptional, IsString } from "class-validator" +import { IsOptional, IsString } from "class-validator" import SalesChannelService from "../../../../services/sales-channel" import { CreateSalesChannelInput } from "../../../../types/sales-channels" @@ -14,7 +14,7 @@ import { CreateSalesChannelInput } from "../../../../types/sales-channels" * - (body) name=* {string} Name of the sales channel * - (body) description=* {string} Description of the sales channel * tags: - * - Sales Channels + * - Sales Channel * responses: * 200: * description: OK diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index ba880949ad..d82c2d2420 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -3,15 +3,27 @@ import { DeleteResponse, PaginatedResponse } from "../../../../types/common" import "reflect-metadata" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import { SalesChannel } from "../../../../models" -import middlewares, { transformBody } from "../../../middlewares" +import middlewares, { + transformBody, + transformQuery, +} from "../../../middlewares" import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel" import { AdminPostSalesChannelsReq } from "./create-sales-channel" +import { AdminGetSalesChannelsParams } from "./list-sales-channels" const route = Router() export default (app) => { app.use("/sales-channels", isFeatureFlagEnabled("sales_channels"), route) + route.get( + "/", + transformQuery(AdminGetSalesChannelsParams, { + isList: true, + }), + middlewares.wrap(require("./list-sales-channels").default) + ) + const salesChannelRouter = Router({ mergeParams: true }) route.use("/:id", salesChannelRouter) @@ -23,8 +35,11 @@ export default (app) => { "/", middlewares.wrap(require("./delete-sales-channel").default) ) - - route.get("/", (req, res) => {}) + salesChannelRouter.post( + "/", + transformBody(AdminPostSalesChannelsSalesChannelReq), + middlewares.wrap(require("./update-sales-channel").default) + ) route.post( "/", @@ -38,8 +53,6 @@ export default (app) => { middlewares.wrap(require("./update-sales-channel").default) ) - - return app } @@ -53,9 +66,12 @@ export type AdminSalesChannelListRes = PaginatedResponse & { sales_channels: SalesChannel[] } +export type AdminSalesChannelsListRes = PaginatedResponse & { + sales_channels: SalesChannel[] +} + export * from "./get-sales-channel" export * from "./create-sales-channel" -// export * from './' -// export * from './' +export * from "./list-sales-channels" export * from "./update-sales-channel" -// export * from './' +export * from "./delete-sales-channel" diff --git a/packages/medusa/src/api/routes/admin/sales-channels/list-sales-channels.ts b/packages/medusa/src/api/routes/admin/sales-channels/list-sales-channels.ts new file mode 100644 index 0000000000..ef776f2f1a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/list-sales-channels.ts @@ -0,0 +1,109 @@ +import { Request, Response } from "express" +import { IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" +import { Type } from "class-transformer" +import { removeUndefinedProperties } from "../../../../utils" +import { SalesChannelService } from "../../../../services" +import { + DateComparisonOperator, + extendedFindParamsMixin, +} from "../../../../types/common" + +/** + * @oas [get] /sales-channels + * operationId: "GetSalesChannels" + * summary: "List sales channels" + * description: "Retrieves a list of sales channels" + * x-authenticated: true + * parameters: + * - (query) id {string} id of the sales channel + * - (query) name {string} Name of the sales channel + * - (query) description {string} Description of the sales channel + * - (query) q {string} Query used for searching sales channels. + * - (query) order {string} to retrieve sales channels in. + * - (query) deleted_at {DateComparisonOperator} Date comparison for when resulting sales channels was deleted, i.e. less than, greater than etc. + * - (query) created_at {DateComparisonOperator} Date comparison for when resulting sales channels was created, i.e. less than, greater than etc. + * - (query) updated_at {DateComparisonOperator} Date comparison for when resulting sales channels was updated, i.e. less than, greater than etc. + * - (query) offset {string} How many sales channels to skip in the result. + * - (query) limit {string} Limit the number of sales channels returned. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each sales channel of the result. + * - (query) fields {string} (Comma separated) Which fields should be included in each sales channel of the result. + * tags: + * - Sales Channel + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * count: + * description: The number of Sales channels. + * type: integer + * offset: + * description: The offset of the Sales channel query. + * type: integer + * limit: + * description: The limit of the Sales channel query. + * type: integer + * sales_channels: + * type: array + * items: + * $ref: "#/components/schemas/sales_channel" + */ +export default async (req: Request, res: Response) => { + const salesChannelService: SalesChannelService = req.scope.resolve( + "salesChannelService" + ) + + const listConfig = req.listConfig + const filterableFields = req.filterableFields + + const [salesChannels, count] = await salesChannelService.listAndCount( + filterableFields, + listConfig + ) + + res.status(200).json({ + sales_channels: salesChannels, + count, + offset: listConfig.skip, + limit: listConfig.take, + }) +} + +export class AdminGetSalesChannelsParams extends extendedFindParamsMixin() { + @IsString() + @IsOptional() + id?: string + + @IsOptional() + @IsString() + q?: string + + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @IsString() + description?: string + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + @ValidateNested() + @IsOptional() + @Type(() => DateComparisonOperator) + deleted_at?: DateComparisonOperator + + @IsString() + @IsOptional() + order?: string +} diff --git a/packages/medusa/src/repositories/sales-channel.ts b/packages/medusa/src/repositories/sales-channel.ts index 08e5d4d682..d24e3644e4 100644 --- a/packages/medusa/src/repositories/sales-channel.ts +++ b/packages/medusa/src/repositories/sales-channel.ts @@ -1,5 +1,33 @@ -import { EntityRepository, Repository } from "typeorm" +import { Brackets, EntityRepository, Repository } from "typeorm" import { SalesChannel } from "../models" +import { ExtendedFindConfig, Selector } from "../types/common"; @EntityRepository(SalesChannel) -export class SalesChannelRepository extends Repository {} +export class SalesChannelRepository extends Repository { + public async getFreeTextSearchResultsAndCount( + q: string, + options: ExtendedFindConfig> = { where: {} }, + ): Promise<[SalesChannel[], number]> { + const options_ = { ...options } + delete options_?.where?.name + delete options_?.where?.description + + let qb = this.createQueryBuilder("sales_channel") + .select() + .where(options_.where) + .andWhere( + new Brackets((qb) => { + qb.where(`sales_channel.description ILIKE :q`, { q: `%${q}%` }) + .orWhere(`sales_channel.name ILIKE :q`, { q: `%${q}%` }) + }) + ) + .skip(options.skip) + .take(options.take) + + if (options.withDeleted) { + qb = qb.withDeleted() + } + + return await qb.getManyAndCount() + } +} diff --git a/packages/medusa/src/services/__mocks__/sales-channel.js b/packages/medusa/src/services/__mocks__/sales-channel.js index a5cd66a013..5f994a4bf5 100644 --- a/packages/medusa/src/services/__mocks__/sales-channel.js +++ b/packages/medusa/src/services/__mocks__/sales-channel.js @@ -1,3 +1,5 @@ +import { IdMap } from "medusa-test-utils"; + export const SalesChannelServiceMock = { withTransaction: function () { return this @@ -15,7 +17,17 @@ export const SalesChannelServiceMock = { return Promise.resolve({ id, ...data }) }), - listAndCount: jest.fn().mockImplementation(() => {}), + listAndCount: jest.fn().mockImplementation(() => { + return Promise.resolve([ + [{ + id: IdMap.getId("sales_channel_1"), + name: "sales channel 1 name", + description: "sales channel 1 description", + is_disabled: false, + }], + 1, + ]) + }), create: jest.fn().mockImplementation((data) => { return Promise.resolve({ diff --git a/packages/medusa/src/services/__tests__/sales-channel.ts b/packages/medusa/src/services/__tests__/sales-channel.ts index 3a8016da75..ab99475574 100644 --- a/packages/medusa/src/services/__tests__/sales-channel.ts +++ b/packages/medusa/src/services/__tests__/sales-channel.ts @@ -13,31 +13,45 @@ describe("SalesChannelService", () => { is_disabled: false, } - const salesChannelRepositoryMock = MockRepository({ - findOne: jest - .fn() - .mockImplementation( - (queryOrId: string | FindOneOptions): any => { + const salesChannelRepositoryMock = { + ...MockRepository({ + findOne: jest.fn().mockImplementation((queryOrId: string | FindOneOptions): any => { return Promise.resolve({ id: typeof queryOrId === "string" ? queryOrId : (queryOrId?.where as FindConditions)?.id ?? - IdMap.getId("sc_adjhlukiaeswhfae"), + IdMap.getId("sc_adjhlukiaeswhfae"), ...salesChannelData, }) } ), - create: jest.fn().mockImplementation((data) => data), - save: (salesChannel) => - Promise.resolve({ + findAndCount: jest.fn().mockImplementation(() => + Promise.resolve([ + { + id: IdMap.getId("sales_channel_1"), + ...salesChannelData + }, + ]), + ), + create: jest.fn().mockImplementation((data) => data), + save: (salesChannel) => Promise.resolve({ id: IdMap.getId("sales_channel_1"), - ...salesChannel, + ...salesChannel + }), + softRemove: jest.fn().mockImplementation((id: string): any => { + return Promise.resolve() }), - softRemove: jest.fn().mockImplementation((id: string): any => { - return Promise.resolve() }), - }) + getFreeTextSearchResultsAndCount: jest.fn().mockImplementation(() => + Promise.resolve([ + { + id: IdMap.getId("sales_channel_1"), + ...salesChannelData + }, + ]), + ) + } describe("create default", async () => { const salesChannelService = new SalesChannelService({ @@ -158,6 +172,70 @@ describe("SalesChannelService", () => { }) }) + describe("list", () => { + const salesChannelService = new SalesChannelService({ + manager: MockManager, + eventBusService: EventBusServiceMock as unknown as EventBusService, + salesChannelRepository: salesChannelRepositoryMock, + storeService: StoreServiceMock as unknown as StoreService, + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("should retrieve a sales channel using free text search", async () => { + const q = "free search text" + + const salesChannel = await salesChannelService.listAndCount({ q }) + + expect(salesChannel).toBeTruthy() + expect(salesChannel).toEqual( + expect.arrayContaining([{ + id: IdMap.getId("sales_channel_1"), + ...salesChannelData, + }]) + ) + + expect(salesChannelRepositoryMock.findAndCount).toHaveBeenCalledTimes(0) + expect(salesChannelRepositoryMock.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(1) + expect(salesChannelRepositoryMock.getFreeTextSearchResultsAndCount).toHaveBeenLastCalledWith( + q, + { + skip: 0, + take: 20, + where: {}, + } + ) + }) + + it("should retrieve a sales channel using find and count", async () => { + const salesChannel = await salesChannelService.listAndCount({ + id: IdMap.getId("sales_channel_1") + }) + + expect(salesChannel).toBeTruthy() + expect(salesChannel).toEqual( + expect.arrayContaining([{ + id: IdMap.getId("sales_channel_1"), + ...salesChannelData, + }]) + ) + + expect(salesChannelRepositoryMock.getFreeTextSearchResultsAndCount).toHaveBeenCalledTimes(0) + expect(salesChannelRepositoryMock.findAndCount).toHaveBeenCalledTimes(1) + expect(salesChannelRepositoryMock.findAndCount).toHaveBeenLastCalledWith( + { + skip: 0, + take: 20, + where: { + id: IdMap.getId("sales_channel_1"), + }, + } + ) + }) + }) + describe("delete", () => { const salesChannelService = new SalesChannelService({ manager: MockManager, diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index e0457e1fe9..ed44d7276c 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -85,11 +85,40 @@ class SalesChannelService extends TransactionBaseService { }) } + /** + * Lists sales channels based on the provided parameters and includes the count of + * sales channels that match the query. + * @return an array containing the sales channels as + * the first element and the total count of sales channels that matches the query + * as the second element. + */ async listAndCount( - selector: QuerySelector = {}, - config: FindConfig = { relations: [], skip: 0, take: 10 } + selector: QuerySelector, + config: FindConfig = { + skip: 0, + take: 20, + } ): Promise<[SalesChannel[], number]> { - throw new Error("Method not implemented.") + return await this.atomicPhase_(async (transactionManager) => { + const salesChannelRepo = transactionManager.getCustomRepository( + this.salesChannelRepository_ + ) + + const selector_ = { ...selector } + let q: string | undefined + if ("q" in selector_) { + q = selector_.q + delete selector_.q + } + + const query = buildQuery(selector_, config) + + if (q) { + return await salesChannelRepo.getFreeTextSearchResultsAndCount(q, query) + } + + return await salesChannelRepo.findAndCount(query) + }) } /** diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index dffbc91dc8..5033c59612 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -14,7 +14,8 @@ import { OrderByCondition, } from "typeorm" import { transformDate } from "../utils/validators/date-transform" -import { BaseEntity } from "../interfaces/models/base-entity" +import { BaseEntity } from "../interfaces" +import { ClassConstructor } from "./global" /** * Utility type used to remove some optional attributes (coming from K) from a type T @@ -270,3 +271,37 @@ export class FindParams { @IsOptional() fields?: string } + +export class FindPaginationParams { + @IsNumber() + @IsOptional() + @Type(() => Number) + offset?: number = 0 + + @IsNumber() + @IsOptional() + @Type(() => Number) + limit?: number = 20 +} + +export function extendedFindParamsMixin({ + limit, + offset, +}: { + limit?: number + offset?: number +} = {}): ClassConstructor { + class FindExtendedPaginationParams extends FindParams { + @IsNumber() + @IsOptional() + @Type(() => Number) + offset?: number = offset ?? 0 + + @IsNumber() + @IsOptional() + @Type(() => Number) + limit?: number = limit ?? 20 + } + + return FindExtendedPaginationParams +} diff --git a/packages/medusa/src/types/sales-channels.ts b/packages/medusa/src/types/sales-channels.ts index 8f7d822b48..61d0e07394 100644 --- a/packages/medusa/src/types/sales-channels.ts +++ b/packages/medusa/src/types/sales-channels.ts @@ -1,3 +1,5 @@ +import { SalesChannel } from "../models" + export type CreateSalesChannelInput = { name: string description?: string diff --git a/packages/medusa/src/utils/__tests__/remove-undefined-properties.spec.ts b/packages/medusa/src/utils/__tests__/remove-undefined-properties.spec.ts new file mode 100644 index 0000000000..eb00912f13 --- /dev/null +++ b/packages/medusa/src/utils/__tests__/remove-undefined-properties.spec.ts @@ -0,0 +1,47 @@ +import { removeUndefinedProperties } from "../remove-undefined-properties"; + +describe("removeUndefinedProperties", () => { + it("should remove all undefined properties from an input object", () => { + const inputObj = { + test: undefined, + test1: "test1", + test2: null, + test3: { + test3_1: undefined, + test3_2: "test3_2", + test3_3: null, + }, + test4: [ + undefined, + null, + "null", + [1, 2, undefined], + { + test4_1: undefined, + test4_2: "test4_2", + test4_3: null, + } + ] + } + + const cleanObject = removeUndefinedProperties(inputObj) + + expect(cleanObject).toEqual({ + test1: "test1", + test2: null, + test3: { + test3_2: "test3_2", + test3_3: null, + }, + test4: [ + null, + null, + [1, 2], + { + test4_2: "test4_2", + test4_3: null + } + ] + }) + }) +}) diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts index bd91a42f49..6b1988de67 100644 --- a/packages/medusa/src/utils/index.ts +++ b/packages/medusa/src/utils/index.ts @@ -1,4 +1,5 @@ -export * from './build-query' -export * from './set-metadata' -export * from './validate-id' -export * from './generate-entity-id' \ No newline at end of file +export * from "./build-query" +export * from "./set-metadata" +export * from "./validate-id" +export * from "./generate-entity-id" +export * from "./remove-undefined-properties" diff --git a/packages/medusa/src/utils/remove-undefined-properties.ts b/packages/medusa/src/utils/remove-undefined-properties.ts new file mode 100644 index 0000000000..0898238880 --- /dev/null +++ b/packages/medusa/src/utils/remove-undefined-properties.ts @@ -0,0 +1,41 @@ +export function removeUndefinedProperties(inputObj: T): T { + const removeProperties = (obj: T) => { + const res = {} as T + + Object.keys(obj).reduce((acc: T, key: string) => { + if (typeof obj[key] === "undefined") { + return acc + } + acc[key] = removeUndefinedDeeply(obj[key]) + return acc + }, res) + + return res + } + + return removeProperties(inputObj) +} + +function removeUndefinedDeeply(input: unknown): any { + if (typeof input !== "undefined") { + if (input === null || input === "null") { + return null + } else if (Array.isArray(input)) { + return input.map((item) => { + return removeUndefinedDeeply(item) + }).filter(v => typeof v !== "undefined") + } else if (Object.prototype.toString.call(input) === '[object Date]') { + return input + } else if (typeof input === "object") { + return Object.keys(input).reduce((acc: Record, key: string) => { + if (typeof input[key] === "undefined") { + return acc + } + acc[key] = removeUndefinedDeeply(input[key]) + return acc + }, {}) + } else { + return input + } + } +} \ No newline at end of file