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:
Adrien de Peretti
2022-07-13 12:28:53 +02:00
committed by GitHub
parent c20d720040
commit a1a5848827
23 changed files with 750 additions and 53 deletions

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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

View File

@@ -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

View File

@@ -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
}]
}
}

View File

@@ -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),

View File

@@ -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
}
*/

View File

@@ -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)
})
})

View File

@@ -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(

View File

@@ -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

View File

@@ -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,
})
})
})
})

View File

@@ -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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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({

View File

@@ -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,

View File

@@ -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)
})
}
/**

View File

@@ -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
}

View File

@@ -1,3 +1,5 @@
import { SalesChannel } from "../models"
export type CreateSalesChannelInput = {
name: string
description?: string

View File

@@ -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
}
]
})
})
})

View File

@@ -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"

View 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
}
}
}