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

This commit is contained in:
Philip Korsholm authored and GitHub committed 2022-07-06 15:44:09 +02:00
1 parent 263a661031
commit 9d19cc0818
18 files changed
+374 -68

No files matched your search

@@ -11,3 +11,15 @@ Object {
"updated_at": Any<String>, "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>,
}
`;
@@ -4,7 +4,7 @@ const { useApi } = require("../../../helpers/use-api")
const { useDb } = require("../../../helpers/use-db") const { useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder") const adminSeeder = require("../../helpers/admin-seeder")
const { simpleSalesChannelFactory, } = require("../../factories") const { simpleSalesChannelFactory } = require("../../factories")
const startServerWithEnvironment = const startServerWithEnvironment =
require("../../../helpers/start-server-with-environment").default require("../../../helpers/start-server-with-environment").default
@@ -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", () => {}) describe("DELETE /admin/sales-channels/:id", () => {})
}) })
@@ -1,5 +1,6 @@
import { import {
AdminSalesChannelRes, AdminSalesChannelsRes,
AdminPostSalesChannelsSalesChannelReq,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { ResponsePromise } from "../../typings" import { ResponsePromise } from "../../typings"
import BaseResource from "../base" import BaseResource from "../base"
@@ -12,7 +13,7 @@ class AdminSalesChannelsResource extends BaseResource {
retrieve( retrieve(
salesChannelId: string, salesChannelId: string,
customHeaders: Record<string, any> = {} customHeaders: Record<string, any> = {}
): ResponsePromise<AdminSalesChannelRes> { ): ResponsePromise<AdminSalesChannelsRes> {
const path = `/admin/sales-channels/${salesChannelId}` const path = `/admin/sales-channels/${salesChannelId}`
return this.client.request("GET", path, {}, {}, customHeaders) return this.client.request("GET", path, {}, {}, customHeaders)
} }
@@ -22,11 +23,17 @@ class AdminSalesChannelsResource extends BaseResource {
customHeaders: Record<string, any> = {} customHeaders: Record<string, any> = {}
): ResponsePromise<any> {}*/ ): ResponsePromise<any> {}*/
/*update( /** @description updates a sales channel
id: string, * @returns the updated medusa sales channel
payload: any, */
update(
salesChannelId: string,
payload: AdminPostSalesChannelsSalesChannelReq,
customHeaders: Record<string, any> = {} 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, id: string,
@@ -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),
},
})
)
})
] ]
@@ -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 = ( /*export const useAdminCreateSalesChannel = (
options?: UseMutationOptions< options?: UseMutationOptions<
Response<AdminSalesChannelsRes>, 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 = ( /*export const useAdminDeleteSalesChannel = (
id: string, id: string,
options?: UseMutationOptions<Response<AdminSalesChannelsDeleteRes>, Error, void> options?: UseMutationOptions<Response<AdminSalesChannelsDeleteRes>, Error, void>
@@ -1,6 +1,4 @@
import { import { AdminSalesChannelsRes } from "@medusajs/medusa"
AdminSalesChannelRes
} 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"
@@ -9,14 +7,16 @@ import { queryKeysFactory } from "../../utils"
const ADMIN_SALES_CHANNELS_QUERY_KEY = `admin_sales_channels` as const 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 type SalesChannelsQueryKeys = typeof adminSalesChannelsKeys
export const useAdminSalesChannel = ( export const useAdminSalesChannel = (
id: string, id: string,
options?: UseQueryOptionsWrapper< options?: UseQueryOptionsWrapper<
Response<AdminSalesChannelRes>, Response<AdminSalesChannelsRes>,
Error, Error,
ReturnType<SalesChannelsQueryKeys["detail"]> ReturnType<SalesChannelsQueryKeys["detail"]>
> >
@@ -48,4 +48,3 @@ export const useAdminSalesChannels = (
return { ...data, ...rest } as const return { ...data, ...rest } as const
} }
*/ */
@@ -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,
})
})
})
@@ -1,6 +1,4 @@
import { import { useAdminSalesChannel } from "../../../../src"
useAdminSalesChannel,
} 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"
@@ -20,8 +20,8 @@ import productTypesRoutes from "./product-types"
import productRoutes from "./products" import productRoutes from "./products"
import regionRoutes from "./regions" import regionRoutes from "./regions"
import returnReasonRoutes from "./return-reasons" import returnReasonRoutes from "./return-reasons"
import salesChannelRoutes from "./sales-channels"
import returnRoutes from "./returns" import returnRoutes from "./returns"
import salesChannelRoutes from "./sales-channels"
import shippingOptionRoutes from "./shipping-options" import shippingOptionRoutes from "./shipping-options"
import shippingProfileRoutes from "./shipping-profiles" import shippingProfileRoutes from "./shipping-profiles"
import storeRoutes from "./store" import storeRoutes from "./store"
@@ -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",
}
)
})
})
})
@@ -10,7 +10,7 @@ import SalesChannelService from "../../../../services/sales-channel"
* parameters: * parameters:
* - (path) id=* {string} The id of the Sales channel. * - (path) id=* {string} The id of the Sales channel.
* tags: * tags:
* - Sales channel * - Sales Channel
* responses: * responses:
* 200: * 200:
* description: OK * description: OK
@@ -3,7 +3,8 @@ 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 from "../../../middlewares" import middlewares, { transformBody } from "../../../middlewares"
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
const route = Router() const route = Router()
@@ -22,14 +23,18 @@ export default (app) => {
route.post("/", (req, res) => {}) 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) => {}) route.delete("/:id", (req, res) => {})
return app return app
} }
export type AdminSalesChannelRes = { export type AdminSalesChannelsRes = {
sales_channel: SalesChannel sales_channel: SalesChannel
} }
@@ -42,5 +47,5 @@ export type AdminSalesChannelListRes = PaginatedResponse & {
export * from "./get-sales-channel" export * from "./get-sales-channel"
// export * from './' // export * from './'
// export * from './' // export * from './'
// export * from './' export * from "./update-sales-channel"
// export * from './' // export * from './'
@@ -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
}
@@ -16,9 +16,6 @@ export class SalesChannel extends SoftDeletableEntity {
@Column({ default: false }) @Column({ default: false })
is_disabled: boolean is_disabled: boolean
// @Column({ type: resolveDbType("timestamptz"), nullable: true })
// disabled_at: Date | null
@BeforeInsert() @BeforeInsert()
private beforeInsert(): void { private beforeInsert(): void {
this.id = generateEntityId(this.id, "sc") this.id = generateEntityId(this.id, "sc")
@@ -11,6 +11,15 @@ export const SalesChannelServiceMock = {
is_disabled: false, 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(() => { const mock = jest.fn().mockImplementation(() => {
@@ -5,8 +5,7 @@ import { EventBusService } from "../index"
import { FindConditions, FindOneOptions } from "typeorm" import { FindConditions, FindOneOptions } from "typeorm"
import { SalesChannel } from "../../models" import { SalesChannel } from "../../models"
describe('SalesChannelService', () => { describe("SalesChannelService", () => {
describe("retrieve", () => {
const salesChannelData = { const salesChannelData = {
name: "sales channel 1 name", name: "sales channel 1 name",
description: "sales channel 1 description", description: "sales channel 1 description",
@@ -14,28 +13,35 @@ describe('SalesChannelService', () => {
} }
const salesChannelRepositoryMock = MockRepository({ const salesChannelRepositoryMock = MockRepository({
findOne: jest.fn().mockImplementation((queryOrId: string | FindOneOptions<SalesChannel>): any => { findOne: jest
.fn()
.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 ?? IdMap.getId("sc_adjhlukiaeswhfae")), : (queryOrId?.where as FindConditions<SalesChannel>)?.id ??
...salesChannelData IdMap.getId("sc_adjhlukiaeswhfae"),
...salesChannelData,
}) })
}), }
),
save: (salesChannel) => Promise.resolve(salesChannel),
}) })
describe("retrieve", () => {
const salesChannelService = new SalesChannelService({ const salesChannelService = new SalesChannelService({
manager: MockManager, manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService, eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock salesChannelRepository: salesChannelRepositoryMock,
}) })
afterEach(() => { afterEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it('should retrieve a sales channel', async () => { it("should retrieve a sales channel", async () => {
const salesChannel = await salesChannelService.retrieve( const salesChannel = await salesChannelService.retrieve(
IdMap.getId("sales_channel_1") IdMap.getId("sales_channel_1")
) )
@@ -43,15 +49,47 @@ describe('SalesChannelService', () => {
expect(salesChannel).toBeTruthy() expect(salesChannel).toBeTruthy()
expect(salesChannel).toEqual({ expect(salesChannel).toEqual({
id: IdMap.getId("sales_channel_1"), id: IdMap.getId("sales_channel_1"),
...salesChannelData ...salesChannelData,
}) })
expect(salesChannelRepositoryMock.findOne) expect(salesChannelRepositoryMock.findOne).toHaveBeenCalledTimes(1)
.toHaveBeenCalledTimes(1) expect(salesChannelRepositoryMock.findOne).toHaveBeenLastCalledWith({
expect(salesChannelRepositoryMock.findOne) where: { id: IdMap.getId("sales_channel_1") },
.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,
})
}) })
}) })
}) })
+1
View File
@@ -30,6 +30,7 @@ export { default as QueryBuilderService } from "./query-builder"
export { default as RegionService } from "./region" export { default as RegionService } from "./region"
export { default as ReturnReasonService } from "./return-reason" export { default as ReturnReasonService } from "./return-reason"
export { default as ReturnService } from "./return" export { default as ReturnService } from "./return"
export { default as SalesChannelService } from "./sales-channel"
export { default as SearchService } from "./search" export { default as SearchService } from "./search"
export { default as ShippingOptionService } from "./shipping-option" export { default as ShippingOptionService } from "./shipping-option"
export { default as ShippingProfileService } from "./shipping-profile" export { default as ShippingProfileService } from "./shipping-profile"
+29 -4
View File
@@ -1,4 +1,5 @@
import { EntityManager } from "typeorm" import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { TransactionBaseService } from "../interfaces" import { TransactionBaseService } from "../interfaces"
import { SalesChannel } from "../models" import { SalesChannel } from "../models"
import { SalesChannelRepository } from "../repositories/sales-channel" import { SalesChannelRepository } from "../repositories/sales-channel"
@@ -9,7 +10,6 @@ import {
} from "../types/sales-channels" } from "../types/sales-channels"
import EventBusService from "./event-bus" import EventBusService from "./event-bus"
import { buildQuery } from "../utils" import { buildQuery } from "../utils"
import { MedusaError } from "medusa-core-utils"
type InjectedDependencies = { type InjectedDependencies = {
salesChannelRepository: typeof SalesChannelRepository salesChannelRepository: typeof SalesChannelRepository
@@ -18,6 +18,10 @@ type InjectedDependencies = {
} }
class SalesChannelService extends TransactionBaseService<SalesChannelService> { class SalesChannelService extends TransactionBaseService<SalesChannelService> {
static Events = {
UPDATED: "sales_channel.updated",
}
protected manager_: EntityManager protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined protected transactionManager_: EntityManager | undefined
@@ -78,10 +82,31 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
} }
async update( async update(
id: string, salesChannelId: string,
data: UpdateSalesChannelInput data: UpdateSalesChannelInput
): Promise<SalesChannel> { ): Promise<SalesChannel | never> {
throw new Error("Method not implemented.") 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> { async delete(id: string): Promise<void> {