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:
Adrien de Peretti
2023-07-16 20:19:23 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl Riqwan Thamir Carlos R. L. Rodrigues
parent 5b91a3503a
commit befc2f1c80
98 changed files with 5444 additions and 688 deletions
+2
View File
@@ -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() {
+14 -3
View File
@@ -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() {
+10 -1
View File
@@ -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)
+11 -2
View File
@@ -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() {
+12 -10
View File
@@ -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)
+35 -8
View File
@@ -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