feat(medusa): Add /admin/products/:id/variants end point (#1471)
* feat(medusa): Add /admin/products/:id/variants end point * test(medusa): Fix get-variants test casees * feat(medusa): Include the config to the ProdutService#retrieveVariants as a method parameter * feat(medusa): Improve get-variants endpoint * feat(medusa): Improve get-variants endpoint * test(medusa): Fix unit tests * test(medusa): Fix unit tests * feat(medusa): Improve typings * feat(medusa): Update according to feedback * feat(medusa): Update according to feedback * test(medusa): Fix list-variants tests * feat(medusa): Getting the variants from the product end point should use the productVariantService * fix(medusa): list-variants expand possibly undefined * Fix(medusa): List-variants endpoint * fix(medusa): Tests suite for list-variant * test(integration-tests): Fix yarn lock * test(integration-tests): Fix yarn lock
This commit is contained in:
committed by
GitHub
parent
79345d27ec
commit
edeac8ac72
@@ -1387,6 +1387,48 @@ describe("/admin/products", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /admin/products/:id/variants", () => {
|
||||
beforeEach(async() => {
|
||||
try {
|
||||
await productSeeder(dbConnection)
|
||||
await adminSeeder(dbConnection)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async() => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
it('should return the variants related to the requested product', async () => {
|
||||
const api = useApi()
|
||||
|
||||
const res = await api
|
||||
.get("/admin/products/test-product/variants", {
|
||||
headers: {
|
||||
Authorization: "Bearer test_token",
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
expect(res.status).toEqual(200)
|
||||
expect(res.data.variants.length).toBe(4)
|
||||
expect(res.data.variants).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: "test-variant", product_id: "test-product" }),
|
||||
expect.objectContaining({ id: "test-variant_1", product_id: "test-product" }),
|
||||
expect.objectContaining({ id: "test-variant_2", product_id: "test-product" }),
|
||||
expect.objectContaining({ id: "test-variant-sale", product_id: "test-product" }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updates a variant's default prices (ignores prices associated with a Price List)", () => {
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/medusa": "1.2.1-dev-1650573289860",
|
||||
"@medusajs/medusa": "1.3.0-dev-1652692202580",
|
||||
"faker": "^5.5.3",
|
||||
"medusa-interfaces": "1.2.1-dev-1650573289860",
|
||||
"medusa-interfaces": "1.3.0-dev-1652692202580",
|
||||
"typeorm": "^0.2.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/node": "^7.12.10",
|
||||
"babel-preset-medusa-package": "1.1.19-dev-1650573289860",
|
||||
"babel-preset-medusa-package": "1.1.19-dev-1652692202580",
|
||||
"jest": "^26.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
|
||||
|
||||
describe("GET /admin/products/:id/variants", () => {
|
||||
describe("successfully gets a product variants", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"GET",
|
||||
`/admin/products/${IdMap.getId("product1")}/variants`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should cal the get product from productService with the expected parameters without giving any config", () => {
|
||||
expect(ProductVariantServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
product_id: IdMap.getId("product1"),
|
||||
},
|
||||
{
|
||||
relations: [],
|
||||
select: ["id", "product_id"],
|
||||
skip: 0,
|
||||
take: 100
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("should returns product decorated", () => {
|
||||
expect(subject.body.variants.length).toEqual(2)
|
||||
expect(subject.body.variants).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ product_id: IdMap.getId("product1") }),
|
||||
expect.objectContaining({ product_id: IdMap.getId("product1") }),
|
||||
]))
|
||||
})
|
||||
|
||||
it("should call the get product from productService with the expected parameters including the config that has been given", async () => {
|
||||
await request(
|
||||
"GET",
|
||||
`/admin/products/${IdMap.getId("product1")}/variants`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
query: {
|
||||
expand: "variants.options",
|
||||
fields: "id, variants.id",
|
||||
limit: 10,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(ProductVariantServiceMock.listAndCount).toHaveBeenCalledTimes(2)
|
||||
expect(ProductVariantServiceMock.listAndCount).toHaveBeenLastCalledWith(
|
||||
{
|
||||
product_id: IdMap.getId("product1"),
|
||||
},
|
||||
{
|
||||
relations: ["variants.options"],
|
||||
select: ["id", "product_id", "variants.id"],
|
||||
skip: 0,
|
||||
take: 10
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ProductService } from "../../../../services"
|
||||
|
||||
/**
|
||||
* @oas [get] /products/{id}/variants
|
||||
* operationId: "GetProductsProductVariants"
|
||||
* summary: "List a Product's Product Variants"
|
||||
* description: "Retrieves a list of the Product Variants associated with a Product."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Product.
|
||||
* tags:
|
||||
* - Product
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* variants:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/product_variant"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const productService: ProductService = req.scope.resolve("productService")
|
||||
const variants = await productService.retrieveVariants(id)
|
||||
|
||||
res.json({ variants })
|
||||
}
|
||||
@@ -17,6 +17,11 @@ export default (app) => {
|
||||
middlewares.wrap(require("./list-tag-usage-count").default)
|
||||
)
|
||||
|
||||
route.get(
|
||||
"/:id/variants",
|
||||
middlewares.normalizeQuery(),
|
||||
middlewares.wrap(require("./list-variants").default)
|
||||
)
|
||||
route.post(
|
||||
"/:id/variants",
|
||||
middlewares.wrap(require("./create-variant").default)
|
||||
@@ -97,6 +102,11 @@ export const defaultAdminProductFields = [
|
||||
"metadata",
|
||||
]
|
||||
|
||||
export const defaultAdminGetProductsVariantsFields = [
|
||||
"id",
|
||||
"product_id"
|
||||
]
|
||||
|
||||
export const allowedAdminProductFields = [
|
||||
"id",
|
||||
"title",
|
||||
@@ -178,7 +188,7 @@ export * from "./delete-option"
|
||||
export * from "./delete-product"
|
||||
export * from "./delete-variant"
|
||||
export * from "./get-product"
|
||||
export * from "./get-variants"
|
||||
export * from "./list-variants"
|
||||
export * from "./list-products"
|
||||
export * from "./list-tag-usage-count"
|
||||
export * from "./list-types"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Request, Response } from "express"
|
||||
import { ProductVariantService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import { Type } from "class-transformer"
|
||||
import { getRetrieveConfig } from "../../../../utils/get-query-config"
|
||||
import { ProductVariant } from "../../../../models"
|
||||
import { defaultAdminGetProductsVariantsFields } from "./index"
|
||||
|
||||
/**
|
||||
* @oas [get] /products/{id}/variants
|
||||
* operationId: "GetProductsProductVariants"
|
||||
* summary: "List a Product's Product Variants"
|
||||
* description: "Retrieves a list of the Product Variants associated with a Product."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} Id of the product to search for the variants.
|
||||
* - (query) fields {string} Comma separated string of the column to select.
|
||||
* - (query) expand {string} Comma separated string of the relations to include.
|
||||
* - (query) offset {string} How many products to skip in the result.
|
||||
* - (query) limit {string} Limit the number of products returned.
|
||||
* tags:
|
||||
* - Product
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* variants:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/product_variant"
|
||||
*/
|
||||
export default async (req: Request, res: Response) => {
|
||||
const { id } = req.params
|
||||
|
||||
const { expand, fields, limit, offset } = await validator(
|
||||
AdminGetProductsVariantsParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const queryConfig = getRetrieveConfig<ProductVariant>(
|
||||
defaultAdminGetProductsVariantsFields as (keyof ProductVariant)[],
|
||||
[],
|
||||
[
|
||||
...new Set([
|
||||
...defaultAdminGetProductsVariantsFields,
|
||||
...(fields?.split(",") ?? []),
|
||||
]),
|
||||
] as (keyof ProductVariant)[],
|
||||
expand ? expand?.split(",") : undefined
|
||||
)
|
||||
|
||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||
"productVariantService"
|
||||
)
|
||||
const [variants, count] = await productVariantService.listAndCount(
|
||||
{
|
||||
product_id: id,
|
||||
},
|
||||
{
|
||||
...queryConfig,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
}
|
||||
)
|
||||
|
||||
res.json({
|
||||
count,
|
||||
variants,
|
||||
offset,
|
||||
limit,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetProductsVariantsParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fields?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number = 0
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number = 100
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import jwt from "jsonwebtoken"
|
||||
import { MockManager } from "medusa-test-utils"
|
||||
import "reflect-metadata"
|
||||
import supertest from "supertest"
|
||||
import querystring from "querystring"
|
||||
import apiLoader from "../loaders/api"
|
||||
import passportLoader from "../loaders/passport"
|
||||
import servicesLoader from "../loaders/services"
|
||||
@@ -68,10 +69,10 @@ apiLoader({ container, app: testApp, configModule: config })
|
||||
const supertestRequest = supertest(testApp)
|
||||
|
||||
export async function request(method, url, opts = {}) {
|
||||
let { payload, headers } = opts
|
||||
const { payload, query, headers = {} } = opts
|
||||
|
||||
const req = supertestRequest[method.toLowerCase()](url)
|
||||
headers = headers || {}
|
||||
const queryParams = query && querystring.stringify(query);
|
||||
const req = supertestRequest[method.toLowerCase()](`${url}${queryParams ? "?" + queryParams : ''}`)
|
||||
headers.Cookie = headers.Cookie || ""
|
||||
if (opts.adminSession) {
|
||||
if (opts.adminSession.jwt) {
|
||||
|
||||
@@ -302,6 +302,20 @@ export const ProductVariantServiceMock = {
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve([testVariant])
|
||||
}),
|
||||
listAndCount: jest.fn().mockImplementation(({ product_id }) => {
|
||||
if (product_id === IdMap.getId("product1")) {
|
||||
return Promise.resolve( [
|
||||
[
|
||||
{ id: IdMap.getId("1"), product_id: IdMap.getId("product1") },
|
||||
{ id: IdMap.getId("2"), product_id: IdMap.getId("product1") }
|
||||
],
|
||||
2
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
return []
|
||||
}),
|
||||
deleteOptionValue: jest.fn().mockImplementation((variantId, optionId) => {
|
||||
return Promise.resolve({})
|
||||
}),
|
||||
|
||||
@@ -108,9 +108,16 @@ export const ProductServiceMock = {
|
||||
.mockReturnValue(Promise.resolve(products.productWithOptions)),
|
||||
retrieveVariants: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve([{ id: IdMap.getId("1") }, { id: IdMap.getId("2") }])
|
||||
),
|
||||
.mockImplementation((productId) => {
|
||||
if (productId === IdMap.getId("product1")) {
|
||||
return Promise.resolve([
|
||||
{ id: IdMap.getId("1"), product_id: IdMap.getId("product1") },
|
||||
{ id: IdMap.getId("2"), product_id: IdMap.getId("product1") }
|
||||
])
|
||||
}
|
||||
|
||||
return []
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((productId) => {
|
||||
if (productId === IdMap.getId("product1")) {
|
||||
return Promise.resolve(products.product1)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets } from "typeorm"
|
||||
import { formatException } from "../utils/exception-formatter"
|
||||
import { defaultAdminProductsVariantsRelations } from "../api/routes/admin/products"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate products.
|
||||
@@ -385,10 +386,18 @@ class ProductService extends BaseService {
|
||||
/**
|
||||
* Gets all variants belonging to a product.
|
||||
* @param {string} productId - the id of the product to get variants from.
|
||||
* @param {FindConfig<Product>} config - The config to select and configure relations etc...
|
||||
* @return {Promise} an array of variants
|
||||
*/
|
||||
async retrieveVariants(productId) {
|
||||
const product = await this.retrieve(productId, { relations: ["variants"] })
|
||||
async retrieveVariants(
|
||||
productId,
|
||||
config = {
|
||||
skip: 0,
|
||||
take: 50,
|
||||
relations: defaultAdminProductsVariantsRelations,
|
||||
}
|
||||
) {
|
||||
const product = await this.retrieve(productId, config)
|
||||
return product.variants
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { Logger as _Logger } from "winston"
|
||||
import { LoggerOptions } from "typeorm"
|
||||
import { Customer, User } from "../models"
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User | Customer
|
||||
scope: MedusaContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ClassConstructor<T> = {
|
||||
new (...args: unknown[]): T
|
||||
|
||||
@@ -30,14 +30,14 @@ export function getRetrieveConfig<TModel extends BaseEntity>(
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
const fieldSet = new Set(fields)
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TModel)[]
|
||||
includeFields = Array
|
||||
.from(new Set([...fields, "id"]))
|
||||
.map(field => (typeof field === "string") ? field.trim() : field) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand
|
||||
expandFields = expand.map(expandRelation => expandRelation.trim())
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user