feat(medusa, medusa-js, medusa-react): Implement Sales Channel update (#1797)

This commit is contained in:
Philip Korsholm
2022-07-06 15:44:09 +02:00
committed by GitHub
parent 263a661031
commit 9d19cc0818
19 changed files with 395 additions and 89 deletions

View File

@@ -11,3 +11,15 @@ Object {
"updated_at": Any<String>,
}
`;
exports[`sales channels POST /admin/sales-channels/:id updates sales channel properties 1`] = `
Object {
"created_at": Any<String>,
"deleted_at": null,
"description": "updated description",
"id": Any<String>,
"is_disabled": true,
"name": "updated name",
"updated_at": Any<String>,
}
`;

View File

@@ -4,7 +4,7 @@ const { useApi } = require("../../../helpers/use-api")
const { useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder")
const { simpleSalesChannelFactory, } = require("../../factories")
const { simpleSalesChannelFactory } = require("../../factories")
const startServerWithEnvironment =
require("../../../helpers/start-server-with-environment").default
@@ -49,7 +49,7 @@ describe("sales channels", () => {
describe("GET /admin/sales-channels/:id", () => {
let salesChannel
beforeEach(async() => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
salesChannel = await simpleSalesChannelFactory(dbConnection, {
@@ -61,12 +61,12 @@ describe("sales channels", () => {
}
})
afterEach(async() => {
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should retrieve the requested sales channel", async() => {
it("should retrieve the requested sales channel", async () => {
const api = useApi()
const response = await api.get(
`/admin/sales-channels/${salesChannel.id}`,
@@ -85,6 +85,56 @@ describe("sales channels", () => {
})
})
describe("POST /admin/sales-channels/:id", () => {})
describe("POST /admin/sales-channels/:id", () => {
let sc
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
sc = await simpleSalesChannelFactory(dbConnection, {
name: "test name",
description: "test description",
})
} catch (err) {
console.log(err)
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("updates sales channel properties", async () => {
const api = useApi()
const payload = {
name: "updated name",
description: "updated description",
is_disabled: true,
}
const response = await api.post(
`/admin/sales-channels/${sc.id}`,
payload,
{
headers: {
authorization: "Bearer test_token",
},
}
)
expect(response.status).toEqual(200)
expect(response.data.sales_channel).toMatchSnapshot({
id: expect.any(String),
name: payload.name,
description: payload.description,
is_disabled: payload.is_disabled,
created_at: expect.any(String),
updated_at: expect.any(String),
})
})
})
describe("DELETE /admin/sales-channels/:id", () => {})
})

View File

@@ -28,4 +28,4 @@ export const simpleSalesChannelFactory = async (
})
return await manager.save(salesChannel)
}
}

View File

@@ -1,5 +1,6 @@
import {
AdminSalesChannelRes,
AdminSalesChannelsRes,
AdminPostSalesChannelsSalesChannelReq,
} from "@medusajs/medusa"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
@@ -12,29 +13,35 @@ class AdminSalesChannelsResource extends BaseResource {
retrieve(
salesChannelId: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminSalesChannelRes> {
): ResponsePromise<AdminSalesChannelsRes> {
const path = `/admin/sales-channels/${salesChannelId}`
return this.client.request("GET", path, {}, {}, customHeaders)
}
/*create(
/* create(
payload: any,
customHeaders: Record<string, any> = {}
): ResponsePromise<any> {}*/
/*update(
id: string,
payload: any,
/** @description updates a sales channel
* @returns the updated medusa sales channel
*/
update(
salesChannelId: string,
payload: AdminPostSalesChannelsSalesChannelReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<any> {}*/
): ResponsePromise<AdminSalesChannelsRes> {
const path = `/admin/sales-channels/${salesChannelId}`
return this.client.request("POST", path, payload, {}, customHeaders)
}
/*delete(
/* delete(
id: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<any> {
}*/
/*list(
/* list(
query?: any,
customHeaders: Record<string, any> = {}
): ResponsePromise<any> {

View File

@@ -1681,4 +1681,16 @@ export const adminHandlers = [
})
)
}),
rest.post("/admin/sales-channels/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
sales_channel: {
...fixtures.get("sales_channel"),
...(req.body as any),
},
})
)
})
]

View File

@@ -1,4 +1,34 @@
export {}
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import {
AdminSalesChannelsRes,
AdminPostSalesChannelsSalesChannelReq,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
import { adminSalesChannelsKeys } from "./queries"
export const useAdminUpdateSalesChannel = (
id: string,
options?: UseMutationOptions<
Response<AdminSalesChannelsRes>,
Error,
AdminPostSalesChannelsSalesChannelReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostSalesChannelsSalesChannelReq) =>
client.admin.salesChannels.update(id, payload),
buildOptions(
queryClient,
[adminSalesChannelsKeys.lists(), adminSalesChannelsKeys.detail(id)],
options
)
)
}
/*export const useAdminCreateSalesChannel = (
options?: UseMutationOptions<
Response<AdminSalesChannelsRes>,
@@ -14,28 +44,6 @@ export {}
)
}*/
/*export const useAdminUpdateSalesChannel = (
id: string,
options?: UseMutationOptions<
Response<AdminSalesChannelsRes>,
Error,
AdminPostSalesChannelsSalesChannelReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostSalesChannelsSalesChannelReq) =>
client.admin.salesChannels.update(id, payload),
buildOptions(
queryClient,
[adminSalesChannelsKeys.lists(), adminSalesChannelsKeys.detail(id)],
options
)
)
}*/
/*export const useAdminDeleteSalesChannel = (
id: string,
options?: UseMutationOptions<Response<AdminSalesChannelsDeleteRes>, Error, void>

View File

@@ -1,6 +1,4 @@
import {
AdminSalesChannelRes
} from "@medusajs/medusa"
import { AdminSalesChannelsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../../contexts"
@@ -9,14 +7,16 @@ import { queryKeysFactory } from "../../utils"
const ADMIN_SALES_CHANNELS_QUERY_KEY = `admin_sales_channels` as const
export const adminSalesChannelsKeys = queryKeysFactory(ADMIN_SALES_CHANNELS_QUERY_KEY)
export const adminSalesChannelsKeys = queryKeysFactory(
ADMIN_SALES_CHANNELS_QUERY_KEY
)
type SalesChannelsQueryKeys = typeof adminSalesChannelsKeys
export const useAdminSalesChannel = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminSalesChannelRes>,
Response<AdminSalesChannelsRes>,
Error,
ReturnType<SalesChannelsQueryKeys["detail"]>
>
@@ -48,4 +48,3 @@ export const useAdminSalesChannels = (
return { ...data, ...rest } as const
}
*/

View File

@@ -0,0 +1,33 @@
import { useAdminUpdateSalesChannel } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminUpdateStore hook", () => {
test("updates a store", async () => {
const salesChannel = {
name: "medusa sales channel",
description: "main sales channel for medusa",
is_disabled: true,
}
const salesChannelId = fixtures.get("sales_channel").id
const { result, waitFor } = renderHook(
() => useAdminUpdateSalesChannel(salesChannelId),
{
wrapper: createWrapper(),
}
)
result.current.mutate(salesChannel)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.sales_channel).toEqual({
...fixtures.get("sales_channel"),
...salesChannel,
})
})
})

View File

@@ -1,6 +1,4 @@
import {
useAdminSalesChannel,
} from "../../../../src"
import { useAdminSalesChannel } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"

View File

@@ -20,8 +20,8 @@ import productTypesRoutes from "./product-types"
import productRoutes from "./products"
import regionRoutes from "./regions"
import returnReasonRoutes from "./return-reasons"
import salesChannelRoutes from "./sales-channels"
import returnRoutes from "./returns"
import salesChannelRoutes from "./sales-channels"
import shippingOptionRoutes from "./shipping-options"
import shippingProfileRoutes from "./shipping-profiles"
import storeRoutes from "./store"

View File

@@ -0,0 +1,48 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
describe("POST /admin/regions/:region_id/countries", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
const id = IdMap.getId("test_sales_channel")
subject = await request("POST", `/admin/sales-channels/${id}`, {
payload: {
name: "amazon",
description: "This is our amazon sales channel",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
flags: ["sales_channels"],
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns updated sales channel", () => {
expect(subject.body.sales_channel).toEqual({
id: IdMap.getId("test_sales_channel"),
name: "amazon",
description: "This is our amazon sales channel",
})
})
it("calls service update", () => {
expect(SalesChannelServiceMock.update).toHaveBeenCalledTimes(1)
expect(SalesChannelServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("test_sales_channel"),
{
name: "amazon",
description: "This is our amazon sales channel",
}
)
})
})
})

View File

@@ -10,7 +10,7 @@ import SalesChannelService from "../../../../services/sales-channel"
* parameters:
* - (path) id=* {string} The id of the Sales channel.
* tags:
* - Sales channel
* - Sales Channel
* responses:
* 200:
* description: OK

View File

@@ -3,7 +3,8 @@ import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import "reflect-metadata"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { SalesChannel } from "../../../../models"
import middlewares from "../../../middlewares"
import middlewares, { transformBody } from "../../../middlewares"
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
const route = Router()
@@ -22,14 +23,18 @@ export default (app) => {
route.post("/", (req, res) => {})
route.post("/:id", (req, res) => {})
route.post(
"/:id",
transformBody(AdminPostSalesChannelsSalesChannelReq),
middlewares.wrap(require("./update-sales-channel").default)
)
route.delete("/:id", (req, res) => {})
return app
}
export type AdminSalesChannelRes = {
export type AdminSalesChannelsRes = {
sales_channel: SalesChannel
}
@@ -42,5 +47,5 @@ export type AdminSalesChannelListRes = PaginatedResponse & {
export * from "./get-sales-channel"
// export * from './'
// export * from './'
// export * from './'
export * from "./update-sales-channel"
// export * from './'

View File

@@ -0,0 +1,64 @@
import { IsBoolean, IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { SalesChannelService } from "../../../../services"
/**
* @oas [post] /sales-channels/{id}
* operationId: "PostSalesChannelsSalesChannel"
* summary: "Update a Sales Channel"
* description: "Updates a Sales Channel."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The id of the Sales Channel.
* requestBody:
* content:
* application/json:
* schema:
* properties:
* name:
* type: string
* description: Name of the sales channel.
* description:
* type: string
* description: Sales Channel description.
* is_disabled:
* type: boolean
* description: Indication of if the sales channel is active.
* tags:
* - Sales Channel
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* customer:
* $ref: "#/components/schemas/sales-channel"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const { validatedBody } = req as {
validatedBody: AdminPostSalesChannelsSalesChannelReq
}
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const sales_channel = await salesChannelService.update(id, validatedBody)
res.status(200).json({ sales_channel })
}
export class AdminPostSalesChannelsSalesChannelReq {
@IsOptional()
@IsString()
name?: string
@IsOptional()
@IsString()
description?: string
@IsBoolean()
@IsOptional()
is_disabled?: boolean
}

View File

@@ -16,9 +16,6 @@ export class SalesChannel extends SoftDeletableEntity {
@Column({ default: false })
is_disabled: boolean
// @Column({ type: resolveDbType("timestamptz"), nullable: true })
// disabled_at: Date | null
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "sc")

View File

@@ -11,6 +11,15 @@ export const SalesChannelServiceMock = {
is_disabled: false,
})
}),
update: jest.fn().mockImplementation((id, data) => {
return Promise.resolve({ id, ...data })
}),
listAndCount: jest.fn().mockImplementation(() => {}),
create: jest.fn().mockImplementation(() => {}),
delete: jest.fn().mockImplementation(() => {}),
}
const mock = jest.fn().mockImplementation(() => {

View File

@@ -5,53 +5,91 @@ import { EventBusService } from "../index"
import { FindConditions, FindOneOptions } from "typeorm"
import { SalesChannel } from "../../models"
describe('SalesChannelService', () => {
describe("SalesChannelService", () => {
const salesChannelData = {
name: "sales channel 1 name",
description: "sales channel 1 description",
is_disabled: false,
}
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"),
...salesChannelData,
})
}
),
save: (salesChannel) => Promise.resolve(salesChannel),
})
describe("retrieve", () => {
const salesChannelData = {
name: "sales channel 1 name",
description: "sales channel 1 description",
is_disabled: false,
}
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")),
...salesChannelData
})
}),
})
const salesChannelService = new SalesChannelService({
manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock
salesChannelRepository: salesChannelRepositoryMock,
})
afterEach(() => {
jest.clearAllMocks()
})
it('should retrieve a sales channel', async () => {
it("should retrieve a sales channel", async () => {
const salesChannel = await salesChannelService.retrieve(
IdMap.getId("sales_channel_1")
)
expect(salesChannel).toBeTruthy()
expect(salesChannel).toEqual({
id: IdMap.getId("sales_channel_1"),
...salesChannelData
id: IdMap.getId("sales_channel_1"),
...salesChannelData,
})
expect(salesChannelRepositoryMock.findOne)
.toHaveBeenCalledTimes(1)
expect(salesChannelRepositoryMock.findOne)
.toHaveBeenLastCalledWith(
{ where: { id: IdMap.getId("sales_channel_1") } },
)
expect(salesChannelRepositoryMock.findOne).toHaveBeenCalledTimes(1)
expect(salesChannelRepositoryMock.findOne).toHaveBeenLastCalledWith({
where: { id: IdMap.getId("sales_channel_1") },
})
})
})
describe("update", () => {
const salesChannelService = new SalesChannelService({
manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock,
})
const update = {
name: "updated name",
description: "updated description",
is_disabled: true,
}
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls save with the updated sales channel", async () => {
await salesChannelService.update(IdMap.getId("sc"), update)
expect(salesChannelRepositoryMock.save).toHaveBeenCalledWith({
id: IdMap.getId("sc"),
...update,
})
})
it("returns the saved sales channel", async () => {
const res = await salesChannelService.update(IdMap.getId("sc"), update)
expect(res).toEqual({
id: IdMap.getId("sc"),
...update,
})
})
})
})

View File

@@ -30,6 +30,7 @@ export { default as QueryBuilderService } from "./query-builder"
export { default as RegionService } from "./region"
export { default as ReturnReasonService } from "./return-reason"
export { default as ReturnService } from "./return"
export { default as SalesChannelService } from "./sales-channel"
export { default as SearchService } from "./search"
export { default as ShippingOptionService } from "./shipping-option"
export { default as ShippingProfileService } from "./shipping-profile"

View File

@@ -1,4 +1,5 @@
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { TransactionBaseService } from "../interfaces"
import { SalesChannel } from "../models"
import { SalesChannelRepository } from "../repositories/sales-channel"
@@ -9,7 +10,6 @@ import {
} from "../types/sales-channels"
import EventBusService from "./event-bus"
import { buildQuery } from "../utils"
import { MedusaError } from "medusa-core-utils"
type InjectedDependencies = {
salesChannelRepository: typeof SalesChannelRepository
@@ -18,6 +18,10 @@ type InjectedDependencies = {
}
class SalesChannelService extends TransactionBaseService<SalesChannelService> {
static Events = {
UPDATED: "sales_channel.updated",
}
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
@@ -78,10 +82,31 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
}
async update(
id: string,
salesChannelId: string,
data: UpdateSalesChannelInput
): Promise<SalesChannel> {
throw new Error("Method not implemented.")
): Promise<SalesChannel | never> {
return await this.atomicPhase_(async (transactionManager) => {
const salesChannelRepo: SalesChannelRepository =
transactionManager.getCustomRepository(this.salesChannelRepository_)
const salesChannel = await this.retrieve(salesChannelId)
for (const key of Object.keys(data)) {
if (typeof data[key] !== `undefined`) {
salesChannel[key] = data[key]
}
}
const result = await salesChannelRepo.save(salesChannel)
await this.eventBusService_
.withTransaction(transactionManager)
.emit(SalesChannelService.Events.UPDATED, {
id: result.id,
})
return result
})
}
async delete(id: string): Promise<void> {