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:
@@ -1,5 +1,71 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
exports[`sales channels DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"deleted": true,
|
"deleted": true,
|
||||||
|
|||||||
@@ -1521,7 +1521,7 @@ describe("/admin/orders", () => {
|
|||||||
|
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
expect(response.data.count).toEqual(2)
|
expect(response.data.count).toEqual(2)
|
||||||
expect(response.data.orders).toEqual([
|
expect(response.data.orders).toEqual(expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "test-order",
|
id: "test-order",
|
||||||
shipping_address: expect.objectContaining({ first_name: "lebron" }),
|
shipping_address: expect.objectContaining({ first_name: "lebron" }),
|
||||||
@@ -1530,7 +1530,7 @@ describe("/admin/orders", () => {
|
|||||||
id: "discount-order",
|
id: "discount-order",
|
||||||
shipping_address: expect.objectContaining({ first_name: "lebron" }),
|
shipping_address: expect.objectContaining({ first_name: "lebron" }),
|
||||||
}),
|
}),
|
||||||
])
|
]))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully lists orders with greater than", async () => {
|
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", () => {
|
describe("POST /admin/sales-channels/:id", () => {
|
||||||
let sc
|
let sc
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
|
AdminGetSalesChannelsParams,
|
||||||
AdminPostSalesChannelsReq,
|
AdminPostSalesChannelsReq,
|
||||||
AdminSalesChannelsRes,
|
AdminSalesChannelsRes,
|
||||||
AdminPostSalesChannelsSalesChannelReq,
|
AdminPostSalesChannelsSalesChannelReq,
|
||||||
AdminSalesChannelsDeleteRes,
|
AdminSalesChannelsDeleteRes,
|
||||||
|
AdminSalesChannelsListRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import { ResponsePromise } from "../../typings"
|
import { ResponsePromise } from "../../typings"
|
||||||
import BaseResource from "../base"
|
import BaseResource from "../base"
|
||||||
|
import qs from "qs"
|
||||||
|
|
||||||
class AdminSalesChannelsResource extends BaseResource {
|
class AdminSalesChannelsResource extends BaseResource {
|
||||||
/** retrieve a sales channel
|
/** retrieve a sales channel
|
||||||
@@ -49,11 +52,26 @@ class AdminSalesChannelsResource extends BaseResource {
|
|||||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
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> = {}
|
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
|
* Delete a sales channel
|
||||||
|
|||||||
@@ -1270,6 +1270,15 @@
|
|||||||
"updated_at": "2022-07-05T15:16:01.959Z",
|
"updated_at": "2022-07-05T15:16:01.959Z",
|
||||||
"created_at": "2022-07-05T15:16:01.959Z",
|
"created_at": "2022-07-05T15:16:01.959Z",
|
||||||
"deleted_at": null
|
"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) => {
|
rest.post("/admin/sales-channels/:id", (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
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 { Response } from "@medusajs/medusa-js"
|
||||||
import { useQuery } from "react-query"
|
import { useQuery } from "react-query"
|
||||||
import { useMedusa } from "../../../contexts"
|
import { useMedusa } from "../../../contexts"
|
||||||
@@ -36,21 +40,26 @@ export const useAdminSalesChannel = (
|
|||||||
return { ...data, ...rest } as const
|
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 = (
|
export const useAdminSalesChannels = (
|
||||||
query?: AdminGetSalesChannelsParams,
|
query?: AdminGetSalesChannelsParams,
|
||||||
options?: UseQueryOptionsWrapper<
|
options?: UseQueryOptionsWrapper<
|
||||||
Response<AdminProductsListRes>,
|
Response<AdminSalesChannelsListRes>,
|
||||||
Error,
|
Error,
|
||||||
ReturnType<SalesChannelsQueryKeys["list"]>
|
ReturnType<SalesChannelsQueryKeys["list"]>
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const { client } = useMedusa()
|
const { client } = useMedusa()
|
||||||
const { data, ...rest } = useQuery(
|
const { data, ...rest } = useQuery(
|
||||||
adminProductKeys.list(query),
|
adminSalesChannelsKeys.list(query),
|
||||||
() => client.admin.salesChannels.list(query),
|
() => client.admin.salesChannels.list(query),
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
return { ...data, ...rest } as const
|
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 { renderHook } from "@testing-library/react-hooks"
|
||||||
import { fixtures } from "../../../../mocks/data"
|
import { fixtures } from "../../../../mocks/data"
|
||||||
import { createWrapper } from "../../../utils"
|
import { createWrapper } from "../../../utils"
|
||||||
|
|
||||||
describe("useAdminSalesChannel hook", () => {
|
describe("useAdminSalesChannel hook", () => {
|
||||||
test("returns a product", async () => {
|
test("returns a sales channel", async () => {
|
||||||
const salesChannel = fixtures.get("sales_channel")
|
const salesChannel = fixtures.get("sales_channel")
|
||||||
const { result, waitFor } = renderHook(
|
const { result, waitFor } = renderHook(
|
||||||
() => useAdminSalesChannel(salesChannel.id),
|
() => useAdminSalesChannel(salesChannel.id),
|
||||||
@@ -19,3 +19,17 @@ describe("useAdminSalesChannel hook", () => {
|
|||||||
expect(result.current.sales_channel).toEqual(salesChannel)
|
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,
|
prepareListQuery,
|
||||||
prepareRetrieveQuery,
|
prepareRetrieveQuery,
|
||||||
} from "../../utils/get-query-config"
|
} from "../../utils/get-query-config"
|
||||||
import { BaseEntity } from "../../interfaces/models/base-entity"
|
import { BaseEntity } from "../../interfaces"
|
||||||
import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common"
|
import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common"
|
||||||
import { omit } from "lodash"
|
import { omit } from "lodash"
|
||||||
|
import { removeUndefinedProperties } from "../../utils"
|
||||||
|
|
||||||
export function transformQuery<
|
export function transformQuery<
|
||||||
T extends RequestQueryFields,
|
T extends RequestQueryFields,
|
||||||
@@ -36,6 +37,7 @@ export function transformQuery<
|
|||||||
"fields",
|
"fields",
|
||||||
"order",
|
"order",
|
||||||
])
|
])
|
||||||
|
req.filterableFields = removeUndefinedProperties(req.filterableFields)
|
||||||
|
|
||||||
if (queryConfig?.isList) {
|
if (queryConfig?.isList) {
|
||||||
req.listConfig = prepareListQuery(
|
req.listConfig = prepareListQuery(
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||||
import omit from "lodash/omit"
|
|
||||||
import { PriceList } from "../../../.."
|
|
||||||
import PriceListService from "../../../../services/price-list"
|
import PriceListService from "../../../../services/price-list"
|
||||||
import { FindConfig } from "../../../../types/common"
|
|
||||||
import { FilterablePriceListProps } from "../../../../types/price-list"
|
import { FilterablePriceListProps } from "../../../../types/price-list"
|
||||||
import { validator } from "../../../../utils/validator"
|
|
||||||
import { Request } from "express"
|
import { Request } from "express"
|
||||||
/**
|
/**
|
||||||
* @oas [get] /price-lists
|
* @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 { Request, Response } from "express"
|
||||||
import { IsObject, IsOptional, IsString } from "class-validator"
|
import { IsOptional, IsString } from "class-validator"
|
||||||
|
|
||||||
import SalesChannelService from "../../../../services/sales-channel"
|
import SalesChannelService from "../../../../services/sales-channel"
|
||||||
import { CreateSalesChannelInput } from "../../../../types/sales-channels"
|
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) name=* {string} Name of the sales channel
|
||||||
* - (body) description=* {string} Description of the sales channel
|
* - (body) description=* {string} Description of the sales channel
|
||||||
* tags:
|
* tags:
|
||||||
* - Sales Channels
|
* - Sales Channel
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: OK
|
* description: OK
|
||||||
|
|||||||
@@ -3,15 +3,27 @@ import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
|||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
|
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
|
||||||
import { SalesChannel } from "../../../../models"
|
import { SalesChannel } from "../../../../models"
|
||||||
import middlewares, { transformBody } from "../../../middlewares"
|
import middlewares, {
|
||||||
|
transformBody,
|
||||||
|
transformQuery,
|
||||||
|
} from "../../../middlewares"
|
||||||
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
|
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
|
||||||
import { AdminPostSalesChannelsReq } from "./create-sales-channel"
|
import { AdminPostSalesChannelsReq } from "./create-sales-channel"
|
||||||
|
import { AdminGetSalesChannelsParams } from "./list-sales-channels"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
app.use("/sales-channels", isFeatureFlagEnabled("sales_channels"), route)
|
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 })
|
const salesChannelRouter = Router({ mergeParams: true })
|
||||||
route.use("/:id", salesChannelRouter)
|
route.use("/:id", salesChannelRouter)
|
||||||
|
|
||||||
@@ -23,8 +35,11 @@ export default (app) => {
|
|||||||
"/",
|
"/",
|
||||||
middlewares.wrap(require("./delete-sales-channel").default)
|
middlewares.wrap(require("./delete-sales-channel").default)
|
||||||
)
|
)
|
||||||
|
salesChannelRouter.post(
|
||||||
route.get("/", (req, res) => {})
|
"/",
|
||||||
|
transformBody(AdminPostSalesChannelsSalesChannelReq),
|
||||||
|
middlewares.wrap(require("./update-sales-channel").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
"/",
|
"/",
|
||||||
@@ -38,8 +53,6 @@ export default (app) => {
|
|||||||
middlewares.wrap(require("./update-sales-channel").default)
|
middlewares.wrap(require("./update-sales-channel").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +66,12 @@ export type AdminSalesChannelListRes = PaginatedResponse & {
|
|||||||
sales_channels: SalesChannel[]
|
sales_channels: SalesChannel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminSalesChannelsListRes = PaginatedResponse & {
|
||||||
|
sales_channels: SalesChannel[]
|
||||||
|
}
|
||||||
|
|
||||||
export * from "./get-sales-channel"
|
export * from "./get-sales-channel"
|
||||||
export * from "./create-sales-channel"
|
export * from "./create-sales-channel"
|
||||||
// export * from './'
|
export * from "./list-sales-channels"
|
||||||
// export * from './'
|
|
||||||
export * from "./update-sales-channel"
|
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 { SalesChannel } from "../models"
|
||||||
|
import { ExtendedFindConfig, Selector } from "../types/common";
|
||||||
|
|
||||||
@EntityRepository(SalesChannel)
|
@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 = {
|
export const SalesChannelServiceMock = {
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
@@ -15,7 +17,17 @@ export const SalesChannelServiceMock = {
|
|||||||
return Promise.resolve({ id, ...data })
|
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) => {
|
create: jest.fn().mockImplementation((data) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
|||||||
@@ -13,31 +13,45 @@ describe("SalesChannelService", () => {
|
|||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const salesChannelRepositoryMock = MockRepository({
|
const salesChannelRepositoryMock = {
|
||||||
findOne: jest
|
...MockRepository({
|
||||||
.fn()
|
findOne: jest.fn().mockImplementation((queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
||||||
.mockImplementation(
|
|
||||||
(queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id:
|
id:
|
||||||
typeof queryOrId === "string"
|
typeof queryOrId === "string"
|
||||||
? queryOrId
|
? queryOrId
|
||||||
: (queryOrId?.where as FindConditions<SalesChannel>)?.id ??
|
: (queryOrId?.where as FindConditions<SalesChannel>)?.id ??
|
||||||
IdMap.getId("sc_adjhlukiaeswhfae"),
|
IdMap.getId("sc_adjhlukiaeswhfae"),
|
||||||
...salesChannelData,
|
...salesChannelData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
create: jest.fn().mockImplementation((data) => data),
|
findAndCount: jest.fn().mockImplementation(() =>
|
||||||
save: (salesChannel) =>
|
Promise.resolve([
|
||||||
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"),
|
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 () => {
|
describe("create default", async () => {
|
||||||
const salesChannelService = new SalesChannelService({
|
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", () => {
|
describe("delete", () => {
|
||||||
const salesChannelService = new SalesChannelService({
|
const salesChannelService = new SalesChannelService({
|
||||||
manager: MockManager,
|
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(
|
async listAndCount(
|
||||||
selector: QuerySelector<any> = {},
|
selector: QuerySelector<SalesChannel>,
|
||||||
config: FindConfig<any> = { relations: [], skip: 0, take: 10 }
|
config: FindConfig<SalesChannel> = {
|
||||||
|
skip: 0,
|
||||||
|
take: 20,
|
||||||
|
}
|
||||||
): Promise<[SalesChannel[], number]> {
|
): 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,
|
OrderByCondition,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { transformDate } from "../utils/validators/date-transform"
|
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
|
* Utility type used to remove some optional attributes (coming from K) from a type T
|
||||||
@@ -270,3 +271,37 @@ export class FindParams {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
fields?: string
|
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 = {
|
export type CreateSalesChannelInput = {
|
||||||
name: string
|
name: string
|
||||||
description?: 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 "./build-query"
|
||||||
export * from './set-metadata'
|
export * from "./set-metadata"
|
||||||
export * from './validate-id'
|
export * from "./validate-id"
|
||||||
export * from './generate-entity-id'
|
export * from "./generate-entity-id"
|
||||||
|
export * from "./remove-undefined-properties"
|
||||||
|
|||||||
@@ -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