feat(medusa, medusa-js, medusa-react): Implement Sales Channel list (#1815)
**What** Support sales channel list in medusa, medusa-js and medusa-react **How** By implementing a new endpoint and the associated service method as well as the repository methods. Medusa-js new list method in the resource Medusa-react new hook in the queries **Tests** Endpoint test Service test Integration test Hook tests Fixes CORE-280
This commit is contained in:
committed by
GitHub
parent
c20d720040
commit
a1a5848827
@@ -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<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
Object {
|
||||
"created_at": Any<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description 2",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name 2",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
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<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description 2",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name 2",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
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<String>,
|
||||
"deleted_at": null,
|
||||
"description": "test description",
|
||||
"id": Any<String>,
|
||||
"is_disabled": false,
|
||||
"name": "test name",
|
||||
"updated_at": Any<String>,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`sales channels DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = `
|
||||
Object {
|
||||
"deleted": true,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, any> = {}
|
||||
): ResponsePromise<any> {
|
||||
}*/
|
||||
): ResponsePromise<AdminSalesChannelsListRes> {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<AdminProductsListRes>,
|
||||
Response<AdminSalesChannelsListRes>,
|
||||
Error,
|
||||
ReturnType<SalesChannelsQueryKeys["list"]>
|
||||
>
|
||||
) => {
|
||||
const { client } = useMedusa()
|
||||
const { data, ...rest } = useQuery(
|
||||
adminProductKeys.list(query),
|
||||
adminSalesChannelsKeys.list(query),
|
||||
() => client.admin.salesChannels.list(query),
|
||||
options
|
||||
)
|
||||
return { ...data, ...rest } as const
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<SalesChannel> {}
|
||||
export class SalesChannelRepository extends Repository<SalesChannel> {
|
||||
public async getFreeTextSearchResultsAndCount(
|
||||
q: string,
|
||||
options: ExtendedFindConfig<SalesChannel, Selector<SalesChannel>> = { 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -13,31 +13,45 @@ describe("SalesChannelService", () => {
|
||||
is_disabled: false,
|
||||
}
|
||||
|
||||
const salesChannelRepositoryMock = MockRepository({
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
||||
const salesChannelRepositoryMock = {
|
||||
...MockRepository({
|
||||
findOne: jest.fn().mockImplementation((queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
||||
return Promise.resolve({
|
||||
id:
|
||||
typeof queryOrId === "string"
|
||||
? queryOrId
|
||||
: (queryOrId?.where as FindConditions<SalesChannel>)?.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,
|
||||
|
||||
@@ -85,11 +85,40 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<any> = {},
|
||||
config: FindConfig<any> = { relations: [], skip: 0, take: 10 }
|
||||
selector: QuerySelector<SalesChannel>,
|
||||
config: FindConfig<SalesChannel> = {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<FindParams & FindPaginationParams> {
|
||||
class FindExtendedPaginationParams extends FindParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number = offset ?? 0
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number = limit ?? 20
|
||||
}
|
||||
|
||||
return FindExtendedPaginationParams
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SalesChannel } from "../models"
|
||||
|
||||
export type CreateSalesChannelInput = {
|
||||
name: string
|
||||
description?: string
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './build-query'
|
||||
export * from './set-metadata'
|
||||
export * from './validate-id'
|
||||
export * from './generate-entity-id'
|
||||
export * from "./build-query"
|
||||
export * from "./set-metadata"
|
||||
export * from "./validate-id"
|
||||
export * from "./generate-entity-id"
|
||||
export * from "./remove-undefined-properties"
|
||||
|
||||
41
packages/medusa/src/utils/remove-undefined-properties.ts
Normal file
41
packages/medusa/src/utils/remove-undefined-properties.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export function removeUndefinedProperties<T extends object>(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<string, unknown>, key: string) => {
|
||||
if (typeof input[key] === "undefined") {
|
||||
return acc
|
||||
}
|
||||
acc[key] = removeUndefinedDeeply(input[key])
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user