feat: Product Module (#4161)

* chore: boilerplate setup

* wip: add Product, ProductTag, ProductType, ProductCollection models

* wip: `IProductService` definition

* wip: test function in index, build passing

* fix: where condition

* chore: get boilerplate working with modules sdk, create a boilerplate test, create product variant model, register services properly

* chore: added variant to model

* chore: changed definition details + add migrator

* cleanup and update product entity

* Update product unique index to include soft deleted

* Migrations tests

* generated migration

* update dev orm config

* add path aliases

* WIP

* chore: added boilerplate integration test + database helper + product variant migraiton + model

* chore: remove old test utils

* update ts and jest config to include path aliases

* tweak config

* WIP migrations variant

* Migrations round

* integration tests migrations polishing

* chore: fixed issues with test db

* fix path aliases when published

* use ts-alias

* fix connection loader

* fixes

* wip: product list

* (WIP): Data access layer

* (WIP): DAL cleanup services

* wip: `ProductTag` DAL

* wip: `ProductTag` expose list in product service

* (WIP): Continue DAL and test list product filtering/populate

* WIP: unit tests

* chore: added tests for service - productvariant

* chore: WIP finding issues with orm manager fork

* WIP fix fields selection

* chore: make text fixes work

* (WIP) product integration

* (WIP) product integration

* chore: create a product in variant test

* list product with relations

* wip: `ProductTag` service + integrations

* chore: added with and without serialization example

* chore: remove only in spec

* wip: `ProductCollection` service + integrations

* uncomment product.variants

* Update type IProductService

* (WIP) type work

* (WIP) Product variants relation

* WIP: replacable data layer

* (WIP): Use bundle types

* WIP: update type

* WIP upadte tests

* WIP

* wip: options/option values entites

* (WIP): cleanup

* wip: add option value to variant, fix collection

* Integration tests for custom data access layer

* update tests

* chore: merge with latest branch

* chore: scope tests to relations and add category to models/index

* chore: ignore dist folders for jest

* chore: modularize spec data file

* improve DX

* module fixture naming

* chore: added category tests + fix model

* chore: use kebab case

* chore: allow scoping products by category id

* chore: replace `kebabCase` import

* feat: add `deleted_at` to options

* improve typings

* chore: wip

* fix query util

* revert webpack

* fix: update option models, create option DTOs, tests wip, fix `deduplicateIfNecessary` returning `undefined`

* fix: merge conflict

* WIP connection

* rm unsues deps

* fix migrations

* fix query util

* chore: adds mpath on creation

* WIP update types

* improve typings

* WIP typeings improvement

* WIP

* deps

* chore: package medusa/product ot medusa-commerce/product

* chore: added product categories service + descendants filter

* add missing index

* Add support for strict categories not in

* Add support for strict categories not in

* lint

* rename module

* rename module

* Create small-ducks-doubt.md

* yarn lock

* update initialise

* chore: fix/finalise DTOs

* fix: wrong types in `IProductService`

* fix type

* Load database config from env if present (#4175)

* Load database config from env if present

* Load database config from env if present

* options optionnal

* update util

* add defaults

* improve filterable interfaces

* fix import

* fix types

* remove medusa-telemetry from modules-sdk

* WIP fixing webpack issues when bootstraping module

* cleanup

* improve loading driver options

* cleanup

* yarn lock

* fix import

* improve sdk types and naming

* align orther modules initialise method

* fix module tests with singleton module

* fix module tests with singleton module

* add up/down migration scripts

* update types

* scripts

* cleanup migrations and scripts

* hash module singleton

* cleanup migration

* cleanup

* fix stringifyCircular usage

* improvements

* fix deps

* fix deps

* improve load config utils

* improve load config utils

* fix deps

* add declaration to the build

* update yarn

* Do not resolve a module path if the exports are explicitly given

* fix module registration resolution path when exports are provided. Explicitly check for false and assign an empty string in this scenario for segregation purpose

* add comment

* fix migration options to prevent set replica errors

* chore: update types to a proper depedency

* chore: update type package

* add seed scripts

* Add descriptive error during database config loading

* use MedusaError

* chore: added lodash to package

* add more test to the database config loader util

* create bin scripts

* add bin

* update argv retrieval

* update package.json

* chore: add product category to injected deps

* chore: replace with product category service

* move dotenv usage to the functions

* do not load db if there is custom manager

* chore: fix some tests on products repo

* chore: fixed product spec

* chore: skip products module on modules register

* stringifyCircular update

* chore: fix incorrect module resolution

* fix: circular stringify and non required module loading

* yarn lock

* target es5

* chore: mikro-orm back to 5.7.4

* revert module registry

* skip external modules

* es2020

* update indexes, migration and integration tests

* rm only

* unit test script should only run unit tests

* Exclude product integration from the unit tests and make use of the global integration script to run all packages integration tests

* fix integration tests

* improve setup

* cleanup

* log error on setup fail

* Create enum like for package names

* chore: remove EOL

* chore: review part 2

* renamve gateway to productModuleService

* chore: added filters and collections to productmoduleservice

* chore: add collection to the singleton instance

* chore: remove skipped test + add todo

* fix indexes on fields and relations + update migration

* update yarn lock

* update idx

* add foreign key

* rename interface and add listCategories

* rename product module definition

---------

Co-authored-by: fPolic <frane@medusajs.com>
Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com>
This commit is contained in:
Adrien de Peretti
2023-06-09 20:47:24 +02:00
committed by GitHub
co-authored by fPolic Riqwan Thamir Carlos R. L. Rodrigues
parent ffeeb84d97
commit 14c0f62f84
104 changed files with 6472 additions and 176 deletions
+2
View File
@@ -2,7 +2,9 @@ export * as CacheTypes from "./cache"
export * as CommonTypes from "./common"
export * as EventBusTypes from "./event-bus"
export * as InventoryTypes from "./inventory"
export * as ProductTypes from "./product"
export * as ModulesSdkTypes from "./modules-sdk"
export * as SearchTypes from "./search"
export * as StockLocationTypes from "./stock-location"
export * as TransactionBaseTypes from "./transaction-base"
export * as DAL from "./dal"
+2
View File
@@ -126,6 +126,8 @@ export type DeleteResponse = {
}
export interface EmptyQueryParams {}
// TODO: Build a tree repository options from this
export interface RepositoryTransformOptions {}
export interface DateComparisonOperator {
lt?: Date
+24
View File
@@ -0,0 +1,24 @@
import { Dictionary, FilterQuery, Order } from "./utils"
export { FilterQuery } from "./utils"
export interface BaseFilterable<T> {
$and?: T
$or?: T
}
export interface OptionsQuery<T, P extends string = never> {
populate?: string[]
orderBy?: Order<T> | Order<T>[]
limit?: number
offset?: number
fields?: string[]
groupBy?: string | string[]
filters?: Dictionary<boolean | Dictionary> | string[] | boolean
}
export type FindOptions<T = any> = {
where: FilterQuery<T>
options?: OptionsQuery<T, any>
}
export * from "./repository-service"
@@ -0,0 +1,12 @@
import { FindOptions } from "./index"
import { RepositoryTransformOptions } from "../common"
/**
* Data access layer (DAL) interface to implements for any repository service.
* This layer helps to separate the business logic (service layer) from accessing the
* ORM directly and allows to switch to another ORM without changing the business logic.
*/
export interface RepositoryService<T = any> {
find(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<T[]>
findAndCount(options?: FindOptions<T>, transformOptions?: RepositoryTransformOptions): Promise<[T[], number]>
}
+127
View File
@@ -0,0 +1,127 @@
type ExpandProperty<T> = T extends (infer U)[] ? NonNullable<U> : NonNullable<T>
export type Dictionary<T = any> = {
[k: string]: T
}
type Query<T> = T extends object
? T extends Scalar
? never
: FilterQuery<T>
: FilterValue<T>
type ExpandScalar<T> =
| null
| (T extends string ? string | RegExp : T extends Date ? Date | string : T)
type Scalar =
| boolean
| number
| string
| bigint
| symbol
| Date
| RegExp
| Buffer
| {
toHexString(): string
}
type ExcludeFunctions<T, K extends keyof T> = T[K] extends Function
? never
: K extends symbol
? never
: K
type ReadonlyPrimary<T> = T extends any[] ? Readonly<T> : T
declare const PrimaryKeyType: unique symbol
type Primary<T> = T extends {
[PrimaryKeyType]?: infer PK
}
? ReadonlyPrimary<PK>
: T extends {
_id?: infer PK
}
? ReadonlyPrimary<PK> | string
: T extends {
uuid?: infer PK
}
? ReadonlyPrimary<PK>
: T extends {
id?: infer PK
}
? ReadonlyPrimary<PK>
: never
export type OperatorMap<T> = {
$and?: Query<T>[]
$or?: Query<T>[]
$eq?: ExpandScalar<T> | ExpandScalar<T>[]
$ne?: ExpandScalar<T>
$in?: ExpandScalar<T>[]
$nin?: ExpandScalar<T>[]
$not?: Query<T>
$gt?: ExpandScalar<T>
$gte?: ExpandScalar<T>
$lt?: ExpandScalar<T>
$lte?: ExpandScalar<T>
$like?: string
$re?: string
$ilike?: string
$fulltext?: string
$overlap?: string[]
$contains?: string[]
$contained?: string[]
$exists?: boolean
}
type FilterValue2<T> = T | ExpandScalar<T> | Primary<T>
type FilterValue<T> =
| OperatorMap<FilterValue2<T>>
| FilterValue2<T>
| FilterValue2<T>[]
| null
type PrevLimit = [never, 1, 2, 3]
export type FilterQuery<T = any, Prev extends number = 3> = Prev extends never
? never
: {
[Key in keyof T]?: T[Key] extends
| boolean
| number
| string
| bigint
| symbol
| Date
? T[Key] | OperatorMap<T[Key]>
: T[Key] extends infer U
? U extends { [x: number]: infer V }
? V extends object
? FilterQuery<Partial<V>, PrevLimit[Prev]>
: never
: never
: never
}
declare enum QueryOrder {
ASC = "ASC",
DESC = "DESC",
asc = "asc",
desc = "desc",
}
type QueryOrderKeysFlat = QueryOrder | 1 | -1 | keyof typeof QueryOrder
type QueryOrderKeys<T> = QueryOrderKeysFlat | QueryOrderMap<T>
type QueryOrderMap<T> = {
[K in keyof T as ExcludeFunctions<T, K>]?: QueryOrderKeys<
ExpandProperty<T[K]>
>
}
export type Order<T> = {
[key in keyof T]?:
| "ASC"
| "DESC"
| Order<T[key] extends Array<any> ? T[key][0] : T[key]>
}
+3
View File
@@ -4,7 +4,10 @@ export * from "./common"
export * from "./event-bus"
export * from "./inventory"
export * from "./modules-sdk"
export * from "./product"
export * from "./product-category"
export * from "./search"
export * from "./shared-context"
export * from "./stock-location"
export * from "./transaction-base"
export * from "./dal"
+3 -1
View File
@@ -1,5 +1,6 @@
import { Logger as _Logger } from "winston"
import { MedusaContainer } from "../common/medusa-container"
export type Constructor<T> = new (...args: any[]) => T
export * from "../common/medusa-container"
@@ -39,7 +40,7 @@ export type InternalModuleDeclaration = {
export type ExternalModuleDeclaration = {
scope: MODULE_SCOPE.EXTERNAL
server: {
server?: {
type: "http"
url: string
keepAlive: boolean
@@ -52,6 +53,7 @@ export type ModuleResolution = {
options?: Record<string, unknown>
dependencies?: string[]
moduleDeclaration?: InternalModuleDeclaration | ExternalModuleDeclaration
moduleExports?: ModuleExports
}
export type ModuleDefinition = {
@@ -0,0 +1 @@
export * from "./repository"
@@ -0,0 +1,5 @@
import { RepositoryTransformOptions } from '../common'
export interface ProductCategoryTransformOptions extends RepositoryTransformOptions {
includeDescendantsTree?: boolean
}
+166
View File
@@ -0,0 +1,166 @@
import { BaseFilterable } from "../dal"
import { OperatorMap } from "../dal/utils"
export enum ProductStatus {
DRAFT = "draft",
PROPOSED = "proposed",
PUBLISHED = "published",
REJECTED = "rejected",
}
/**
* DTO in and out of the module (module API)
*/
export interface ProductDTO {
id: string
title: string
handle?: string | null
subtitle?: string | null
description?: string | null
is_giftcard: boolean
status: ProductStatus
thumbnail?: string | null
weight?: number | null
length?: number | null
height?: number | null
origin_country?: string | null
hs_code?: string | null
mid_code?: string | null
material?: string | null
collection: ProductCollectionDTO
categories?: ProductCategoryDTO[] | null
type: ProductTypeDTO[]
tags: ProductTagDTO[]
variants: ProductVariantDTO[]
options: ProductOptionDTO[]
discountable?: boolean
external_id?: string | null
created_at?: string | Date
updated_at?: string | Date
deleted_at?: string | Date
}
export interface ProductVariantDTO {
id: string
title: string
sku?: string | null
barcode?: string | null
ean?: string | null
upc?: string | null
inventory_quantity: number
allow_backorder?: boolean
manage_inventory?: boolean
hs_code?: string | null
origin_country?: string | null
mid_code?: string | null
material?: string | null
weight?: number | null
length?: number | null
height?: number | null
width?: number | null
options: ProductOptionValueDTO
metadata?: Record<string, unknown> | null
product: ProductDTO
variant_rank?: number | null
created_at: string | Date
updated_at: string | Date
deleted_at: string | Date
}
export interface ProductCategoryDTO {
id: string
name: string
description?: string
handle?: string
is_active?: boolean
is_internal?: boolean
rank?: number
parent_category?: ProductCategoryDTO
category_children: ProductCategoryDTO[]
created_at: string | Date
updated_at: string | Date
}
export interface ProductTagDTO {
id: string
value: string
metadata?: Record<string, unknown> | null
products: ProductDTO[]
}
export interface ProductCollectionDTO {
id: string
title: string
handle: string
metadata?: Record<string, unknown> | null
deleted_at?: string | Date
}
export interface ProductTypeDTO {
id: string
value: string
metadata?: Record<string, unknown> | null
deleted_at?: string | Date
}
export interface ProductOptionDTO {
id: string
title: string
product: ProductDTO
values: ProductOptionValueDTO[]
metadata?: Record<string, unknown> | null
deleted_at?: string | Date
}
export interface ProductOptionValueDTO {
id: string
value: string
option: ProductOptionDTO
variant: ProductVariantDTO
metadata?: Record<string, unknown> | null
deleted_at?: string | Date
}
/**
* Filters/Config (module API input filters and config)
*/
export interface FilterableProductProps
extends BaseFilterable<FilterableProductProps> {
handle?: string | string[]
id?: string | string[]
tags?: { value?: string[] }
categories?: {
id?: string | string[] | OperatorMap<string>
}
category_ids?: string | string[] | OperatorMap<string>
}
export interface FilterableProductTagProps
extends BaseFilterable<FilterableProductTagProps> {
id?: string | string[]
value?: string
}
export interface FilterableProductCollectionProps
extends BaseFilterable<FilterableProductCollectionProps> {
id?: string | string[]
title?: string
}
export interface FilterableProductVariantProps
extends BaseFilterable<FilterableProductVariantProps> {
id?: string | string[]
sku?: string | string[]
options?: { id?: string[] }
}
export interface FilterableProductCategoryProps
extends BaseFilterable<FilterableProductCategoryProps> {
id?: string | string[]
parent_category_id?: string | string[] | null
handle?: string | string[]
is_active?: boolean
is_internal?: boolean
include_descendants_tree?: boolean
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./service"
export * from "./common"
+58
View File
@@ -0,0 +1,58 @@
import {
FilterableProductCategoryProps,
FilterableProductCollectionProps,
FilterableProductProps,
FilterableProductTagProps,
FilterableProductVariantProps,
ProductCategoryDTO,
ProductCollectionDTO,
ProductDTO,
ProductTagDTO,
ProductVariantDTO,
} from "./common"
import { FindConfig } from "../common"
import { SharedContext } from "../shared-context"
export interface IProductModuleService<
TProduct = any,
TProductVariant = any,
TProductTag = any,
TProductCollection = any,
TProductCategory = any
> {
list(
filter: FilterableProductProps,
config?: FindConfig<ProductDTO>,
context?: SharedContext
): Promise<ProductDTO[]>
listAndCount(
filter: FilterableProductProps,
config?: FindConfig<ProductDTO>,
context?: SharedContext
): Promise<[ProductDTO[], number]>
listTags(
filter: FilterableProductTagProps,
config?: FindConfig<ProductTagDTO>,
context?: SharedContext
): Promise<ProductTagDTO[]>
listVariants(
filter: FilterableProductVariantProps,
config?: FindConfig<ProductVariantDTO>,
context?: SharedContext
): Promise<ProductVariantDTO[]>
listCollections(
filter: FilterableProductCollectionProps,
config?: FindConfig<ProductCollectionDTO>,
context?: SharedContext
): Promise<ProductCollectionDTO[]>
listCategories(
filters: FilterableProductCategoryProps,
config?: FindConfig<ProductCategoryDTO>,
sharedContext?: SharedContext
): Promise<ProductCategoryDTO[]>
}