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
|
||||
*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-preset-medusa-package": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@ module.exports = {
|
||||
`.cache`,
|
||||
],
|
||||
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
|
||||
/models
|
||||
/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) {
|
||||
try {
|
||||
const p = await this.productService_.retrieve(product.id, {
|
||||
relations: ["variants", "options"],
|
||||
relations: ["variants", "options", "tags", "type", "collection"],
|
||||
})
|
||||
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const variantEntries = await this.getVariantEntries_(p.variants)
|
||||
const variantLinks = this.getVariantLinks_(variantEntries)
|
||||
|
||||
const result = await environment.createEntryWithId("product", p.id, {
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": p.title,
|
||||
},
|
||||
variants: {
|
||||
"en-US": variantLinks,
|
||||
},
|
||||
options: {
|
||||
"en-US": p.options,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": p.id,
|
||||
},
|
||||
const fields = {
|
||||
title: {
|
||||
"en-US": p.title,
|
||||
},
|
||||
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")) || []
|
||||
@@ -210,7 +240,7 @@ class ContentfulService extends BaseService {
|
||||
}
|
||||
|
||||
const p = await this.productService_.retrieve(product.id, {
|
||||
relations: ["options", "variants"],
|
||||
relations: ["options", "variants", "type", "collection", "tags"],
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
const updatedEntry = await productEntry.update()
|
||||
@@ -372,6 +430,11 @@ class ContentfulService extends BaseService {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getType(type) {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
return environment.getContentType(type)
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentfulService
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"awilix": "^4.2.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"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 returnRoutes from "./returns"
|
||||
import variantRoutes from "./variants"
|
||||
import collectionRoutes from "./collections"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -60,6 +61,7 @@ export default (app, container, config) => {
|
||||
swapRoutes(route)
|
||||
returnRoutes(route)
|
||||
variantRoutes(route)
|
||||
collectionRoutes(route)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("POST /admin/products", () => {
|
||||
payload: {
|
||||
title: "Test Product",
|
||||
description: "Test Description",
|
||||
tags: "hi,med,dig",
|
||||
tags: [{ id: "test", value: "test" }],
|
||||
handle: "test-product",
|
||||
},
|
||||
adminSession: {
|
||||
@@ -36,7 +36,7 @@ describe("POST /admin/products", () => {
|
||||
expect(ProductServiceMock.create).toHaveBeenCalledWith({
|
||||
title: "Test Product",
|
||||
description: "Test Description",
|
||||
tags: "hi,med,dig",
|
||||
tags: [{ id: "test", value: "test" }],
|
||||
handle: "test-product",
|
||||
is_giftcard: false,
|
||||
profile_id: IdMap.getId("default_shipping_profile"),
|
||||
|
||||
@@ -34,11 +34,12 @@ describe("GET /admin/products/:id", () => {
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -57,6 +58,9 @@ describe("GET /admin/products/:id", () => {
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,13 +6,28 @@ export default async (req, res) => {
|
||||
title: Validator.string().required(),
|
||||
subtitle: Validator.string().allow(""),
|
||||
description: Validator.string().allow(""),
|
||||
tags: Validator.string().optional(),
|
||||
is_giftcard: Validator.boolean().default(false),
|
||||
images: Validator.array()
|
||||
.items(Validator.string())
|
||||
.optional(),
|
||||
thumbnail: 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({
|
||||
title: Validator.string().required(),
|
||||
}),
|
||||
@@ -48,7 +63,7 @@ export default async (req, res) => {
|
||||
Validator.object()
|
||||
.keys({
|
||||
region_id: Validator.string(),
|
||||
currency_code: Validator.string().required(),
|
||||
currency_code: Validator.string(),
|
||||
amount: Validator.number()
|
||||
.integer()
|
||||
.required(),
|
||||
|
||||
@@ -8,6 +8,11 @@ export default app => {
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-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(
|
||||
"/:id/variants",
|
||||
@@ -52,6 +57,9 @@ export const defaultRelations = [
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
]
|
||||
|
||||
export const defaultFields = [
|
||||
@@ -59,11 +67,12 @@ export const defaultFields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -82,11 +91,12 @@ export const allowedFields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -105,4 +115,7 @@ export const allowedRelations = [
|
||||
"variants.prices",
|
||||
"images",
|
||||
"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({
|
||||
title: 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(),
|
||||
weight: Validator.number().optional(),
|
||||
length: Validator.number().optional(),
|
||||
|
||||
@@ -21,10 +21,13 @@ export { Order } from "./models/order"
|
||||
export { PaymentProvider } from "./models/payment-provider"
|
||||
export { PaymentSession } from "./models/payment-session"
|
||||
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 { 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 { Region } from "./models/region"
|
||||
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
|
||||
|
||||
@Column()
|
||||
product_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Product,
|
||||
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 { Image } from "./image"
|
||||
import { ProductCollection } from "./product-collection"
|
||||
import { ProductOption } from "./product-option"
|
||||
import { ProductTag } from "./product-tag"
|
||||
import { ProductType } from "./product-type"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { ShippingProfile } from "./shipping-profile"
|
||||
import _ from "lodash"
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
@@ -35,9 +39,6 @@ export class Product {
|
||||
@Column({ nullable: true })
|
||||
description: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
tags: string
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ nullable: true })
|
||||
handle: string
|
||||
@@ -107,6 +108,34 @@ export class Product {
|
||||
@Column({ nullable: true })
|
||||
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" })
|
||||
created_at: Date
|
||||
|
||||
@@ -124,5 +153,9 @@ export class Product {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
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", () => {
|
||||
const productRepository = MockRepository({
|
||||
create: () =>
|
||||
Promise.resolve({ id: IdMap.getId("ironman"), title: "Suit" }),
|
||||
create: () => ({
|
||||
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({
|
||||
manager: MockManager,
|
||||
productRepository,
|
||||
eventBusService,
|
||||
productCollectionService,
|
||||
productTagRepository,
|
||||
productTypeRepository,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -50,9 +91,11 @@ describe("ProductService", () => {
|
||||
})
|
||||
|
||||
it("successfully create a product", async () => {
|
||||
const result = await productService.create({
|
||||
await productService.create({
|
||||
title: "Suit",
|
||||
options: [],
|
||||
tags: [{ value: "title" }, { value: "title2" }],
|
||||
type: "type-1",
|
||||
})
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
@@ -64,15 +107,30 @@ describe("ProductService", () => {
|
||||
expect(productRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(productRepository.create).toHaveBeenCalledWith({
|
||||
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"),
|
||||
title: "Suit",
|
||||
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 productVariantService = {
|
||||
@@ -102,11 +167,24 @@ describe("ProductService", () => {
|
||||
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({
|
||||
manager: MockManager,
|
||||
productRepository,
|
||||
productVariantService,
|
||||
productVariantRepository,
|
||||
productTagRepository,
|
||||
productTypeRepository,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
@@ -146,6 +224,10 @@ describe("ProductService", () => {
|
||||
it("successfully updates product", async () => {
|
||||
await productService.update(IdMap.getId("ironman"), {
|
||||
title: "Full suit",
|
||||
collection: {
|
||||
id: IdMap.getId("test"),
|
||||
value: "test",
|
||||
},
|
||||
})
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
@@ -158,6 +240,34 @@ describe("ProductService", () => {
|
||||
expect(productRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("ironman"),
|
||||
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,
|
||||
eventBusService,
|
||||
productVariantService,
|
||||
productCollectionService,
|
||||
productTypeRepository,
|
||||
productTagRepository,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -40,6 +43,15 @@ class ProductService extends BaseService {
|
||||
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productCollectionService_ = productCollectionService
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productTypeRepository_ = productTypeRepository
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productTagRepository_ = productTagRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -54,6 +66,9 @@ class ProductService extends BaseService {
|
||||
productOptionRepository: this.productOptionRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productCollectionService: this.productCollectionService_,
|
||||
productTagRepository: this.productTagRepository_,
|
||||
productTypeRepository: this.productTypeRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -88,6 +103,7 @@ class ProductService extends BaseService {
|
||||
alias: "product",
|
||||
leftJoinAndSelect: {
|
||||
variant: "product.variants",
|
||||
collection: "product.collection",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -100,6 +116,7 @@ class ProductService extends BaseService {
|
||||
.orWhere(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.title 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
|
||||
}
|
||||
|
||||
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.
|
||||
* @param {object} productObject - the product to create
|
||||
@@ -164,17 +253,29 @@ class ProductService extends BaseService {
|
||||
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(
|
||||
productObject.options.map(async o => {
|
||||
const res = await optionRepo.create({ ...o, product_id: product.id })
|
||||
options.map(async o => {
|
||||
const res = optionRepo.create({ ...o, product_id: product.id })
|
||||
await optionRepo.save(res)
|
||||
return res
|
||||
})
|
||||
)
|
||||
|
||||
const result = await productRepo.save(product)
|
||||
const result = await this.retrieve(product.id, { relations: ["options"] })
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
@@ -202,10 +303,18 @@ class ProductService extends BaseService {
|
||||
)
|
||||
|
||||
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) {
|
||||
product.thumbnail = images[0]
|
||||
@@ -215,6 +324,14 @@ class ProductService extends BaseService {
|
||||
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) {
|
||||
// Iterate product variants and update their properties accordingly
|
||||
for (const variant of product.variants) {
|
||||
@@ -321,7 +438,7 @@ class ProductService extends BaseService {
|
||||
product_id: productId,
|
||||
})
|
||||
|
||||
const result = await productOptionRepo.save(option)
|
||||
await productOptionRepo.save(option)
|
||||
|
||||
for (const variant of product.variants) {
|
||||
this.productVariantService_
|
||||
@@ -329,6 +446,8 @@ class ProductService extends BaseService {
|
||||
.addOptionValue(variant.id, option.id, "Default Value")
|
||||
}
|
||||
|
||||
const result = await this.retrieve(productId)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductService.Events.UPDATED, result)
|
||||
|
||||
@@ -2101,6 +2101,11 @@
|
||||
"@types/istanbul-lib-coverage" "*"
|
||||
"@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@*":
|
||||
version "13.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c"
|
||||
|
||||
Reference in New Issue
Block a user