feat(product): Create (+ workflow), delete, restore (#4459)
* Feat: create product with product module * feat: create product wip * feat: create product wip * feat: update product relation and generate image migration * lint * conitnue implementation * continue implementation and add integration tests for produceService.create * Add integration tests for product creation at the module level for the complete flow * only use persist since write operations are always wrapped in a transaction which will be committed and flushed * simplify the transaction wrapper to make future changes easier * feat: move some utils to the utils package to simplify its usage * tests: fix unit tests * feat: create variants along side the product * Add more integration tests an update migrations * chore: Update actions workflow to include packages integration tests * small types and utils cleanup * chore: Add support for database debug option * chore: Add missing types in package.json from types and util, validate that all the models are sync with medusa * expose retrieve method * fix types issues * fix unit tests and move integration tests workflow with the plugins integration tests * chore: remove migration function export from the definition to prevent them to be ran by the medusa cli just in case * fix package.json script * chore: workflows * feat: start creating the create product workflow * feat: add empty step for prices and sales channel * tests: update scripts and action envs * fix imports * feat: Add proper soft deleted support + add product deletion service public api * chore: update migrations * chore: update migrations * chore: update todo * feat: Add product deletion to the create-product workflow as compensation * chore: cleanup product utils * feat: Add support for cascade soft-remove * feat: refactor repository to take into account withDeleted * fix integration tests * Add support for force delete -> delete, cleanup repositories and improvements * Add support for restoring a product and add integration tests * cleaup + tests * types * fix integration tests * remove unnecessary comments * move specific mikro orm usage to the DAL * Cleanup workflow functions * Make deleted_at optional at the property level and add url index for the images * address feedback + cleanup * fix export * merge migrations into one * feat(product, types): added missing product variant methods (#4475) * chore: added missing product variant methods * chore: address PR feedback * chore: catch undefined case for retrieve + specs for variant service * chore: align TEntity + add changeset * chore: revert changeset, TEntity to ProductVariant * chore: write tests for pagination, unskip the test * Create chilled-mice-deliver.md * update integration fixtuers * update pipeline node version * rename github action * fix pipeline * feat(medusa, types): added missing category tests and service methods (#4499) * chore: added missing category tests and service methods * chore: added type changes to module service * chore: address pr feedback * update repositories manager usage and serialisation from the write public API * move serializisation to the DAL * rename template args * chore: added collection methods for module and collection service (#4505) * chore: added collection methods for module and collection service * Create fresh-islands-teach.md * chore: move retrieve entity to utils package * chore: make products optional in DTO type --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * feat(product): Apply transaction decorators to the services (#4512) --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
Riqwan Thamir
Carlos R. L. Rodrigues
parent
5b91a3503a
commit
befc2f1c80
@@ -4,3 +4,5 @@ export { default as ProductCollection } from "./product-collection"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as Image } from "./product-image"
|
||||
|
||||
@@ -21,7 +21,7 @@ class ProductCategory {
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name: string
|
||||
name?: string
|
||||
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_collection" })
|
||||
@SoftDeletable()
|
||||
class ProductCollection {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,13 +32,17 @@ class ProductCollection {
|
||||
name: "IDX_product_collection_handle_unique",
|
||||
properties: ["handle"],
|
||||
})
|
||||
handle: string
|
||||
handle?: string
|
||||
|
||||
@OneToMany(() => Product, (product) => product.collection)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_collection_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "image" })
|
||||
@SoftDeletable()
|
||||
class ProductImage {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Index({ name: "IDX_product_image_url" })
|
||||
@Property({ columnType: "text" })
|
||||
url: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_image_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductImage
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
@@ -9,28 +11,53 @@ import { generateEntityId } from "@medusajs/utils"
|
||||
|
||||
import ProductOption from "./product-option"
|
||||
import { ProductVariant } from "./index"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "option_id"
|
||||
| "variant_id"
|
||||
type OptionalRelations = "product" | "option" | "variant"
|
||||
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
@SoftDeletable()
|
||||
class ProductOptionValue {
|
||||
[OptionalProps]?: OptionalFields | OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ persist: false })
|
||||
option_id!: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
index: "IDX_product_option_value_product_option",
|
||||
index: "IDX_product_option_value_option_id",
|
||||
fieldName: "option_id",
|
||||
})
|
||||
option: ProductOption
|
||||
|
||||
@ManyToOne(() => ProductVariant, { onDelete: "cascade" })
|
||||
@Property({ persist: false })
|
||||
variant_id!: string
|
||||
|
||||
@ManyToOne(() => ProductVariant, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_option_value_variant_id",
|
||||
fieldName: "variant_id",
|
||||
})
|
||||
variant: ProductVariant
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_value_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -3,17 +3,26 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "values" | "product"
|
||||
type OptionalFields = "product_id"
|
||||
|
||||
@Entity({ tableName: "product_option" })
|
||||
@SoftDeletable()
|
||||
class ProductOption {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,23 +30,25 @@ class ProductOption {
|
||||
title: string
|
||||
|
||||
@Property({ persist: false })
|
||||
product_id!: number
|
||||
product_id!: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
index: "IDX_product_option_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
cascade: [Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -2,16 +2,24 @@ import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_tag" })
|
||||
@SoftDeletable()
|
||||
class ProductTag {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,8 +29,9 @@ class ProductTag {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_tag_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { BeforeCreate, Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
@Entity({ tableName: "product_type" })
|
||||
@SoftDeletable()
|
||||
class ProductType {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
@@ -13,8 +21,9 @@ class ProductType {
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_type_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
@@ -13,18 +14,18 @@ import {
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "product"
|
||||
| "product_id"
|
||||
|
||||
@Entity({ tableName: "product_variant" })
|
||||
@SoftDeletable()
|
||||
class ProductVariant {
|
||||
[OptionalProps]?: OptionalFields
|
||||
|
||||
@@ -65,14 +66,14 @@ class ProductVariant {
|
||||
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose
|
||||
// precision if you cast numeric to JS number, as JS number is a float.
|
||||
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
|
||||
@Property({ columnType: "numeric" })
|
||||
inventory_quantity: number
|
||||
@Property({ columnType: "numeric", default: 100 })
|
||||
inventory_quantity?: number = 100
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder: boolean
|
||||
allow_backorder?: boolean = false
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory: boolean
|
||||
manage_inventory?: boolean = true
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
@@ -101,7 +102,7 @@ class ProductVariant {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
@Property({ columnType: "numeric", nullable: true, default: 0 })
|
||||
variant_rank?: number | null
|
||||
|
||||
@Property({ persist: false })
|
||||
@@ -117,18 +118,19 @@ class ProductVariant {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_variant_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_variant_product_id_index",
|
||||
index: "IDX_product_variant_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product!: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, {
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE],
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@@ -20,16 +21,20 @@ import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
import ProductImage from "./product-image"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "collection" | "type"
|
||||
type OptionalFields =
|
||||
| "collection_id"
|
||||
| "type_id"
|
||||
| "is_giftcard"
|
||||
| "discountable"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
|
||||
@Entity({ tableName: "product" })
|
||||
@SoftDeletable()
|
||||
class Product {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@@ -58,16 +63,17 @@ class Product {
|
||||
@Enum(() => ProductTypes.ProductStatus)
|
||||
status!: ProductTypes.ProductStatus
|
||||
|
||||
// TODO: add images model
|
||||
// images: Image[]
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product)
|
||||
@OneToMany(() => ProductOption, (o) => o.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product)
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@@ -94,12 +100,22 @@ class Product {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, { nullable: true })
|
||||
@Property({ persist: false })
|
||||
collection_id!: string
|
||||
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
nullable: true,
|
||||
fieldName: "collection_id",
|
||||
})
|
||||
collection!: ProductCollection
|
||||
|
||||
@Property({ persist: false })
|
||||
type_id!: string
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
index: "IDX_product_type_id",
|
||||
fieldName: "type_id",
|
||||
})
|
||||
type!: ProductType
|
||||
|
||||
@@ -107,12 +123,22 @@ class Product {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
index: "IDX_product_image_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
images = new Collection<ProductImage>(this)
|
||||
|
||||
@ManyToMany(() => ProductCategory, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_category_product",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
categories = new Collection<ProductCategory>(this)
|
||||
|
||||
@@ -132,8 +158,9 @@ class Product {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
Reference in New Issue
Block a user