feat(medusa): Product category, type and tags
This commit is contained in:
committed by
GitHub
parent
2a8b556256
commit
c4d1203155
2
integration-tests/api/.gitignore
vendored
2
integration-tests/api/.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
dist
|
dist/
|
||||||
node_modules
|
node_modules
|
||||||
*yarn-error.log
|
*yarn-error.log
|
||||||
|
|
||||||
|
|||||||
205
integration-tests/api/__tests__/admin/product.js
Normal file
205
integration-tests/api/__tests__/admin/product.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
const { dropDatabase } = require("pg-god");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const setupServer = require("../../../helpers/setup-server");
|
||||||
|
const { useApi } = require("../../../helpers/use-api");
|
||||||
|
const { initDb } = require("../../../helpers/use-db");
|
||||||
|
|
||||||
|
const adminSeeder = require("../../helpers/admin-seeder");
|
||||||
|
const productSeeder = require("../../helpers/product-seeder");
|
||||||
|
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
|
||||||
|
describe("/admin/products", () => {
|
||||||
|
let medusaProcess;
|
||||||
|
let dbConnection;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."));
|
||||||
|
dbConnection = await initDb({ cwd });
|
||||||
|
medusaProcess = await setupServer({ cwd });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await dbConnection.close();
|
||||||
|
await dropDatabase({ databaseName: "medusa-integration" });
|
||||||
|
|
||||||
|
medusaProcess.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("POST /admin/products", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await productSeeder(dbConnection);
|
||||||
|
await adminSeeder(dbConnection);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const manager = dbConnection.manager;
|
||||||
|
await manager.query(`DELETE FROM "product_option_value"`);
|
||||||
|
await manager.query(`DELETE FROM "product_option"`);
|
||||||
|
await manager.query(`DELETE FROM "money_amount"`);
|
||||||
|
await manager.query(`DELETE FROM "product_variant"`);
|
||||||
|
await manager.query(`DELETE FROM "product"`);
|
||||||
|
await manager.query(`DELETE FROM "product_collection"`);
|
||||||
|
await manager.query(`DELETE FROM "product_tag"`);
|
||||||
|
await manager.query(`DELETE FROM "product_type"`);
|
||||||
|
await manager.query(
|
||||||
|
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
|
||||||
|
);
|
||||||
|
await manager.query(`DELETE FROM "region"`);
|
||||||
|
await manager.query(`DELETE FROM "user"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a product", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "Test product",
|
||||||
|
description: "test-product-description",
|
||||||
|
type: { value: "test-type" },
|
||||||
|
collection_id: "test-collection",
|
||||||
|
tags: [{ value: "123" }, { value: "456" }],
|
||||||
|
options: [{ title: "size" }, { title: "color" }],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
prices: [{ currency_code: "usd", amount: 100 }],
|
||||||
|
options: [{ value: "large" }, { value: "green" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post("/admin/products", payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test product",
|
||||||
|
handle: "test-product",
|
||||||
|
tags: [
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "123",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "456",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
type: expect.objectContaining({
|
||||||
|
value: "test-type",
|
||||||
|
}),
|
||||||
|
collection: expect.objectContaining({
|
||||||
|
id: "test-collection",
|
||||||
|
title: "Test collection",
|
||||||
|
}),
|
||||||
|
options: [
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "size",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "color",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "Test variant",
|
||||||
|
prices: [
|
||||||
|
expect.objectContaining({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 100,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "large",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "green",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates a product (update tags, delete collection, delete type)", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
collection_id: null,
|
||||||
|
type: null,
|
||||||
|
tags: [{ value: "123" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post("/admin/products/test-product", payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
tags: [
|
||||||
|
expect.objectContaining({
|
||||||
|
value: "123",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
type: null,
|
||||||
|
collection: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add option", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: "should_add",
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await api
|
||||||
|
.post("/admin/products/test-product/options", payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
|
expect(response.data.product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
options: [
|
||||||
|
expect.objectContaining({
|
||||||
|
title: "should_add",
|
||||||
|
product_id: "test-product",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
68
integration-tests/api/helpers/product-seeder.js
Normal file
68
integration-tests/api/helpers/product-seeder.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
const {
|
||||||
|
ProductCollection,
|
||||||
|
ProductTag,
|
||||||
|
ProductType,
|
||||||
|
Region,
|
||||||
|
Product,
|
||||||
|
ShippingProfile,
|
||||||
|
ProductVariant,
|
||||||
|
} = require("@medusajs/medusa");
|
||||||
|
|
||||||
|
module.exports = async (connection, data = {}) => {
|
||||||
|
const manager = connection.manager;
|
||||||
|
|
||||||
|
const defaultProfile = await manager.findOne(ShippingProfile, {
|
||||||
|
type: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const coll = manager.create(ProductCollection, {
|
||||||
|
id: "test-collection",
|
||||||
|
title: "Test collection",
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.save(coll);
|
||||||
|
|
||||||
|
const tag = manager.create(ProductTag, {
|
||||||
|
id: "tag1",
|
||||||
|
value: "123",
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.save(tag);
|
||||||
|
|
||||||
|
const type = manager.create(ProductType, {
|
||||||
|
id: "test-type",
|
||||||
|
value: "test-type",
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.save(type);
|
||||||
|
|
||||||
|
await manager.insert(Region, {
|
||||||
|
id: "test-region",
|
||||||
|
name: "Test Region",
|
||||||
|
currency_code: "usd",
|
||||||
|
tax_rate: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.insert(Product, {
|
||||||
|
id: "test-product",
|
||||||
|
title: "Test product",
|
||||||
|
profile_id: defaultProfile.id,
|
||||||
|
description: "test-product-description",
|
||||||
|
collection_id: "test-collection",
|
||||||
|
type: { id: "test-type", value: "test-type" },
|
||||||
|
tags: [
|
||||||
|
{ id: "tag1", value: "123" },
|
||||||
|
{ tag: "tag2", value: "456" },
|
||||||
|
],
|
||||||
|
options: [{ id: "test-option", title: "Default value" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.insert(ProductVariant, {
|
||||||
|
id: "test-variant",
|
||||||
|
inventory_quantity: 10,
|
||||||
|
title: "Test variant",
|
||||||
|
product_id: "test-product",
|
||||||
|
prices: [{ id: "test-price", currency_code: "usd", amount: 100 }],
|
||||||
|
options: [{ id: "test-variant-option", value: "Default variant" }],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
"@babel/node": "^7.12.10",
|
"@babel/node": "^7.12.10",
|
||||||
"babel-preset-medusa-package": "^1.1.0"
|
"babel-preset-medusa-package": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,4 +18,5 @@ module.exports = {
|
|||||||
`.cache`,
|
`.cache`,
|
||||||
],
|
],
|
||||||
transform: { "^.+\\.[jt]s$": `<rootDir>/jest-transformer.js` },
|
transform: { "^.+\\.[jt]s$": `<rootDir>/jest-transformer.js` },
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
|
||||||
};
|
};
|
||||||
|
|||||||
5
integration-tests/setup.js
Normal file
5
integration-tests/setup.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const { dropDatabase } = require("pg-god");
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
dropDatabase({ databaseName: "medusa-integration" });
|
||||||
|
});
|
||||||
1
packages/medusa-plugin-contentful/.gitignore
vendored
1
packages/medusa-plugin-contentful/.gitignore
vendored
@@ -12,4 +12,5 @@ yarn.lock
|
|||||||
/services
|
/services
|
||||||
/models
|
/models
|
||||||
/subscribers
|
/subscribers
|
||||||
|
/loaders
|
||||||
|
|
||||||
|
|||||||
59
packages/medusa-plugin-contentful/src/loaders/check-types.js
Normal file
59
packages/medusa-plugin-contentful/src/loaders/check-types.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const checkContentTypes = async (container) => {
|
||||||
|
const contentfulService = container.resolve("contentfulService")
|
||||||
|
|
||||||
|
let product
|
||||||
|
let variant
|
||||||
|
|
||||||
|
try {
|
||||||
|
product = await contentfulService.getType("product")
|
||||||
|
variant = await contentfulService.getType("productVariant")
|
||||||
|
} catch (error) {
|
||||||
|
if (!product) {
|
||||||
|
throw Error("Content type: `product` is missing in Contentful")
|
||||||
|
}
|
||||||
|
if (!variant) {
|
||||||
|
throw Error("Content type: `productVariant` is missing in Contentful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product && product.fields) {
|
||||||
|
const productFields = product.fields
|
||||||
|
|
||||||
|
const keys = Object.values(productFields).map((f) => f.id)
|
||||||
|
if (!requiredProductFields.every((f) => keys.includes(f))) {
|
||||||
|
throw Error(
|
||||||
|
`Contentful: Content type ${`product`} is missing some required key(s). Required: ${requiredProductFields.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variant && variant.fields) {
|
||||||
|
const variantFields = variant.fields
|
||||||
|
|
||||||
|
const keys = Object.values(variantFields).map((f) => f.id)
|
||||||
|
if (!requiredVariantFields.every((f) => keys.includes(f))) {
|
||||||
|
throw Error(
|
||||||
|
`Contentful: Content type ${`productVariant`} is missing some required key(s). Required: ${requiredVariantFields.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredProductFields = [
|
||||||
|
"title",
|
||||||
|
"variants",
|
||||||
|
"options",
|
||||||
|
"objectId",
|
||||||
|
"type",
|
||||||
|
"collection",
|
||||||
|
"tags",
|
||||||
|
"handle",
|
||||||
|
]
|
||||||
|
|
||||||
|
const requiredVariantFields = ["title", "sku", "prices", "options", "objectId"]
|
||||||
|
|
||||||
|
export default checkContentTypes
|
||||||
@@ -114,28 +114,58 @@ class ContentfulService extends BaseService {
|
|||||||
async createProductInContentful(product) {
|
async createProductInContentful(product) {
|
||||||
try {
|
try {
|
||||||
const p = await this.productService_.retrieve(product.id, {
|
const p = await this.productService_.retrieve(product.id, {
|
||||||
relations: ["variants", "options"],
|
relations: ["variants", "options", "tags", "type", "collection"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const environment = await this.getContentfulEnvironment_()
|
const environment = await this.getContentfulEnvironment_()
|
||||||
const variantEntries = await this.getVariantEntries_(p.variants)
|
const variantEntries = await this.getVariantEntries_(p.variants)
|
||||||
const variantLinks = this.getVariantLinks_(variantEntries)
|
const variantLinks = this.getVariantLinks_(variantEntries)
|
||||||
|
|
||||||
const result = await environment.createEntryWithId("product", p.id, {
|
const fields = {
|
||||||
fields: {
|
title: {
|
||||||
title: {
|
"en-US": p.title,
|
||||||
"en-US": p.title,
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
"en-US": variantLinks,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
"en-US": p.options,
|
|
||||||
},
|
|
||||||
objectId: {
|
|
||||||
"en-US": p.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
variants: {
|
||||||
|
"en-US": variantLinks,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
"en-US": p.options,
|
||||||
|
},
|
||||||
|
objectId: {
|
||||||
|
"en-US": p.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.type) {
|
||||||
|
const type = {
|
||||||
|
"en-US": p.type.value,
|
||||||
|
}
|
||||||
|
fields.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.collection) {
|
||||||
|
const collection = {
|
||||||
|
"en-US": p.collection.title,
|
||||||
|
}
|
||||||
|
fields.collection = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.tags) {
|
||||||
|
const tags = {
|
||||||
|
"en-US": p.tags,
|
||||||
|
}
|
||||||
|
fields.tags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.handle) {
|
||||||
|
const handle = {
|
||||||
|
"en-US": p.handle,
|
||||||
|
}
|
||||||
|
fields.handle = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await environment.createEntryWithId("product", p.id, {
|
||||||
|
fields,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
||||||
@@ -210,7 +240,7 @@ class ContentfulService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = await this.productService_.retrieve(product.id, {
|
const p = await this.productService_.retrieve(product.id, {
|
||||||
relations: ["options", "variants"],
|
relations: ["options", "variants", "type", "collection", "tags"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const variantEntries = await this.getVariantEntries_(p.variants)
|
const variantEntries = await this.getVariantEntries_(p.variants)
|
||||||
@@ -232,6 +262,34 @@ class ContentfulService extends BaseService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p.type) {
|
||||||
|
const type = {
|
||||||
|
"en-US": p.type.value,
|
||||||
|
}
|
||||||
|
productEntryFields.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.collection) {
|
||||||
|
const collection = {
|
||||||
|
"en-US": p.collection.title,
|
||||||
|
}
|
||||||
|
productEntryFields.collection = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.tags) {
|
||||||
|
const tags = {
|
||||||
|
"en-US": p.tags,
|
||||||
|
}
|
||||||
|
productEntryFields.tags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.handle) {
|
||||||
|
const handle = {
|
||||||
|
"en-US": p.handle,
|
||||||
|
}
|
||||||
|
productEntryFields.handle = handle
|
||||||
|
}
|
||||||
|
|
||||||
productEntry.fields = productEntryFields
|
productEntry.fields = productEntryFields
|
||||||
|
|
||||||
const updatedEntry = await productEntry.update()
|
const updatedEntry = await productEntry.update()
|
||||||
@@ -372,6 +430,11 @@ class ContentfulService extends BaseService {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getType(type) {
|
||||||
|
const environment = await this.getContentfulEnvironment_()
|
||||||
|
return environment.getContentType(type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContentfulService
|
export default ContentfulService
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-classes": "^7.9.5",
|
"@babel/plugin-transform-classes": "^7.9.5",
|
||||||
"@hapi/joi": "^16.1.8",
|
"@hapi/joi": "^16.1.8",
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
"awilix": "^4.2.3",
|
"awilix": "^4.2.3",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bull": "^3.12.1",
|
"bull": "^3.12.1",
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("POST /admin/collections", () => {
|
||||||
|
describe("successful creation", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request("POST", "/admin/collections", {
|
||||||
|
payload: {
|
||||||
|
title: "Suits",
|
||||||
|
handle: "suits",
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns created product collection", () => {
|
||||||
|
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls production collection service create", () => {
|
||||||
|
expect(ProductCollectionServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ProductCollectionServiceMock.create).toHaveBeenCalledWith({
|
||||||
|
title: "Suits",
|
||||||
|
handle: "suits",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("invalid data returns error details", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request("POST", "/admin/collections", {
|
||||||
|
payload: {
|
||||||
|
handle: "no-title-collection",
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 400", () => {
|
||||||
|
expect(subject.status).toEqual(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns error details", () => {
|
||||||
|
expect(subject.body.name).toEqual("invalid_data")
|
||||||
|
expect(subject.body.message[0].message).toEqual(`"title" is required`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("DELETE /admin/collections/:id", () => {
|
||||||
|
describe("successful removes collection", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"DELETE",
|
||||||
|
`/admin/collections/${IdMap.getId("collection")}`,
|
||||||
|
{
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls product collection service delete", () => {
|
||||||
|
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("collection")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns delete result", () => {
|
||||||
|
expect(subject.body).toEqual({
|
||||||
|
id: IdMap.getId("collection"),
|
||||||
|
object: "product-collection",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("GET /admin/categories/:id", () => {
|
||||||
|
describe("get collection by id successfully", () => {
|
||||||
|
let subject
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"GET",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}`,
|
||||||
|
{
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls retrieve from product collection service", () => {
|
||||||
|
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("col")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns variant decorated", () => {
|
||||||
|
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("GET /admin/collections", () => {
|
||||||
|
describe("successful retrieval", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
subject = await request("GET", `/admin/collections`, {
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls product collection service list", () => {
|
||||||
|
expect(ProductCollectionServiceMock.list).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import { request } from "../../../../../helpers/test-request"
|
||||||
|
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||||
|
|
||||||
|
describe("POST /admin/collections/:id", () => {
|
||||||
|
describe("successful update", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
subject = await request(
|
||||||
|
"POST",
|
||||||
|
`/admin/collections/${IdMap.getId("col")}`,
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
title: "Suits and vests",
|
||||||
|
},
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns updated product collection", () => {
|
||||||
|
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("product collection service update", () => {
|
||||||
|
expect(ProductCollectionServiceMock.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(ProductCollectionServiceMock.update).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("col"),
|
||||||
|
{
|
||||||
|
title: "Suits and vests",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { MedusaError, Validator } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
title: Validator.string().required(),
|
||||||
|
handle: Validator.string()
|
||||||
|
.optional()
|
||||||
|
.allow(""),
|
||||||
|
metadata: Validator.object().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const productCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const created = await productCollectionService.create(value)
|
||||||
|
const collection = await productCollectionService.retrieve(created.id)
|
||||||
|
|
||||||
|
res.status(200).json({ collection })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const productCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
await productCollectionService.delete(id)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id,
|
||||||
|
object: "product-collection",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
try {
|
||||||
|
const productCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const collection = await productCollectionService.retrieve(id)
|
||||||
|
res.status(200).json({ collection })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/medusa/src/api/routes/admin/collections/index.js
Normal file
21
packages/medusa/src/api/routes/admin/collections/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import middlewares from "../../../middlewares"
|
||||||
|
|
||||||
|
const route = Router()
|
||||||
|
|
||||||
|
export default app => {
|
||||||
|
app.use("/collections", route)
|
||||||
|
|
||||||
|
route.post("/", middlewares.wrap(require("./create-collection").default))
|
||||||
|
route.post("/:id", middlewares.wrap(require("./update-collection").default))
|
||||||
|
|
||||||
|
route.delete("/:id", middlewares.wrap(require("./delete-collection").default))
|
||||||
|
|
||||||
|
route.get("/:id", middlewares.wrap(require("./get-collection").default))
|
||||||
|
route.get("/", middlewares.wrap(require("./list-collections").default))
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFields = ["id", "title", "handle"]
|
||||||
|
export const defaultRelations = ["products"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { defaultFields, defaultRelations } from "."
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
try {
|
||||||
|
const selector = {}
|
||||||
|
|
||||||
|
const limit = parseInt(req.query.limit) || 10
|
||||||
|
const offset = parseInt(req.query.offset) || 0
|
||||||
|
|
||||||
|
const productCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const listConfig = {
|
||||||
|
select: defaultFields,
|
||||||
|
relations: defaultRelations,
|
||||||
|
skip: offset,
|
||||||
|
take: limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
const collections = await productCollectionService.list(
|
||||||
|
selector,
|
||||||
|
listConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ collections })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { MedusaError, Validator } from "medusa-core-utils"
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const schema = Validator.object().keys({
|
||||||
|
title: Validator.string().optional(),
|
||||||
|
handle: Validator.string().optional(),
|
||||||
|
metadata: Validator.object().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(req.body)
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const productCollectionService = req.scope.resolve(
|
||||||
|
"productCollectionService"
|
||||||
|
)
|
||||||
|
|
||||||
|
const updated = await productCollectionService.update(id, value)
|
||||||
|
const collection = await productCollectionService.retrieve(updated.id)
|
||||||
|
|
||||||
|
res.status(200).json({ collection })
|
||||||
|
} catch (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import appRoutes from "./apps"
|
|||||||
import swapRoutes from "./swaps"
|
import swapRoutes from "./swaps"
|
||||||
import returnRoutes from "./returns"
|
import returnRoutes from "./returns"
|
||||||
import variantRoutes from "./variants"
|
import variantRoutes from "./variants"
|
||||||
|
import collectionRoutes from "./collections"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ export default (app, container, config) => {
|
|||||||
swapRoutes(route)
|
swapRoutes(route)
|
||||||
returnRoutes(route)
|
returnRoutes(route)
|
||||||
variantRoutes(route)
|
variantRoutes(route)
|
||||||
|
collectionRoutes(route)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("POST /admin/products", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
title: "Test Product",
|
title: "Test Product",
|
||||||
description: "Test Description",
|
description: "Test Description",
|
||||||
tags: "hi,med,dig",
|
tags: [{ id: "test", value: "test" }],
|
||||||
handle: "test-product",
|
handle: "test-product",
|
||||||
},
|
},
|
||||||
adminSession: {
|
adminSession: {
|
||||||
@@ -36,7 +36,7 @@ describe("POST /admin/products", () => {
|
|||||||
expect(ProductServiceMock.create).toHaveBeenCalledWith({
|
expect(ProductServiceMock.create).toHaveBeenCalledWith({
|
||||||
title: "Test Product",
|
title: "Test Product",
|
||||||
description: "Test Description",
|
description: "Test Description",
|
||||||
tags: "hi,med,dig",
|
tags: [{ id: "test", value: "test" }],
|
||||||
handle: "test-product",
|
handle: "test-product",
|
||||||
is_giftcard: false,
|
is_giftcard: false,
|
||||||
profile_id: IdMap.getId("default_shipping_profile"),
|
profile_id: IdMap.getId("default_shipping_profile"),
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ describe("GET /admin/products/:id", () => {
|
|||||||
"title",
|
"title",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"description",
|
"description",
|
||||||
"tags",
|
|
||||||
"handle",
|
"handle",
|
||||||
"is_giftcard",
|
"is_giftcard",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"profile_id",
|
"profile_id",
|
||||||
|
"collection_id",
|
||||||
|
"type_id",
|
||||||
"weight",
|
"weight",
|
||||||
"length",
|
"length",
|
||||||
"height",
|
"height",
|
||||||
@@ -57,6 +58,9 @@ describe("GET /admin/products/:id", () => {
|
|||||||
"variants.options",
|
"variants.options",
|
||||||
"images",
|
"images",
|
||||||
"options",
|
"options",
|
||||||
|
"tags",
|
||||||
|
"type",
|
||||||
|
"collection",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,13 +6,28 @@ export default async (req, res) => {
|
|||||||
title: Validator.string().required(),
|
title: Validator.string().required(),
|
||||||
subtitle: Validator.string().allow(""),
|
subtitle: Validator.string().allow(""),
|
||||||
description: Validator.string().allow(""),
|
description: Validator.string().allow(""),
|
||||||
tags: Validator.string().optional(),
|
|
||||||
is_giftcard: Validator.boolean().default(false),
|
is_giftcard: Validator.boolean().default(false),
|
||||||
images: Validator.array()
|
images: Validator.array()
|
||||||
.items(Validator.string())
|
.items(Validator.string())
|
||||||
.optional(),
|
.optional(),
|
||||||
thumbnail: Validator.string().optional(),
|
thumbnail: Validator.string().optional(),
|
||||||
handle: Validator.string().optional(),
|
handle: Validator.string().optional(),
|
||||||
|
type: Validator.object()
|
||||||
|
.keys({
|
||||||
|
id: Validator.string().optional(),
|
||||||
|
value: Validator.string().required(),
|
||||||
|
})
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
collection_id: Validator.string()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
tags: Validator.array()
|
||||||
|
.items({
|
||||||
|
id: Validator.string().optional(),
|
||||||
|
value: Validator.string().required(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
options: Validator.array().items({
|
options: Validator.array().items({
|
||||||
title: Validator.string().required(),
|
title: Validator.string().required(),
|
||||||
}),
|
}),
|
||||||
@@ -48,7 +63,7 @@ export default async (req, res) => {
|
|||||||
Validator.object()
|
Validator.object()
|
||||||
.keys({
|
.keys({
|
||||||
region_id: Validator.string(),
|
region_id: Validator.string(),
|
||||||
currency_code: Validator.string().required(),
|
currency_code: Validator.string(),
|
||||||
amount: Validator.number()
|
amount: Validator.number()
|
||||||
.integer()
|
.integer()
|
||||||
.required(),
|
.required(),
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export default app => {
|
|||||||
|
|
||||||
route.post("/", middlewares.wrap(require("./create-product").default))
|
route.post("/", middlewares.wrap(require("./create-product").default))
|
||||||
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
||||||
|
route.get("/types", middlewares.wrap(require("./list-types").default))
|
||||||
|
route.get(
|
||||||
|
"/tag-usage",
|
||||||
|
middlewares.wrap(require("./list-tag-usage-count").default)
|
||||||
|
)
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
"/:id/variants",
|
"/:id/variants",
|
||||||
@@ -52,6 +57,9 @@ export const defaultRelations = [
|
|||||||
"variants.options",
|
"variants.options",
|
||||||
"images",
|
"images",
|
||||||
"options",
|
"options",
|
||||||
|
"tags",
|
||||||
|
"type",
|
||||||
|
"collection",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultFields = [
|
export const defaultFields = [
|
||||||
@@ -59,11 +67,12 @@ export const defaultFields = [
|
|||||||
"title",
|
"title",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"description",
|
"description",
|
||||||
"tags",
|
|
||||||
"handle",
|
"handle",
|
||||||
"is_giftcard",
|
"is_giftcard",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"profile_id",
|
"profile_id",
|
||||||
|
"collection_id",
|
||||||
|
"type_id",
|
||||||
"weight",
|
"weight",
|
||||||
"length",
|
"length",
|
||||||
"height",
|
"height",
|
||||||
@@ -82,11 +91,12 @@ export const allowedFields = [
|
|||||||
"title",
|
"title",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"description",
|
"description",
|
||||||
"tags",
|
|
||||||
"handle",
|
"handle",
|
||||||
"is_giftcard",
|
"is_giftcard",
|
||||||
"thumbnail",
|
"thumbnail",
|
||||||
"profile_id",
|
"profile_id",
|
||||||
|
"collection_id",
|
||||||
|
"type_id",
|
||||||
"weight",
|
"weight",
|
||||||
"length",
|
"length",
|
||||||
"height",
|
"height",
|
||||||
@@ -105,4 +115,7 @@ export const allowedRelations = [
|
|||||||
"variants.prices",
|
"variants.prices",
|
||||||
"images",
|
"images",
|
||||||
"options",
|
"options",
|
||||||
|
"tags",
|
||||||
|
"type",
|
||||||
|
"collection",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
try {
|
||||||
|
const productService = req.scope.resolve("productService")
|
||||||
|
|
||||||
|
const tags = await productService.listTagsByUsage()
|
||||||
|
|
||||||
|
res.json({ tags })
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/medusa/src/api/routes/admin/products/list-types.js
Normal file
11
packages/medusa/src/api/routes/admin/products/list-types.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default async (req, res) => {
|
||||||
|
try {
|
||||||
|
const productService = req.scope.resolve("productService")
|
||||||
|
|
||||||
|
const types = await productService.listTypes()
|
||||||
|
|
||||||
|
res.json({ types })
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,22 @@ export default async (req, res) => {
|
|||||||
const schema = Validator.object().keys({
|
const schema = Validator.object().keys({
|
||||||
title: Validator.string().optional(),
|
title: Validator.string().optional(),
|
||||||
description: Validator.string().optional(),
|
description: Validator.string().optional(),
|
||||||
tags: Validator.string().optional(),
|
type: Validator.object()
|
||||||
|
.keys({
|
||||||
|
id: Validator.string().optional(),
|
||||||
|
value: Validator.string().required(),
|
||||||
|
})
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
collection_id: Validator.string()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
tags: Validator.array()
|
||||||
|
.items({
|
||||||
|
id: Validator.string().optional(),
|
||||||
|
value: Validator.string().required(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
handle: Validator.string().optional(),
|
handle: Validator.string().optional(),
|
||||||
weight: Validator.number().optional(),
|
weight: Validator.number().optional(),
|
||||||
length: Validator.number().optional(),
|
length: Validator.number().optional(),
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ export { Order } from "./models/order"
|
|||||||
export { PaymentProvider } from "./models/payment-provider"
|
export { PaymentProvider } from "./models/payment-provider"
|
||||||
export { PaymentSession } from "./models/payment-session"
|
export { PaymentSession } from "./models/payment-session"
|
||||||
export { Payment } from "./models/payment"
|
export { Payment } from "./models/payment"
|
||||||
export { ProductOptionValue } from "./models/product-option-value"
|
|
||||||
export { ProductOption } from "./models/product-option"
|
|
||||||
export { ProductVariant } from "./models/product-variant"
|
|
||||||
export { Product } from "./models/product"
|
export { Product } from "./models/product"
|
||||||
|
export { ProductCollection } from "./models/product-collection"
|
||||||
|
export { ProductOption } from "./models/product-option"
|
||||||
|
export { ProductOptionValue } from "./models/product-option-value"
|
||||||
|
export { ProductVariant } from "./models/product-variant"
|
||||||
|
export { ProductTag } from "./models/product-tag"
|
||||||
|
export { ProductType } from "./models/product-type"
|
||||||
export { Refund } from "./models/refund"
|
export { Refund } from "./models/refund"
|
||||||
export { Region } from "./models/region"
|
export { Region } from "./models/region"
|
||||||
export { ReturnItem } from "./models/return-item"
|
export { ReturnItem } from "./models/return-item"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class productTypeCategoryTags1611909563253
|
||||||
|
implements MigrationInterface {
|
||||||
|
name = "productTypeCategoryTags1611909563253"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "product_collection" ("id" character varying NOT NULL, "title" character varying NOT NULL, "handle" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_49d419fc77d3aed46c835c558ac" PRIMARY KEY ("id"))`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "IDX_6910923cb678fd6e99011a21cc" ON "product_collection" ("handle") `
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "product_tag" ("id" character varying NOT NULL, "value" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_1439455c6528caa94fcc8564fda" PRIMARY KEY ("id"))`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "product_type" ("id" character varying NOT NULL, "value" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_e0843930fbb8854fe36ca39dae1" PRIMARY KEY ("id"))`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "product_tags" ("product_id" character varying NOT NULL, "product_tag_id" character varying NOT NULL, CONSTRAINT "PK_1cf5c9537e7198df494b71b993f" PRIMARY KEY ("product_id", "product_tag_id"))`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_5b0c6fc53c574299ecc7f9ee22" ON "product_tags" ("product_id") `
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_21683a063fe82dafdf681ecc9c" ON "product_tags" ("product_tag_id") `
|
||||||
|
)
|
||||||
|
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "tags"`)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" ADD "collection_id" character varying`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" ADD "type_id" character varying`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" ADD CONSTRAINT "FK_49d419fc77d3aed46c835c558ac" FOREIGN KEY ("collection_id") REFERENCES "product_collection"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product_tags" ADD CONSTRAINT "FK_5b0c6fc53c574299ecc7f9ee22e" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product_tags" ADD CONSTRAINT "FK_21683a063fe82dafdf681ecc9c4" FOREIGN KEY ("product_tag_id") REFERENCES "product_tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" ADD CONSTRAINT "FK_e0843930fbb8854fe36ca39dae1" FOREIGN KEY ("type_id") REFERENCES "product_type"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product_tags" DROP CONSTRAINT "FK_21683a063fe82dafdf681ecc9c4"`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product_tags" DROP CONSTRAINT "FK_5b0c6fc53c574299ecc7f9ee22e"`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" DROP CONSTRAINT "FK_49d419fc77d3aed46c835c558ac"`
|
||||||
|
)
|
||||||
|
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "type_id"`)
|
||||||
|
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "collection_id"`)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" DROP CONSTRAINT "FK_e0843930fbb8854fe36ca39dae1"`
|
||||||
|
)
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "product" ADD "tags" character varying`
|
||||||
|
)
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_21683a063fe82dafdf681ecc9c"`)
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_5b0c6fc53c574299ecc7f9ee22"`)
|
||||||
|
await queryRunner.query(`DROP TABLE "product_tags"`)
|
||||||
|
await queryRunner.query(`DROP TABLE "product_type"`)
|
||||||
|
await queryRunner.query(`DROP TABLE "product_tag"`)
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_6910923cb678fd6e99011a21cc"`)
|
||||||
|
await queryRunner.query(`DROP TABLE "product_collection"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
57
packages/medusa/src/models/product-collection.ts
Normal file
57
packages/medusa/src/models/product-collection.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
BeforeInsert,
|
||||||
|
DeleteDateColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Column,
|
||||||
|
PrimaryColumn,
|
||||||
|
ManyToMany,
|
||||||
|
Index,
|
||||||
|
OneToMany,
|
||||||
|
} from "typeorm"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
import { Product } from "./product"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class ProductCollection {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string
|
||||||
|
|
||||||
|
@Index({ unique: true })
|
||||||
|
@Column({ nullable: true })
|
||||||
|
handle: string
|
||||||
|
|
||||||
|
@OneToMany(
|
||||||
|
() => Product,
|
||||||
|
product => product.collection
|
||||||
|
)
|
||||||
|
products: Product[]
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: "timestamptz" })
|
||||||
|
created_at: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: "timestamptz" })
|
||||||
|
updated_at: Date
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: "timestamptz" })
|
||||||
|
deleted_at: Date
|
||||||
|
|
||||||
|
@Column({ type: "jsonb", nullable: true })
|
||||||
|
metadata: any
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
private beforeInsert() {
|
||||||
|
if (this.id) return
|
||||||
|
const id = ulid()
|
||||||
|
this.id = `pcol_${id}`
|
||||||
|
|
||||||
|
if (!this.handle) {
|
||||||
|
this.handle = _.kebabCase(this.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ export class ProductOption {
|
|||||||
)
|
)
|
||||||
values: ProductOptionValue
|
values: ProductOptionValue
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
product_id: string
|
||||||
|
|
||||||
@ManyToOne(
|
@ManyToOne(
|
||||||
() => Product,
|
() => Product,
|
||||||
product => product.options
|
product => product.options
|
||||||
|
|||||||
38
packages/medusa/src/models/product-tag.ts
Normal file
38
packages/medusa/src/models/product-tag.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
BeforeInsert,
|
||||||
|
DeleteDateColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Column,
|
||||||
|
PrimaryColumn,
|
||||||
|
} from "typeorm"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class ProductTag {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
value: string
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: "timestamptz" })
|
||||||
|
created_at: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: "timestamptz" })
|
||||||
|
updated_at: Date
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: "timestamptz" })
|
||||||
|
deleted_at: Date
|
||||||
|
|
||||||
|
@Column({ type: "jsonb", nullable: true })
|
||||||
|
metadata: any
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
private beforeInsert() {
|
||||||
|
if (this.id) return
|
||||||
|
const id = ulid()
|
||||||
|
this.id = `ptag_${id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/medusa/src/models/product-type.ts
Normal file
38
packages/medusa/src/models/product-type.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
BeforeInsert,
|
||||||
|
DeleteDateColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Column,
|
||||||
|
PrimaryColumn,
|
||||||
|
} from "typeorm"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class ProductType {
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
value: string
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: "timestamptz" })
|
||||||
|
created_at: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: "timestamptz" })
|
||||||
|
updated_at: Date
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: "timestamptz" })
|
||||||
|
deleted_at: Date
|
||||||
|
|
||||||
|
@Column({ type: "jsonb", nullable: true })
|
||||||
|
metadata: any
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
private beforeInsert() {
|
||||||
|
if (this.id) return
|
||||||
|
const id = ulid()
|
||||||
|
this.id = `ptyp_${id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,13 @@ import {
|
|||||||
import { ulid } from "ulid"
|
import { ulid } from "ulid"
|
||||||
|
|
||||||
import { Image } from "./image"
|
import { Image } from "./image"
|
||||||
|
import { ProductCollection } from "./product-collection"
|
||||||
import { ProductOption } from "./product-option"
|
import { ProductOption } from "./product-option"
|
||||||
|
import { ProductTag } from "./product-tag"
|
||||||
|
import { ProductType } from "./product-type"
|
||||||
import { ProductVariant } from "./product-variant"
|
import { ProductVariant } from "./product-variant"
|
||||||
import { ShippingProfile } from "./shipping-profile"
|
import { ShippingProfile } from "./shipping-profile"
|
||||||
|
import _ from "lodash"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Product {
|
export class Product {
|
||||||
@@ -35,9 +39,6 @@ export class Product {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description: string
|
description: string
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
tags: string
|
|
||||||
|
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
handle: string
|
handle: string
|
||||||
@@ -107,6 +108,34 @@ export class Product {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
material: string
|
material: string
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
collection_id: string
|
||||||
|
|
||||||
|
@ManyToOne(() => ProductCollection)
|
||||||
|
@JoinColumn({ name: "collection_id" })
|
||||||
|
collection: ProductCollection
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
type_id: string
|
||||||
|
|
||||||
|
@ManyToOne(() => ProductType)
|
||||||
|
@JoinColumn({ name: "type_id" })
|
||||||
|
type: ProductType
|
||||||
|
|
||||||
|
@ManyToMany(() => ProductTag)
|
||||||
|
@JoinTable({
|
||||||
|
name: "product_tags",
|
||||||
|
joinColumn: {
|
||||||
|
name: "product_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: "product_tag_id",
|
||||||
|
referencedColumnName: "id",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
tags: ProductTag[]
|
||||||
|
|
||||||
@CreateDateColumn({ type: "timestamptz" })
|
@CreateDateColumn({ type: "timestamptz" })
|
||||||
created_at: Date
|
created_at: Date
|
||||||
|
|
||||||
@@ -124,5 +153,9 @@ export class Product {
|
|||||||
if (this.id) return
|
if (this.id) return
|
||||||
const id = ulid()
|
const id = ulid()
|
||||||
this.id = `prod_${id}`
|
this.id = `prod_${id}`
|
||||||
|
|
||||||
|
if (!this.handle) {
|
||||||
|
this.handle = _.kebabCase(this.title)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/medusa/src/repositories/product-collection.ts
Normal file
7
packages/medusa/src/repositories/product-collection.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { EntityRepository, Repository } from "typeorm"
|
||||||
|
import { ProductCollection } from "../models/product-collection"
|
||||||
|
|
||||||
|
@EntityRepository(ProductCollection)
|
||||||
|
export class ProductCollectionRepository extends Repository<
|
||||||
|
ProductCollection
|
||||||
|
> {}
|
||||||
5
packages/medusa/src/repositories/product-tag.ts
Normal file
5
packages/medusa/src/repositories/product-tag.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { EntityRepository, Repository } from "typeorm"
|
||||||
|
import { ProductTag } from "../models/product-tag"
|
||||||
|
|
||||||
|
@EntityRepository(ProductTag)
|
||||||
|
export class ProductTagRepository extends Repository<ProductTag> {}
|
||||||
5
packages/medusa/src/repositories/product-type.ts
Normal file
5
packages/medusa/src/repositories/product-type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { EntityRepository, Repository } from "typeorm"
|
||||||
|
import { ProductType } from "../models/product-type"
|
||||||
|
|
||||||
|
@EntityRepository(ProductType)
|
||||||
|
export class ProductTypeRepository extends Repository<ProductType> {}
|
||||||
28
packages/medusa/src/services/__mocks__/product-collection.js
Normal file
28
packages/medusa/src/services/__mocks__/product-collection.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const ProductCollectionServiceMock = {
|
||||||
|
withTransaction: function() {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
create: jest.fn().mockImplementation(data => {
|
||||||
|
return Promise.resolve({ id: IdMap.getId("col"), ...data })
|
||||||
|
}),
|
||||||
|
retrieve: jest.fn().mockImplementation(id => {
|
||||||
|
if (id === IdMap.getId("col")) {
|
||||||
|
return Promise.resolve({ id: IdMap.getId("col"), title: "Suits" })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
update: jest.fn().mockImplementation((id, value) => {
|
||||||
|
return Promise.resolve({ id, title: value })
|
||||||
|
}),
|
||||||
|
list: jest.fn().mockImplementation(data => {
|
||||||
|
return Promise.resolve([{ id: IdMap.getId("col"), title: "Suits" }])
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return ProductCollectionServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
149
packages/medusa/src/services/__tests__/product-collection.js
Normal file
149
packages/medusa/src/services/__tests__/product-collection.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||||
|
import ProductCollectionService from "../product-collection"
|
||||||
|
|
||||||
|
describe("ProductCollectionService", () => {
|
||||||
|
describe("retrieve", () => {
|
||||||
|
const productCollectionRepository = MockRepository({
|
||||||
|
findOne: query => {
|
||||||
|
if (query.where.id === "non-existing") {
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}
|
||||||
|
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const productCollectionService = new ProductCollectionService({
|
||||||
|
manager: MockManager,
|
||||||
|
productCollectionRepository,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully retrieves a product collection", async () => {
|
||||||
|
const result = await productCollectionService.retrieve(
|
||||||
|
IdMap.getId("bathrobe")
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(productCollectionRepository.findOne).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productCollectionRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: { id: IdMap.getId("bathrobe") },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.id).toEqual(IdMap.getId("bathrobe"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails on non-existing product collection id", async () => {
|
||||||
|
try {
|
||||||
|
await productCollectionService.retrieve("non-existing")
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toBe(
|
||||||
|
`Product collection with id: non-existing was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
const productCollectionRepository = MockRepository({
|
||||||
|
findOne: query => Promise.resolve({ id: IdMap.getId("bathrobe") }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const productCollectionService = new ProductCollectionService({
|
||||||
|
manager: MockManager,
|
||||||
|
productCollectionRepository,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully creates a product collection", async () => {
|
||||||
|
await productCollectionService.create({ title: "bathrobe" })
|
||||||
|
|
||||||
|
expect(productCollectionRepository.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productCollectionRepository.create).toHaveBeenCalledWith({
|
||||||
|
title: "bathrobe",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
const productCollectionRepository = MockRepository({
|
||||||
|
findOne: query => {
|
||||||
|
if (query.where.id === "non-existing") {
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}
|
||||||
|
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const productCollectionService = new ProductCollectionService({
|
||||||
|
manager: MockManager,
|
||||||
|
productCollectionRepository,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates a product collection", async () => {
|
||||||
|
await productCollectionService.update(IdMap.getId("bathrobe"), {
|
||||||
|
title: "bathrobes",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(productCollectionRepository.save).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productCollectionRepository.save).toHaveBeenCalledWith({
|
||||||
|
id: IdMap.getId("bathrobe"),
|
||||||
|
title: "bathrobes",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails on non-existing product collection", async () => {
|
||||||
|
try {
|
||||||
|
await productCollectionService.update(IdMap.getId("test"), {
|
||||||
|
title: "bathrobes",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toBe(
|
||||||
|
`Product collection with id: ${IdMap.getId("test")} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("delete", () => {
|
||||||
|
const productCollectionRepository = MockRepository({
|
||||||
|
findOne: query => {
|
||||||
|
if (query.where.id === "non-existing") {
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}
|
||||||
|
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const productCollectionService = new ProductCollectionService({
|
||||||
|
manager: MockManager,
|
||||||
|
productCollectionRepository,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully removes a product collection", async () => {
|
||||||
|
await productCollectionService.delete(IdMap.getId("bathrobe"))
|
||||||
|
|
||||||
|
expect(productCollectionRepository.remove).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productCollectionRepository.remove).toHaveBeenCalledWith({
|
||||||
|
id: IdMap.getId("bathrobe"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("succeeds idempotently", async () => {
|
||||||
|
const result = await productCollectionService.delete(IdMap.getId("test"))
|
||||||
|
expect(result).toBe(undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -36,13 +36,54 @@ describe("ProductService", () => {
|
|||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const productRepository = MockRepository({
|
const productRepository = MockRepository({
|
||||||
create: () =>
|
create: () => ({
|
||||||
Promise.resolve({ id: IdMap.getId("ironman"), title: "Suit" }),
|
id: IdMap.getId("ironman"),
|
||||||
|
title: "Suit",
|
||||||
|
options: [],
|
||||||
|
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||||
|
}),
|
||||||
|
findOne: () => ({
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
title: "Suit",
|
||||||
|
options: [],
|
||||||
|
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const productTagRepository = MockRepository({
|
||||||
|
findOne: () => Promise.resolve(undefined),
|
||||||
|
create: data => {
|
||||||
|
if (data.value === "title") {
|
||||||
|
return { id: "tag-1", value: "title" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.value === "title2") {
|
||||||
|
return { id: "tag-2", value: "title2" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const productTypeRepository = MockRepository({
|
||||||
|
findOne: () => Promise.resolve(undefined),
|
||||||
|
create: data => {
|
||||||
|
return { id: "type", value: "type1" }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const productCollectionService = {
|
||||||
|
withTransaction: function() {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
retrieve: id =>
|
||||||
|
Promise.resolve({ id: IdMap.getId("cat"), title: "Suits" }),
|
||||||
|
}
|
||||||
|
|
||||||
const productService = new ProductService({
|
const productService = new ProductService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
productRepository,
|
productRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
productCollectionService,
|
||||||
|
productTagRepository,
|
||||||
|
productTypeRepository,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -50,9 +91,11 @@ describe("ProductService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("successfully create a product", async () => {
|
it("successfully create a product", async () => {
|
||||||
const result = await productService.create({
|
await productService.create({
|
||||||
title: "Suit",
|
title: "Suit",
|
||||||
options: [],
|
options: [],
|
||||||
|
tags: [{ value: "title" }, { value: "title2" }],
|
||||||
|
type: "type-1",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
@@ -64,15 +107,30 @@ describe("ProductService", () => {
|
|||||||
expect(productRepository.create).toHaveBeenCalledTimes(1)
|
expect(productRepository.create).toHaveBeenCalledTimes(1)
|
||||||
expect(productRepository.create).toHaveBeenCalledWith({
|
expect(productRepository.create).toHaveBeenCalledWith({
|
||||||
title: "Suit",
|
title: "Suit",
|
||||||
options: [],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
expect(productTagRepository.findOne).toHaveBeenCalledTimes(2)
|
||||||
|
// We add two tags, that does not exist therefore we make sure
|
||||||
|
// that create is also called
|
||||||
|
expect(productTagRepository.create).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(productTypeRepository.findOne).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productTypeRepository.create).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productRepository.save).toHaveBeenCalledWith({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
title: "Suit",
|
title: "Suit",
|
||||||
options: [],
|
options: [],
|
||||||
|
tags: [
|
||||||
|
{ id: "tag-1", value: "title" },
|
||||||
|
{ id: "tag-2", value: "title2" },
|
||||||
|
],
|
||||||
|
type_id: "type",
|
||||||
|
collection: {
|
||||||
|
id: IdMap.getId("cat"),
|
||||||
|
title: "Suits",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -93,6 +151,13 @@ describe("ProductService", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const productTypeRepository = MockRepository({
|
||||||
|
findOne: () => Promise.resolve(undefined),
|
||||||
|
create: data => {
|
||||||
|
return { id: "type", value: "type1" }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const productVariantRepository = MockRepository()
|
const productVariantRepository = MockRepository()
|
||||||
|
|
||||||
const productVariantService = {
|
const productVariantService = {
|
||||||
@@ -102,11 +167,24 @@ describe("ProductService", () => {
|
|||||||
update: () => Promise.resolve(),
|
update: () => Promise.resolve(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const productTagRepository = MockRepository({
|
||||||
|
findOne: data => {
|
||||||
|
if (data.where.value === "test") {
|
||||||
|
return Promise.resolve({ id: IdMap.getId("test"), value: "test" })
|
||||||
|
}
|
||||||
|
if (data.where.value === "test2") {
|
||||||
|
return Promise.resolve({ id: IdMap.getId("test2"), value: "test2" })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const productService = new ProductService({
|
const productService = new ProductService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
productRepository,
|
productRepository,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
productVariantRepository,
|
productVariantRepository,
|
||||||
|
productTagRepository,
|
||||||
|
productTypeRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -146,6 +224,10 @@ describe("ProductService", () => {
|
|||||||
it("successfully updates product", async () => {
|
it("successfully updates product", async () => {
|
||||||
await productService.update(IdMap.getId("ironman"), {
|
await productService.update(IdMap.getId("ironman"), {
|
||||||
title: "Full suit",
|
title: "Full suit",
|
||||||
|
collection: {
|
||||||
|
id: IdMap.getId("test"),
|
||||||
|
value: "test",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
@@ -158,6 +240,34 @@ describe("ProductService", () => {
|
|||||||
expect(productRepository.save).toHaveBeenCalledWith({
|
expect(productRepository.save).toHaveBeenCalledWith({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
title: "Full suit",
|
title: "Full suit",
|
||||||
|
collection: {
|
||||||
|
id: IdMap.getId("test"),
|
||||||
|
value: "test",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully updates tags", async () => {
|
||||||
|
await productService.update(IdMap.getId("ironman"), {
|
||||||
|
tags: [
|
||||||
|
{ id: IdMap.getId("test"), value: "test" },
|
||||||
|
{ id: IdMap.getId("test2"), value: "test2" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||||
|
"product.updated",
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productRepository.save).toHaveBeenCalledWith({
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
tags: [
|
||||||
|
{ id: IdMap.getId("test"), value: "test" },
|
||||||
|
{ id: IdMap.getId("test2"), value: "test2" },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
153
packages/medusa/src/services/product-collection.js
Normal file
153
packages/medusa/src/services/product-collection.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import _ from "lodash"
|
||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides layer to manipulate product collections.
|
||||||
|
* @implements BaseService
|
||||||
|
*/
|
||||||
|
class ProductCollectionService extends BaseService {
|
||||||
|
constructor({
|
||||||
|
manager,
|
||||||
|
productCollectionRepository,
|
||||||
|
productRepository,
|
||||||
|
eventBusService,
|
||||||
|
}) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
/** @private @const {EntityManager} */
|
||||||
|
this.manager_ = manager
|
||||||
|
|
||||||
|
/** @private @const {ProductCollectionRepository} */
|
||||||
|
this.productCollectionRepository_ = productCollectionRepository
|
||||||
|
|
||||||
|
/** @private @const {ProductRepository} */
|
||||||
|
this.productRepository_ = productRepository
|
||||||
|
|
||||||
|
/** @private @const {EventBus} */
|
||||||
|
this.eventBus_ = eventBusService
|
||||||
|
}
|
||||||
|
|
||||||
|
withTransaction(transactionManager) {
|
||||||
|
if (!transactionManager) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloned = new ProductCollectionService({
|
||||||
|
manager: transactionManager,
|
||||||
|
productCollectionRepository: this.productCollectionRepository_,
|
||||||
|
productRepository: this.productRepository_,
|
||||||
|
eventBusService: this.eventBus_,
|
||||||
|
})
|
||||||
|
|
||||||
|
cloned.transactionManager_ = transactionManager
|
||||||
|
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a product collection by id.
|
||||||
|
* @param {string} collectionId - the id of the collection to retrieve.
|
||||||
|
* @return {Promise<ProductCollection>} the collection.
|
||||||
|
*/
|
||||||
|
async retrieve(collectionId, config = {}) {
|
||||||
|
const collectionRepo = this.manager_.getCustomRepository(
|
||||||
|
this.productCollectionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const validatedId = this.validateId_(collectionId)
|
||||||
|
|
||||||
|
const query = this.buildQuery_({ id: validatedId }, config)
|
||||||
|
const collection = await collectionRepo.findOne(query)
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Product collection with id: ${collectionId} was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a product collection
|
||||||
|
* @param {object} collection - the collection to create
|
||||||
|
* @return {Promise<ProductCollection>} created collection
|
||||||
|
*/
|
||||||
|
async create(collection) {
|
||||||
|
return this.atomicPhase_(async manager => {
|
||||||
|
const collectionRepo = manager.getCustomRepository(
|
||||||
|
this.productCollectionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const productCollection = collectionRepo.create(collection)
|
||||||
|
return collectionRepo.save(productCollection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a product collection
|
||||||
|
* @param {string} collectionId - id of collection to update
|
||||||
|
* @param {object} update - update object
|
||||||
|
* @return {Promise<ProductCollection>} update collection
|
||||||
|
*/
|
||||||
|
async update(collectionId, update) {
|
||||||
|
return this.atomicPhase_(async manager => {
|
||||||
|
const collectionRepo = manager.getCustomRepository(
|
||||||
|
this.productCollectionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const collection = await this.retrieve(collectionId)
|
||||||
|
|
||||||
|
const { metadata, ...rest } = update
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
collection.metadata = this.setMetadata_(collection, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rest)) {
|
||||||
|
collection[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionRepo.save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a product collection idempotently
|
||||||
|
* @param {string} collectionId - id of collection to delete
|
||||||
|
* @return {Promise} empty promise
|
||||||
|
*/
|
||||||
|
async delete(collectionId) {
|
||||||
|
return this.atomicPhase_(async manager => {
|
||||||
|
const productCollectionRepo = manager.getCustomRepository(
|
||||||
|
this.productCollectionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const collection = await this.retrieve(collectionId)
|
||||||
|
|
||||||
|
if (!collection) return Promise.resolve()
|
||||||
|
|
||||||
|
await productCollectionRepo.remove(collection)
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists product collections
|
||||||
|
* @param {Object} selector - the query object for find
|
||||||
|
* @return {Promise} the result of the find operation
|
||||||
|
*/
|
||||||
|
async list(selector = {}, config = { skip: 0, take: 20 }) {
|
||||||
|
const productCollectionRepo = this.manager_.getCustomRepository(
|
||||||
|
this.productCollectionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const query = this.buildQuery_(selector, config)
|
||||||
|
return productCollectionRepo.find(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductCollectionService
|
||||||
@@ -20,6 +20,9 @@ class ProductService extends BaseService {
|
|||||||
productOptionRepository,
|
productOptionRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
|
productCollectionService,
|
||||||
|
productTypeRepository,
|
||||||
|
productTagRepository,
|
||||||
}) {
|
}) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -40,6 +43,15 @@ class ProductService extends BaseService {
|
|||||||
|
|
||||||
/** @private @const {ProductVariantService} */
|
/** @private @const {ProductVariantService} */
|
||||||
this.productVariantService_ = productVariantService
|
this.productVariantService_ = productVariantService
|
||||||
|
|
||||||
|
/** @private @const {ProductCollectionService} */
|
||||||
|
this.productCollectionService_ = productCollectionService
|
||||||
|
|
||||||
|
/** @private @const {ProductCollectionService} */
|
||||||
|
this.productTypeRepository_ = productTypeRepository
|
||||||
|
|
||||||
|
/** @private @const {ProductCollectionService} */
|
||||||
|
this.productTagRepository_ = productTagRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
withTransaction(transactionManager) {
|
withTransaction(transactionManager) {
|
||||||
@@ -54,6 +66,9 @@ class ProductService extends BaseService {
|
|||||||
productOptionRepository: this.productOptionRepository_,
|
productOptionRepository: this.productOptionRepository_,
|
||||||
eventBusService: this.eventBus_,
|
eventBusService: this.eventBus_,
|
||||||
productVariantService: this.productVariantService_,
|
productVariantService: this.productVariantService_,
|
||||||
|
productCollectionService: this.productCollectionService_,
|
||||||
|
productTagRepository: this.productTagRepository_,
|
||||||
|
productTypeRepository: this.productTypeRepository_,
|
||||||
})
|
})
|
||||||
|
|
||||||
cloned.transactionManager_ = transactionManager
|
cloned.transactionManager_ = transactionManager
|
||||||
@@ -88,6 +103,7 @@ class ProductService extends BaseService {
|
|||||||
alias: "product",
|
alias: "product",
|
||||||
leftJoinAndSelect: {
|
leftJoinAndSelect: {
|
||||||
variant: "product.variants",
|
variant: "product.variants",
|
||||||
|
collection: "product.collection",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +116,7 @@ class ProductService extends BaseService {
|
|||||||
.orWhere(`product.description ILIKE :q`, { q: `%${q}%` })
|
.orWhere(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||||
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
||||||
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
||||||
|
.orWhere(`collection.title ILIKE :q`, { q: `%${q}%` })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -152,6 +169,78 @@ class ProductService extends BaseService {
|
|||||||
return product.variants
|
return product.variants
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listTypes() {
|
||||||
|
const productTypeRepository = this.manager_.getCustomRepository(
|
||||||
|
this.productTypeRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
return await productTypeRepository.find({})
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTagsByUsage(count = 10) {
|
||||||
|
const tags = await this.manager_.query(
|
||||||
|
`
|
||||||
|
SELECT ID, O.USAGE_COUNT, PT.VALUE
|
||||||
|
FROM PRODUCT_TAG PT
|
||||||
|
LEFT JOIN
|
||||||
|
(SELECT COUNT(*) AS USAGE_COUNT,
|
||||||
|
PRODUCT_TAG_ID
|
||||||
|
FROM PRODUCT_TAGS
|
||||||
|
GROUP BY PRODUCT_TAG_ID) O ON O.PRODUCT_TAG_ID = PT.ID
|
||||||
|
ORDER BY O.USAGE_COUNT DESC
|
||||||
|
LIMIT $1`,
|
||||||
|
[count]
|
||||||
|
)
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertProductType_(type) {
|
||||||
|
const productTypeRepository = this.manager_.getCustomRepository(
|
||||||
|
this.productTypeRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
if (type === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await productTypeRepository.findOne({
|
||||||
|
where: { value: type.value },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = productTypeRepository.create(type)
|
||||||
|
const result = await productTypeRepository.save(created)
|
||||||
|
|
||||||
|
return result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsertProductTags_(tags) {
|
||||||
|
const productTagRepository = this.manager_.getCustomRepository(
|
||||||
|
this.productTagRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
let newTags = []
|
||||||
|
for (const tag of tags) {
|
||||||
|
const existing = await productTagRepository.findOne({
|
||||||
|
where: { value: tag.value },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
newTags.push(existing)
|
||||||
|
} else {
|
||||||
|
const created = productTagRepository.create(tag)
|
||||||
|
const result = await productTagRepository.save(created)
|
||||||
|
newTags.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTags
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a product.
|
* Creates a product.
|
||||||
* @param {object} productObject - the product to create
|
* @param {object} productObject - the product to create
|
||||||
@@ -164,17 +253,29 @@ class ProductService extends BaseService {
|
|||||||
this.productOptionRepository_
|
this.productOptionRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const product = await productRepo.create(productObject)
|
const { options, tags, type, ...rest } = productObject
|
||||||
|
|
||||||
|
let product = productRepo.create(rest)
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
product.tags = await this.upsertProductTags_(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof type !== `undefined`) {
|
||||||
|
product.type_id = await this.upsertProductType_(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
product = await productRepo.save(product)
|
||||||
|
|
||||||
product.options = await Promise.all(
|
product.options = await Promise.all(
|
||||||
productObject.options.map(async o => {
|
options.map(async o => {
|
||||||
const res = await optionRepo.create({ ...o, product_id: product.id })
|
const res = optionRepo.create({ ...o, product_id: product.id })
|
||||||
await optionRepo.save(res)
|
await optionRepo.save(res)
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const result = await productRepo.save(product)
|
const result = await this.retrieve(product.id, { relations: ["options"] })
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
@@ -202,10 +303,18 @@ class ProductService extends BaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const product = await this.retrieve(productId, {
|
const product = await this.retrieve(productId, {
|
||||||
relations: ["variants"],
|
relations: ["variants", "tags"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const { variants, metadata, options, images, ...rest } = update
|
const {
|
||||||
|
variants,
|
||||||
|
metadata,
|
||||||
|
options,
|
||||||
|
images,
|
||||||
|
tags,
|
||||||
|
type,
|
||||||
|
...rest
|
||||||
|
} = update
|
||||||
|
|
||||||
if (!product.thumbnail && !update.thumbnail && images && images.length) {
|
if (!product.thumbnail && !update.thumbnail && images && images.length) {
|
||||||
product.thumbnail = images[0]
|
product.thumbnail = images[0]
|
||||||
@@ -215,6 +324,14 @@ class ProductService extends BaseService {
|
|||||||
product.metadata = this.setMetadata_(product, metadata)
|
product.metadata = this.setMetadata_(product, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof type !== `undefined`) {
|
||||||
|
product.type_id = await this.upsertProductType_(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
product.tags = await this.upsertProductTags_(tags)
|
||||||
|
}
|
||||||
|
|
||||||
if (variants) {
|
if (variants) {
|
||||||
// Iterate product variants and update their properties accordingly
|
// Iterate product variants and update their properties accordingly
|
||||||
for (const variant of product.variants) {
|
for (const variant of product.variants) {
|
||||||
@@ -321,7 +438,7 @@ class ProductService extends BaseService {
|
|||||||
product_id: productId,
|
product_id: productId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await productOptionRepo.save(option)
|
await productOptionRepo.save(option)
|
||||||
|
|
||||||
for (const variant of product.variants) {
|
for (const variant of product.variants) {
|
||||||
this.productVariantService_
|
this.productVariantService_
|
||||||
@@ -329,6 +446,8 @@ class ProductService extends BaseService {
|
|||||||
.addOptionValue(variant.id, option.id, "Default Value")
|
.addOptionValue(variant.id, option.id, "Default Value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await this.retrieve(productId)
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.emit(ProductService.Events.UPDATED, result)
|
.emit(ProductService.Events.UPDATED, result)
|
||||||
|
|||||||
@@ -2101,6 +2101,11 @@
|
|||||||
"@types/istanbul-lib-coverage" "*"
|
"@types/istanbul-lib-coverage" "*"
|
||||||
"@types/istanbul-lib-report" "*"
|
"@types/istanbul-lib-report" "*"
|
||||||
|
|
||||||
|
"@types/lodash@^4.14.168":
|
||||||
|
version "4.14.168"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
|
||||||
|
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "13.13.4"
|
version "13.13.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c"
|
||||||
|
|||||||
Reference in New Issue
Block a user