feat: convert MikroORM entities to DML entities (#10043)

* feat: convert MikroORM entities to DML entities

* feat: wip on repository changes

* continue repositories and types rework

* fix order repository usage

* continue to update product category repository

* Add foreign key as part of the inferred DML type

* ../../core/types/src/dml/index.ts

* ../../core/types/src/dml/index.ts

* fix: relationships mapping

* handle nullable foreign keys types

* handle nullable foreign keys types

* handle nullable foreign keys types

* continue to update product category repository

* fix all product category repositories issues

* fix product category service types

* fix product module service types

* fix product module service types

* fix repository template type

* refactor: use a singleton DMLToMikroORM factory instance

Since the MikroORM MetadataStorage is global, we will also have to turn DML
to MikroORM entities conversion use a global bucket as well

* refactor: update product module to use DML in tests

* wip: tests

* WIP product linkable fixes

* continue type fixing and start test fixing

* test: fix more tests

* fix repository

* fix pivot table computaion + fix mikro orm repository

* fix many to many management and configuration

* fix many to many management and configuration

* fix many to many management and configuration

* update product tag relation configuration

* Introduce experimental dml hooks to fix some issues with categories

* more fixes

* fix product tests

* add missing id prefixes

* fix product category handle management

* test: fix more failing tests

* test: make it all green

* test: fix breaking tests

* fix: build issues

* fix: build issues

* fix: more breaking tests

* refactor: fix issues after merge

* refactor: fix issues after merge

* refactor: surpress types error

* test: fix DML failing tests

* improve many to many inference + tests

* Wip fix columns from product entity

* remove product model before create hook and manage handle validation and transformation at the service level

* test: fix breaking unit tests

* fix: product module service to not update handle on product update

* fix define link and joiner config

* test: fix joiner config test

* test: fix joiner config test

* fix joiner config primary keys

* Fix joiner config builder

* Fix joiner config builder

* test: remove only modifier from test

* refactor: remove hooks usage from product collection

* refactor: remove hooks usage from product-option

* refactor: remove hooks usage for computing category handle

* refactor: remove hooks usage from productCategory model

* refactor: remove hooks from DML

* refactor: remove cruft

* cleanup

* re add foerign key indexes

* chore: remove unused types

* refactor: cleanup

* migration and models configuration adjustments

* cleanup

* fix random ordering

* fix

* test: fix product-category tests

* test: update breaking DML tests

* test: array assertion to not care about ordering

* fix: temporarily apply id ordering for products

* fix ordering

* fix ordering remove logs

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2024-11-26 20:01:02 +05:30
1 parent d6fa912b22
commit 9f204817b0
61 files changed
+2254 -1878

No files matched your search

@@ -74,7 +74,8 @@ medusaIntegrationTestRunner({
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string} // BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id, type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }], tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
images: [{ images: [
{
url: "image-one", url: "image-one",
}, },
{ {
@@ -140,7 +141,6 @@ medusaIntegrationTestRunner({
) )
}) })
it("returns a list of products with all statuses when no status or invalid status is provided", async () => { it("returns a list of products with all statuses when no status or invalid status is provided", async () => {
const res = await api const res = await api
.get("/admin/products", adminHeaders) .get("/admin/products", adminHeaders)
@@ -991,7 +991,10 @@ medusaIntegrationTestRunner({
}) })
it("should get a product with images ordered by rank", async () => { it("should get a product with images ordered by rank", async () => {
const res = await api.get(`/admin/products/${baseProduct.id}`, adminHeaders) const res = await api.get(
`/admin/products/${baseProduct.id}`,
adminHeaders
)
expect(res.data.product.images).toEqual( expect(res.data.product.images).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@@ -1436,7 +1436,7 @@ medusaIntegrationTestRunner({
}, },
{ {
url: "image-two", url: "image-two",
} },
], ],
}) })
@@ -1487,7 +1487,10 @@ medusaIntegrationTestRunner({
}) })
it("should retrieve product with images ordered by rank", async () => { it("should retrieve product with images ordered by rank", async () => {
const response = await api.get(`/store/products/${product.id}`, storeHeaders) const response = await api.get(
`/store/products/${product.id}`,
storeHeaders
)
expect(response.data.product.images).toEqual( expect(response.data.product.images).toEqual(
expect.arrayContaining([ expect.arrayContaining([
+6 -4
View File
@@ -1,3 +1,4 @@
import { InferEntityType } from "../dml"
import { Dictionary, FilterQuery, Order } from "./utils" import { Dictionary, FilterQuery, Order } from "./utils"
export { FilterQuery, OperatorMap } from "./utils" export { FilterQuery, OperatorMap } from "./utils"
@@ -22,7 +23,7 @@ export interface BaseFilterable<T> {
/** /**
* The options to apply when retrieving an item. * The options to apply when retrieving an item.
*/ */
export interface OptionsQuery<T, P extends string = never> { export interface OptionsQuery<T> {
/** /**
* Relations to populate in the retrieved items. * Relations to populate in the retrieved items.
*/ */
@@ -54,7 +55,7 @@ export interface OptionsQuery<T, P extends string = never> {
/** /**
* Load strategy (e.g for mikro orm it accept select-in or joined) * Load strategy (e.g for mikro orm it accept select-in or joined)
*/ */
strategy?: 'select-in' | 'joined' | string & {} strategy?: "select-in" | "joined" | (string & {})
} }
/** /**
@@ -66,12 +67,13 @@ export type FindOptions<T = any> = {
/** /**
* The filters to apply on the items. * The filters to apply on the items.
*/ */
where: FilterQuery<T> & BaseFilterable<FilterQuery<T>> where: FilterQuery<InferEntityType<T>> &
BaseFilterable<FilterQuery<InferEntityType<T>>>
/** /**
* The options to apply when retrieving the items. * The options to apply when retrieving the items.
*/ */
options?: OptionsQuery<T, any> options?: OptionsQuery<InferEntityType<T>>
} }
/** /**
@@ -1,16 +1,23 @@
import { RepositoryTransformOptions } from "../common" import { RepositoryTransformOptions } from "../common"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { import {
BaseFilterable,
FilterQuery,
FilterQuery as InternalFilterQuery, FilterQuery as InternalFilterQuery,
FindOptions, FindOptions,
UpsertWithReplaceConfig, UpsertWithReplaceConfig,
} from "./index" } from "./index"
import { EntityClass } from "@mikro-orm/core"
import { IDmlEntity, InferTypeOf } from "../dml"
type EntityClassName = string type EntityClassName = string
type EntityValues = { id: string }[] type EntityValues = { id: string }[]
/**
* Either infer the properties from a DML object or from a Mikro orm class prototype.
*/
export type InferRepositoryReturnType<T> = T extends IDmlEntity<any, any>
? InferTypeOf<T>
: EntityClass<T>["prototype"]
export type PerformedActions = { export type PerformedActions = {
created: Record<EntityClassName, EntityValues> created: Record<EntityClassName, EntityValues>
updated: Record<EntityClassName, EntityValues> updated: Record<EntityClassName, EntityValues>
@@ -22,7 +29,7 @@ export type PerformedActions = {
* This layer helps to separate the business logic (service layer) from accessing the * 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. * ORM directly and allows to switch to another ORM without changing the business logic.
*/ */
interface BaseRepositoryService<T = any> { interface BaseRepositoryService {
transaction<TManager = unknown>( transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>, task: (transactionManager: TManager) => Promise<any>,
context?: { context?: {
@@ -42,22 +49,28 @@ interface BaseRepositoryService<T = any> {
): Promise<TOutput> ): Promise<TOutput>
} }
export interface RepositoryService<T = any> extends BaseRepositoryService<T> { export interface RepositoryService<T = any> extends BaseRepositoryService {
find(options?: FindOptions<T>, context?: Context): Promise<T[]> find(
options?: FindOptions<T>,
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
findAndCount( findAndCount(
options?: FindOptions<T>, options?: FindOptions<T>,
context?: Context context?: Context
): Promise<[T[], number]> ): Promise<[InferRepositoryReturnType<T>[], number]>
create(data: any[], context?: Context): Promise<T[]> create(
data: any[],
update(data: { entity; update }[], context?: Context): Promise<T[]>
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
context?: Context context?: Context
): Promise<void> ): Promise<InferRepositoryReturnType<T>[]>
update(
data: { entity; update }[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
delete(idsOrPKs: FindOptions<T>["where"], context?: Context): Promise<void>
/** /**
* Soft delete entities and cascade to related entities if configured. * Soft delete entities and cascade to related entities if configured.
@@ -74,37 +87,45 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
| InternalFilterQuery | InternalFilterQuery
| InternalFilterQuery[], | InternalFilterQuery[],
context?: Context context?: Context
): Promise<[T[], Record<string, unknown[]>]> ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]>
restore( restore(
idsOrFilter: string[] | InternalFilterQuery, idsOrFilter: string[] | InternalFilterQuery,
context?: Context context?: Context
): Promise<[T[], Record<string, unknown[]>]> ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]>
upsert(data: any[], context?: Context): Promise<T[]> upsert(
data: any[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
upsertWithReplace( upsertWithReplace(
data: any[], data: any[],
config?: UpsertWithReplaceConfig<T>, config?: UpsertWithReplaceConfig<InferRepositoryReturnType<T>>,
context?: Context context?: Context
): Promise<{ entities: T[]; performedActions: PerformedActions }> ): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}>
} }
export interface TreeRepositoryService<T = any> export interface TreeRepositoryService<T = any> extends BaseRepositoryService {
extends BaseRepositoryService<T> {
find( find(
options?: FindOptions<T>, options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions, transformOptions?: RepositoryTransformOptions,
context?: Context context?: Context
): Promise<T[]> ): Promise<InferRepositoryReturnType<T>[]>
findAndCount( findAndCount(
options?: FindOptions<T>, options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions, transformOptions?: RepositoryTransformOptions,
context?: Context context?: Context
): Promise<[T[], number]> ): Promise<[InferRepositoryReturnType<T>[], number]>
create(data: unknown[], context?: Context): Promise<T[]> create(
data: unknown[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
delete(ids: string[], context?: Context): Promise<void> delete(ids: string[], context?: Context): Promise<void>
} }
+10 -4
View File
@@ -1,3 +1,5 @@
import { Constructor } from "../modules-sdk"
type ExpandProperty<T> = T extends (infer U)[] ? NonNullable<U> : NonNullable<T> type ExpandProperty<T> = T extends (infer U)[] ? NonNullable<U> : NonNullable<T>
export type Dictionary<T = any> = { export type Dictionary<T = any> = {
@@ -84,9 +86,7 @@ type FilterValue<T> =
type PrevLimit = [never, 0, 1, 2] type PrevLimit = [never, 0, 1, 2]
export type FilterQuery<T = any, Prev extends number = 3> = Prev extends never export type FilterQueryProperties<T, Prev extends number = 3> = {
? never
: {
[Key in keyof T]?: T[Key] extends [Key in keyof T]?: T[Key] extends
| boolean | boolean
| number | number
@@ -102,7 +102,13 @@ export type FilterQuery<T = any, Prev extends number = 3> = Prev extends never
: never : never
: never : never
: never : never
} }
export type FilterQuery<T = any, Prev extends number = 3> = Prev extends never
? never
: T extends Constructor<infer Prototype>
? FilterQueryProperties<Prototype, Prev>
: FilterQueryProperties<T, Prev>
declare type QueryOrder = "ASC" | "DESC" | "asc" | "desc" declare type QueryOrder = "ASC" | "DESC" | "asc" | "desc"
+16 -9
View File
@@ -55,6 +55,13 @@ export type RelationshipTypes =
| "belongsTo" | "belongsTo"
| "manyToMany" | "manyToMany"
/**
* Return true if the relationship is nullable
*/
export type IsNullableRelation<T> = T extends () => IDmlEntity<any, any> | null
? true
: false
/** /**
* The meta-data returned by the property parse method * The meta-data returned by the property parse method
*/ */
@@ -135,14 +142,12 @@ export interface EntityConstructor<Props> extends Function {
* "belongsTo" relation meaning "hasOne" and "ManyToOne" * "belongsTo" relation meaning "hasOne" and "ManyToOne"
*/ */
export type InferForeignKeys<Schema extends DMLSchema> = { export type InferForeignKeys<Schema extends DMLSchema> = {
[K in keyof Schema as Schema[K] extends { type: infer Type } [K in keyof Schema as Schema[K] extends { type: "belongsTo" }
? Type extends RelationshipTypes
? `${K & string}_id` ? `${K & string}_id`
: never : never]: Schema[K] extends { type: "belongsTo" }
: never]: Schema[K] extends { type: infer Type } ? null extends Schema[K]["$dataType"]
? Type extends RelationshipTypes ? string | null
? string : string
: never
: never : never
} }
@@ -182,7 +187,8 @@ export type InferManyToManyFields<Relation> = InferHasManyFields<Relation>
* Inferring the types of the schema fields from the DML * Inferring the types of the schema fields from the DML
* entity * entity
*/ */
export type InferSchemaFields<Schema extends DMLSchema> = Prettify<{ export type InferSchemaFields<Schema extends DMLSchema> = Prettify<
{
[K in keyof Schema]: Schema[K] extends RelationshipType<any> [K in keyof Schema]: Schema[K] extends RelationshipType<any>
? Schema[K]["type"] extends "belongsTo" ? Schema[K]["type"] extends "belongsTo"
? InferBelongsToFields<Schema[K]["$dataType"]> ? InferBelongsToFields<Schema[K]["$dataType"]>
@@ -194,7 +200,8 @@ export type InferSchemaFields<Schema extends DMLSchema> = Prettify<{
? InferManyToManyFields<Schema[K]["$dataType"]> ? InferManyToManyFields<Schema[K]["$dataType"]>
: never : never
: Schema[K]["$dataType"] : Schema[K]["$dataType"]
}> } & InferForeignKeys<Schema>
>
/** /**
* Helper to infer the schema type of a DmlEntity * Helper to infer the schema type of a DmlEntity
+1 -1
View File
@@ -69,7 +69,7 @@
"scripts": { "scripts": {
"build": "rimraf dist && tsc --build", "build": "rimraf dist && tsc --build",
"watch": "tsc --build --watch", "watch": "tsc --build --watch",
"test": "jest --silent=false --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts", "test": "jest --silent --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
"test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts" "test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts"
} }
} }
@@ -120,9 +120,9 @@ class Entity3 {
} }
} }
const Entity1Repository = mikroOrmBaseRepositoryFactory<Entity1>(Entity1) const Entity1Repository = mikroOrmBaseRepositoryFactory(Entity1)
const Entity2Repository = mikroOrmBaseRepositoryFactory<Entity2>(Entity2) const Entity2Repository = mikroOrmBaseRepositoryFactory(Entity2)
const Entity3Repository = mikroOrmBaseRepositoryFactory<Entity3>(Entity3) const Entity3Repository = mikroOrmBaseRepositoryFactory(Entity3)
describe("mikroOrmRepository", () => { describe("mikroOrmRepository", () => {
let orm!: MikroORM let orm!: MikroORM
@@ -1,8 +1,10 @@
import { import {
BaseFilterable,
Context, Context,
DAL, DAL,
FilterQuery, FilterQuery,
FindOptions,
InferEntityType,
InferRepositoryReturnType,
FilterQuery as InternalFilterQuery, FilterQuery as InternalFilterQuery,
PerformedActions, PerformedActions,
RepositoryService, RepositoryService,
@@ -15,11 +17,10 @@ import {
EntityName, EntityName,
EntityProperty, EntityProperty,
EntitySchema, EntitySchema,
LoadStrategy,
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions, FindOptions as MikroOptions,
LoadStrategy,
ReferenceType, ReferenceType,
RequiredEntityData,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { import {
@@ -28,6 +29,7 @@ import {
MedusaError, MedusaError,
promiseAll, promiseAll,
} from "../../common" } from "../../common"
import { toMikroORMEntity } from "../../dml"
import { buildQuery } from "../../modules-sdk/build-query" import { buildQuery } from "../../modules-sdk/build-query"
import { import {
getSoftDeletedCascadedEntitiesIdsMappedBy, getSoftDeletedCascadedEntitiesIdsMappedBy,
@@ -37,7 +39,7 @@ import { dbErrorMapper } from "./db-error-mapper"
import { mikroOrmSerializer } from "./mikro-orm-serializer" import { mikroOrmSerializer } from "./mikro-orm-serializer"
import { mikroOrmUpdateDeletedAtRecursively } from "./utils" import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
export class MikroOrmBase<T = any> { export class MikroOrmBase {
readonly manager_: any readonly manager_: any
protected constructor({ manager }) { protected constructor({ manager }) {
@@ -90,10 +92,12 @@ export class MikroOrmBase<T = any> {
* related ones. * related ones.
*/ */
export class MikroOrmBaseRepository<T extends object = object> export class MikroOrmBaseRepository<const T extends object = object>
extends MikroOrmBase<T> extends MikroOrmBase
implements RepositoryService<T> implements RepositoryService<T>
{ {
entity: EntityClass<InferEntityType<T>>
constructor(...args: any[]) { constructor(...args: any[]) {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
@@ -144,43 +148,55 @@ export class MikroOrmBaseRepository<T extends object = object>
}) })
} }
create(data: unknown[], context?: Context): Promise<T[]> { create(
throw new Error("Method not implemented.") data: unknown[],
}
update(data: { entity; update }[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.")
}
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
context?: Context context?: Context
): Promise<void> { ): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> { update(
data: { entity; update }[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.")
}
delete(idsOrPKs: FindOptions<T>["where"], context?: Context): Promise<void> {
throw new Error("Method not implemented.")
}
find(
options?: DAL.FindOptions<T>,
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
findAndCount( findAndCount(
options?: DAL.FindOptions<T>, options?: DAL.FindOptions<T>,
context?: Context context?: Context
): Promise<[T[], number]> { ): Promise<[InferRepositoryReturnType<T>[], number]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
upsert(data: unknown[], context: Context = {}): Promise<T[]> { upsert(
data: unknown[],
context: Context = {}
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
upsertWithReplace( upsertWithReplace(
data: unknown[], data: unknown[],
config: UpsertWithReplaceConfig<T> = { config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
relations: [], relations: [],
}, },
context: Context = {} context: Context = {}
): Promise<{ entities: T[]; performedActions: PerformedActions }> { ): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -188,10 +204,10 @@ export class MikroOrmBaseRepository<T extends object = object>
filters: filters:
| string | string
| string[] | string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>) | DAL.FindOptions<T>["where"]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[], | DAL.FindOptions<T>["where"][],
sharedContext: Context = {} sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> { ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
const entities = await this.find({ where: filters as any }, sharedContext) const entities = await this.find({ where: filters as any }, sharedContext)
const date = new Date() const date = new Date()
@@ -212,8 +228,8 @@ export class MikroOrmBaseRepository<T extends object = object>
async restore( async restore(
idsOrFilter: string[] | InternalFilterQuery, idsOrFilter: string[] | InternalFilterQuery,
sharedContext: Context = {} sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> { ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
const query = buildQuery(idsOrFilter, { const query = buildQuery<T>(idsOrFilter, {
withDeleted: true, withDeleted: true,
}) })
@@ -245,13 +261,13 @@ export class MikroOrmBaseRepository<T extends object = object>
findOptions.where = { findOptions.where = {
$and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }], $and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }],
} as unknown as DAL.FilterQuery<T & { q?: string }> } as unknown as DAL.FindOptions<T & { q?: string }>["where"]
} }
} }
export class MikroOrmBaseTreeRepository< export class MikroOrmBaseTreeRepository<
T extends object = object const T extends object = object
> extends MikroOrmBase<T> { > extends MikroOrmBase {
constructor() { constructor() {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
@@ -261,7 +277,7 @@ export class MikroOrmBaseTreeRepository<
options?: DAL.FindOptions, options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions, transformOptions?: RepositoryTransformOptions,
context?: Context context?: Context
): Promise<T[]> { ): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -269,15 +285,21 @@ export class MikroOrmBaseTreeRepository<
options?: DAL.FindOptions, options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions, transformOptions?: RepositoryTransformOptions,
context?: Context context?: Context
): Promise<[T[], number]> { ): Promise<[InferRepositoryReturnType<T>[], number]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
create(data: unknown[], context?: Context): Promise<T[]> { create(
data: unknown[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
update(data: unknown[], context?: Context): Promise<T[]> { update(
data: unknown[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -286,12 +308,18 @@ export class MikroOrmBaseTreeRepository<
} }
} }
export function mikroOrmBaseRepositoryFactory<T extends object = object>( export function mikroOrmBaseRepositoryFactory<const T extends object>(
entity: any entity: T
): { ): {
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T> new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
} { } {
const mikroOrmEntity = toMikroORMEntity(entity) as EntityClass<
InferEntityType<T>
>
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> { class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
entity = mikroOrmEntity
// @ts-ignore // @ts-ignore
constructor(...args: any[]) { constructor(...args: any[]) {
// @ts-ignore // @ts-ignore
@@ -315,19 +343,19 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
}) })
} }
async create(data: any[], context?: Context): Promise<T[]> { async create(
data: any[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
const entities = data.map((data_) => { const entities = data.map((data_) => {
return manager.create( return manager.create(this.entity, data_)
entity as EntityName<T>,
data_ as RequiredEntityData<T>
)
}) })
manager.persist(entities) manager.persist(entities)
return entities return entities as InferRepositoryReturnType<T>[]
} }
/** /**
@@ -350,7 +378,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const relations = manager const relations = manager
.getDriver() .getDriver()
.getMetadata() .getMetadata()
.get(entity.name).relations .get(this.entity.name).relations
// In case an empty array is provided for a collection relation of type m:n, this relation needs to be init in order to be // In case an empty array is provided for a collection relation of type m:n, this relation needs to be init in order to be
// able to perform an application cascade action. // able to perform an application cascade action.
@@ -397,7 +425,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
} }
} }
async update(data: { entity; update }[], context?: Context): Promise<T[]> { async update(
data: { entity; update }[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
await this.initManyToManyToDetachAllItemsIfNeeded(data, context) await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
@@ -411,17 +442,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
} }
async delete( async delete(
filters: FilterQuery<T> & BaseFilterable<FilterQuery<T>>, filters: FindOptions<T>["where"],
context?: Context context?: Context
): Promise<void> { ): Promise<void> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
await manager.nativeDelete<T>(entity as EntityName<T>, filters as any) await manager.nativeDelete<T>(this.entity, filters)
} }
async find( async find(
options: DAL.FindOptions<T> = { where: {} }, options: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context?: Context context?: Context
): Promise<T[]> { ): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
const findOptions_ = { ...options } const findOptions_ = { ...options }
@@ -443,17 +474,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
findOptions: findOptions_, findOptions: findOptions_,
}) })
return await manager.find( return (await manager.find(
entity as EntityName<T>, this.entity as EntityName<T>,
findOptions_.where as MikroFilterQuery<T>, findOptions_.where as MikroFilterQuery<T>,
findOptions_.options as MikroOptions<T> findOptions_.options as MikroOptions<T>
) )) as InferRepositoryReturnType<T>[]
} }
async findAndCount( async findAndCount(
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context: Context = {} context: Context = {}
): Promise<[T[], number]> { ): Promise<[InferRepositoryReturnType<T>[], number]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
const findOptions_ = { ...findOptions } const findOptions_ = { ...findOptions }
@@ -467,18 +498,22 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
findOptions: findOptions_, findOptions: findOptions_,
}) })
return await manager.findAndCount( return (await manager.findAndCount(
entity as EntityName<T>, this.entity,
findOptions_.where as MikroFilterQuery<T>, findOptions_.where,
findOptions_.options as MikroOptions<T> findOptions_.options as any // MikroOptions<T>
) )) as [InferRepositoryReturnType<T>[], number]
} }
async upsert(data: any[], context: Context = {}): Promise<T[]> { async upsert(
data: any[],
context: Context = {}
): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
const primaryKeys = const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) this.entity
)
let primaryKeysCriteria: { [key: string]: any }[] = [] let primaryKeysCriteria: { [key: string]: any }[] = []
if (primaryKeys.length === 1) { if (primaryKeys.length === 1) {
@@ -497,7 +532,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
})) }))
} }
let allEntities: T[][] = [] let allEntities: InferRepositoryReturnType<T>[][] = []
if (primaryKeysCriteria.length) { if (primaryKeysCriteria.length) {
allEntities = await Promise.all( allEntities = await Promise.all(
@@ -513,7 +548,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const existingEntities = allEntities.flat() const existingEntities = allEntities.flat()
const existingEntitiesMap = new Map<string, T>() const existingEntitiesMap = new Map<
string,
InferRepositoryReturnType<T>
>()
existingEntities.forEach((entity) => { existingEntities.forEach((entity) => {
if (entity) { if (entity) {
const key = const key =
@@ -525,9 +563,9 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
} }
}) })
const upsertedEntities: T[] = [] const upsertedEntities: InferRepositoryReturnType<T>[] = []
const createdEntities: T[] = [] const createdEntities: InferRepositoryReturnType<T>[] = []
const updatedEntities: T[] = [] const updatedEntities: InferRepositoryReturnType<T>[] = []
data.forEach((data_) => { data.forEach((data_) => {
// In case the data provided are just strings, then we build an object with the primary key as the key and the data as the valuecd - // In case the data provided are just strings, then we build an object with the primary key as the key and the data as the valuecd -
@@ -542,8 +580,8 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const updatedType = manager.assign(existingEntity, data_) const updatedType = manager.assign(existingEntity, data_)
updatedEntities.push(updatedType) updatedEntities.push(updatedType)
} else { } else {
const newEntity = manager.create<T>(entity, data_) const newEntity = manager.create(this.entity, data_)
createdEntities.push(newEntity) createdEntities.push(newEntity as InferRepositoryReturnType<T>)
} }
}) })
@@ -558,7 +596,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
} }
// TODO return the all, created, updated entities // TODO return the all, created, updated entities
return upsertedEntities return upsertedEntities as InferRepositoryReturnType<T>[]
} }
// UpsertWithReplace does several things to simplify module implementation. // UpsertWithReplace does several things to simplify module implementation.
@@ -570,11 +608,14 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
// We only support 1-level depth of upserts. We don't support custom fields on the many-to-many pivot tables for now // We only support 1-level depth of upserts. We don't support custom fields on the many-to-many pivot tables for now
async upsertWithReplace( async upsertWithReplace(
data: any[], data: any[],
config: UpsertWithReplaceConfig<T> = { config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
relations: [], relations: [],
}, },
context: Context = {} context: Context = {}
): Promise<{ entities: T[]; performedActions: PerformedActions }> { ): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}> {
const performedActions: PerformedActions = { const performedActions: PerformedActions = {
created: {}, created: {},
updated: {}, updated: {},
@@ -593,7 +634,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const allRelations = manager const allRelations = manager
.getDriver() .getDriver()
.getMetadata() .getMetadata()
.get(entity.name).relations .get(this.entity.name).relations
const nonexistentRelations = arrayDifference( const nonexistentRelations = arrayDifference(
(config.relations as any) ?? [], (config.relations as any) ?? [],
@@ -624,7 +665,11 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
) )
}) })
const mainEntity = this.getEntityWithId(manager, entity.name, entryCopy) const mainEntity = this.getEntityWithId(
manager,
this.entity.name,
entryCopy
)
reconstructedResponse.push({ ...mainEntity, ...reconstructedEntry }) reconstructedResponse.push({ ...mainEntity, ...reconstructedEntry })
originalDataMap.set(mainEntity.id, entry) originalDataMap.set(mainEntity.id, entry)
@@ -634,7 +679,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
let { let {
orderedEntities: upsertedTopLevelEntities, orderedEntities: upsertedTopLevelEntities,
performedActions: performedActions_, performedActions: performedActions_,
} = await this.upsertMany_(manager, entity.name, toUpsert) } = await this.upsertMany_(manager, this.entity.name, toUpsert)
this.mergePerformedActions(performedActions, performedActions_) this.mergePerformedActions(performedActions, performedActions_)
@@ -954,10 +999,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters: filters:
| string | string
| string[] | string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>) | DAL.FindOptions<T>["where"]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[], | DAL.FindOptions<T>["where"][],
sharedContext: Context = {} sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> { ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
if (Array.isArray(filters) && !filters.filter(Boolean).length) { if (Array.isArray(filters) && !filters.filter(Boolean).length) {
return [[], {}] return [[], {}]
} }
@@ -975,10 +1020,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters: filters:
| string | string
| string[] | string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>) | DAL.FindOptions<T>["where"]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[], | DAL.FindOptions<T>["where"][],
sharedContext: Context = {} sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> { ): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
if (Array.isArray(filters) && !filters.filter(Boolean).length) { if (Array.isArray(filters) && !filters.filter(Boolean).length) {
return [[], {}] return [[], {}]
} }
@@ -996,11 +1041,12 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters: filters:
| string | string
| string[] | string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>) | DAL.FindOptions<T>["where"]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[] | DAL.FindOptions<T>["where"][]
) { ): DAL.FindOptions<T>["where"] {
const primaryKeys = const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) this.entity
)
const filterArray = Array.isArray(filters) ? filters : [filters] const filterArray = Array.isArray(filters) ? filters : [filters]
const normalizedFilters: FilterQuery = { const normalizedFilters: FilterQuery = {
@@ -1014,7 +1060,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
}), }),
} }
return normalizedFilters return normalizedFilters as DAL.FindOptions<T>["where"]
} }
} }
@@ -4,14 +4,15 @@ import { DmlEntity } from "../entity"
import { model } from "../entity-builder" import { model } from "../entity-builder"
import { DuplicateIdPropertyError } from "../errors" import { DuplicateIdPropertyError } from "../errors"
import { import {
createMikrORMEntity, mikroORMEntityBuilder,
toMikroOrmEntities,
toMikroORMEntity, toMikroORMEntity,
toMikroOrmEntities,
} from "../helpers/create-mikro-orm-entity" } from "../helpers/create-mikro-orm-entity"
describe("Entity builder", () => { describe("Entity builder", () => {
beforeEach(() => { beforeEach(() => {
MetadataStorage.clear() MetadataStorage.clear()
mikroORMEntityBuilder.clear()
}) })
const defaultColumnMetadata = { const defaultColumnMetadata = {
@@ -992,12 +993,14 @@ describe("Entity builder", () => {
}) })
const User = toMikroORMEntity(user) const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{ expectTypeOf(new User()).toEqualTypeOf<{
id: number id: number
username: string username: string
email: string email: string
role: "moderator" | "admin" | "guest" role: "moderator" | "admin" | "guest"
deleted_at: Date | null deleted_at: Date | null
created_at: Date
updated_at: Date
}>() }>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) const metaData = MetadataStorage.getMetadataFromDecorator(User)
@@ -1939,6 +1942,11 @@ describe("Entity builder", () => {
expression: expression:
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL',
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
]) ])
expect(metaData.filters).toEqual({ expect(metaData.filters).toEqual({
@@ -2050,6 +2058,11 @@ describe("Entity builder", () => {
expression: expression:
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "platform"."user" (email) WHERE deleted_at IS NULL', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "platform"."user" (email) WHERE deleted_at IS NULL',
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "platform"."user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
]) ])
expect(metaData.filters).toEqual({ expect(metaData.filters).toEqual({
@@ -2160,6 +2173,11 @@ describe("Entity builder", () => {
expression: expression:
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_myEmail_unique" ON "user" (myEmail) WHERE deleted_at IS NULL', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_myEmail_unique" ON "user" (myEmail) WHERE deleted_at IS NULL',
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
]) ])
expect(metaData.filters).toEqual({ expect(metaData.filters).toEqual({
@@ -2819,7 +2837,6 @@ describe("Entity builder", () => {
reference: "scalar", reference: "scalar",
setter: false, setter: false,
type: "string", type: "string",
isForeignKey: true,
persist: false, persist: false,
}, },
created_at: { created_at: {
@@ -2943,12 +2960,21 @@ describe("Entity builder", () => {
nullable: false, nullable: false,
onDelete: undefined, onDelete: undefined,
reference: "m:1", reference: "m:1",
isForeignKey: true,
}, },
...defaultColumnMetadata, ...defaultColumnMetadata,
}) })
expect(metaData.indexes).toEqual([ expect(metaData.indexes).toEqual([
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
{ {
expression: expression:
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_account_unique" ON "user" (email, account) WHERE deleted_at IS NULL', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_account_unique" ON "user" (email, account) WHERE deleted_at IS NULL',
@@ -2969,11 +2995,6 @@ describe("Entity builder", () => {
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL',
name: "IDX_unique-name", name: "IDX_unique-name",
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
]) ])
}) })
@@ -3025,6 +3046,16 @@ describe("Entity builder", () => {
) )
expect(metaData.indexes).toEqual([ expect(metaData.indexes).toEqual([
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
{ {
expression: expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_organization_account" ON "user" (organization, account) WHERE email IS NOT NULL AND deleted_at IS NULL', 'CREATE INDEX IF NOT EXISTS "IDX_user_organization_account" ON "user" (organization, account) WHERE email IS NOT NULL AND deleted_at IS NULL',
@@ -3050,11 +3081,6 @@ describe("Entity builder", () => {
'CREATE INDEX IF NOT EXISTS "IDX_user_account_group_id" ON "user" (account, group_id) WHERE is_owner IS TRUE AND deleted_at IS NULL', 'CREATE INDEX IF NOT EXISTS "IDX_user_account_group_id" ON "user" (account, group_id) WHERE is_owner IS TRUE AND deleted_at IS NULL',
name: "IDX_user_account_group_id", name: "IDX_user_account_group_id",
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
]) ])
}) })
@@ -3121,6 +3147,11 @@ describe("Entity builder", () => {
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL', 'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id", name: "IDX_user_group_id",
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_user_deleted_at",
},
]) ])
const Setting = toMikroORMEntity(setting) const Setting = toMikroORMEntity(setting)
@@ -3132,6 +3163,11 @@ describe("Entity builder", () => {
'CREATE INDEX IF NOT EXISTS "IDX_setting_user_id" ON "setting" (user_id) WHERE deleted_at IS NULL', 'CREATE INDEX IF NOT EXISTS "IDX_setting_user_id" ON "setting" (user_id) WHERE deleted_at IS NULL',
name: "IDX_setting_user_id", name: "IDX_setting_user_id",
}, },
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_setting_deleted_at" ON "setting" (deleted_at) WHERE deleted_at IS NULL',
name: "IDX_setting_deleted_at",
},
]) ])
}) })
}) })
@@ -3543,7 +3579,6 @@ describe("Entity builder", () => {
nullable: false, nullable: false,
onDelete: "cascade", onDelete: "cascade",
reference: "m:1", reference: "m:1",
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -3616,6 +3651,11 @@ describe("Entity builder", () => {
} }
}>() }>()
const userInstance = new User()
expectTypeOf<
(typeof userInstance)["email"]["user_id"]
>().toEqualTypeOf<string>()
expectTypeOf(new Email()).toMatchTypeOf<{ expectTypeOf(new Email()).toMatchTypeOf<{
email: string email: string
isVerified: boolean isVerified: boolean
@@ -3631,6 +3671,7 @@ describe("Entity builder", () => {
} }
} }
}>() }>()
expectTypeOf(new Email().user_id).toEqualTypeOf<string>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User") expect(metaData.className).toEqual("User")
@@ -3740,7 +3781,6 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
persist: false, persist: false,
}, },
created_at: { created_at: {
@@ -3810,6 +3850,11 @@ describe("Entity builder", () => {
} }
}>() }>()
const userInstance = new User()
expectTypeOf<(typeof userInstance)["email"]["user_id"]>().toEqualTypeOf<
string | null
>()
expectTypeOf(new Email()).toMatchTypeOf<{ expectTypeOf(new Email()).toMatchTypeOf<{
email: string email: string
isVerified: boolean isVerified: boolean
@@ -3822,6 +3867,7 @@ describe("Entity builder", () => {
} }
} | null } | null
}>() }>()
expectTypeOf(new Email().user_id).toEqualTypeOf<string | null>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User") expect(metaData.className).toEqual("User")
@@ -3931,7 +3977,6 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
persist: false, persist: false,
}, },
created_at: { created_at: {
@@ -4121,7 +4166,6 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -4310,7 +4354,6 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: true, nullable: true,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -4566,7 +4609,6 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
persist: false, persist: false,
}, },
created_at: { created_at: {
@@ -4765,7 +4807,6 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
persist: false, persist: false,
}, },
created_at: { created_at: {
@@ -4872,7 +4913,6 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
nullable: false, nullable: false,
onDelete: undefined, onDelete: undefined,
isForeignKey: true,
}, },
children: { children: {
cascade: undefined, cascade: undefined,
@@ -4983,7 +5023,6 @@ describe("Entity builder", () => {
name: "parent_id", name: "parent_id",
type: "string", type: "string",
columnType: "text", columnType: "text",
isForeignKey: true,
persist: false, persist: false,
reference: "scalar", reference: "scalar",
getter: false, getter: false,
@@ -5110,8 +5149,9 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotTable: "team_users", pivotTable: "team_users",
mappedBy: "users", inversedBy: "users",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -5177,7 +5217,9 @@ describe("Entity builder", () => {
users: { users: {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
mappedBy: "teams",
entity: "User", entity: "User",
owner: false,
pivotTable: "team_users", pivotTable: "team_users",
}, },
created_at: { created_at: {
@@ -5288,8 +5330,9 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotTable: "team_users", pivotTable: "team_users",
mappedBy: "users", inversedBy: "users",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -5356,6 +5399,8 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
entity: "User", entity: "User",
owner: false,
mappedBy: "teams",
pivotTable: "team_users", pivotTable: "team_users",
}, },
created_at: { created_at: {
@@ -5500,6 +5545,188 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotTable: "team_users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
expect(teamMetaData.className).toEqual("Team")
expect(teamMetaData.path).toEqual("Team")
expect(teamMetaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
name: {
reference: "scalar",
type: "string",
columnType: "text",
name: "name",
fieldName: "name",
nullable: false,
getter: false,
setter: false,
},
users: {
reference: "m:n",
name: "users",
entity: "User",
owner: false,
pivotTable: "team_users",
mappedBy: "teams",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
})
test("define mappedBy on both sides and reverse order of registering entities", () => {
const team = model.define("team", {
id: model.number(),
name: model.text(),
users: model.manyToMany(() => user, { mappedBy: "teams" }),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const Team = toMikroORMEntity(team)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
teams: {
id: number
name: string
users: {
id: number
username: string
}[]
}[]
}>()
expectTypeOf(new Team()).toMatchTypeOf<{
id: number
name: string
users: {
id: number
username: string
teams: {
id: number
name: string
}[]
}[]
}>()
const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User")
expect(metaData.path).toEqual("User")
expect(metaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
fieldName: "username",
nullable: false,
getter: false,
setter: false,
},
teams: {
reference: "m:n",
name: "teams",
entity: "Team",
owner: false,
pivotTable: "team_users", pivotTable: "team_users",
mappedBy: "users", mappedBy: "users",
}, },
@@ -5568,11 +5795,8 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
entity: "User", entity: "User",
owner: true,
pivotTable: "team_users", pivotTable: "team_users",
/**
* The other side should be inversed in order for Mikro ORM
* to work. Both sides cannot have mappedBy.
*/
inversedBy: "teams", inversedBy: "teams",
}, },
created_at: { created_at: {
@@ -5613,190 +5837,6 @@ describe("Entity builder", () => {
}) })
}) })
test("define mappedBy on both sides and reverse order of registering entities", () => {
const team = model.define("team", {
id: model.number(),
name: model.text(),
users: model.manyToMany(() => user, { mappedBy: "teams" }),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
const Team = entityBuilder(team)
const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
teams: {
id: number
name: string
users: {
id: number
username: string
}[]
}[]
}>()
expectTypeOf(new Team()).toMatchTypeOf<{
id: number
name: string
users: {
id: number
username: string
teams: {
id: number
name: string
}[]
}[]
}>()
const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User")
expect(metaData.path).toEqual("User")
expect(metaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
fieldName: "username",
nullable: false,
getter: false,
setter: false,
},
teams: {
reference: "m:n",
name: "teams",
entity: "Team",
pivotTable: "team_users",
/**
* The other side should be inversed in order for Mikro ORM
* to work. Both sides cannot have mappedBy.
*/
inversedBy: "users",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
expect(teamMetaData.className).toEqual("Team")
expect(teamMetaData.path).toEqual("Team")
expect(teamMetaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
name: {
reference: "scalar",
type: "string",
columnType: "text",
name: "name",
fieldName: "name",
nullable: false,
getter: false,
setter: false,
},
users: {
reference: "m:n",
name: "users",
entity: "User",
pivotTable: "team_users",
mappedBy: "teams",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
})
test("define multiple many to many relationships to the same entity", () => { test("define multiple many to many relationships to the same entity", () => {
const team = model.define("team", { const team = model.define("team", {
id: model.number(), id: model.number(),
@@ -5816,9 +5856,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }), teams: model.manyToMany(() => team, { mappedBy: "users" }),
}) })
const entityBuilder = createMikrORMEntity() const Team = toMikroORMEntity(team)
const Team = entityBuilder(team) const User = toMikroORMEntity(user)
const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{ expectTypeOf(new User()).toMatchTypeOf<{
id: number id: number
@@ -5886,19 +5925,17 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: false,
pivotTable: "team_users", pivotTable: "team_users",
/** mappedBy: "users",
* The other side should be inversed in order for Mikro ORM
* to work. Both sides cannot have mappedBy.
*/
inversedBy: "users",
}, },
activeTeams: { activeTeams: {
reference: "m:n", reference: "m:n",
name: "activeTeams", name: "activeTeams",
entity: "Team", entity: "Team",
owner: false,
pivotTable: "team_users", pivotTable: "team_users",
inversedBy: "activeTeamsUsers", mappedBy: "activeTeamsUsers",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -5965,15 +6002,17 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
entity: "User", entity: "User",
owner: true,
pivotTable: "team_users", pivotTable: "team_users",
mappedBy: "teams", inversedBy: "teams",
}, },
activeTeamsUsers: { activeTeamsUsers: {
reference: "m:n", reference: "m:n",
name: "activeTeamsUsers", name: "activeTeamsUsers",
entity: "User", entity: "User",
owner: true,
pivotTable: "team_users", pivotTable: "team_users",
mappedBy: "activeTeams", inversedBy: "activeTeams",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -6086,8 +6125,9 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotTable: "platform.team_users", pivotTable: "platform.team_users",
mappedBy: "users", inversedBy: "users",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -6155,6 +6195,8 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
entity: "User", entity: "User",
owner: false,
mappedBy: "teams",
pivotTable: "platform.team_users", pivotTable: "platform.team_users",
}, },
created_at: { created_at: {
@@ -6195,13 +6237,168 @@ describe("Entity builder", () => {
}) })
}) })
test("should compute the pivot table name correctly", () => {
const team = model.define("teamSquad", {
id: model.number(),
name: model.text(),
users: model.manyToMany(() => user),
})
const user = model.define("RandomUser", {
id: model.number(),
username: model.text(),
teams: model.manyToMany(() => team, {
mappedBy: "users",
}),
})
const User = toMikroORMEntity(user)
const Team = toMikroORMEntity(team)
const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("RandomUser")
expect(metaData.path).toEqual("RandomUser")
expect(metaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
fieldName: "username",
nullable: false,
getter: false,
setter: false,
},
teams: {
reference: "m:n",
name: "teams",
entity: "TeamSquad",
owner: true,
pivotTable: "random_user_team_squads",
inversedBy: "users",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
expect(teamMetaData.className).toEqual("TeamSquad")
expect(teamMetaData.path).toEqual("TeamSquad")
expect(teamMetaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
name: {
reference: "scalar",
type: "string",
columnType: "text",
name: "name",
fieldName: "name",
nullable: false,
getter: false,
setter: false,
},
users: {
reference: "m:n",
name: "users",
entity: "RandomUser",
owner: false,
mappedBy: "teams",
pivotTable: "random_user_team_squads",
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
fieldName: "updated_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
})
test("define custom pivot table name", () => { test("define custom pivot table name", () => {
const team = model.define("team", { const team = model.define("team", {
id: model.number(), id: model.number(),
name: model.text(), name: model.text(),
users: model.manyToMany(() => user, { users: model.manyToMany(() => user),
pivotTable: "users_teams",
}),
}) })
const user = model.define("user", { const user = model.define("user", {
@@ -6270,8 +6467,9 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotTable: "users_teams", pivotTable: "users_teams",
mappedBy: "users", inversedBy: "users",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -6337,7 +6535,9 @@ describe("Entity builder", () => {
users: { users: {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
owner: false,
entity: "User", entity: "User",
mappedBy: "teams",
pivotTable: "users_teams", pivotTable: "users_teams",
}, },
created_at: { created_at: {
@@ -6454,7 +6654,6 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
user: { user: {
reference: "scalar", reference: "scalar",
@@ -6473,7 +6672,6 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "team_id", fieldName: "team_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
team: { team: {
reference: "scalar", reference: "scalar",
@@ -6549,8 +6747,9 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "teams", name: "teams",
entity: "Team", entity: "Team",
owner: true,
pivotEntity: "TeamUsers", pivotEntity: "TeamUsers",
mappedBy: "users", inversedBy: "users",
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -6617,6 +6816,8 @@ describe("Entity builder", () => {
reference: "m:n", reference: "m:n",
name: "users", name: "users",
entity: "User", entity: "User",
owner: false,
mappedBy: "teams",
pivotEntity: "TeamUsers", pivotEntity: "TeamUsers",
}, },
created_at: { created_at: {
@@ -56,6 +56,14 @@ export type ManyToManyOptions = RelationshipOptions &
* @ignore * @ignore
*/ */
pivotEntity?: never pivotEntity?: never
/**
* The column name in the pivot table that for the current entity
*/
joinColumn?: string
/**
* The column name in the pivot table for the opposite entity
*/
inverseJoinColumn?: string
} }
| { | {
/** /**
@@ -1,8 +1,8 @@
import type { PropertyType } from "@medusajs/types" import type { PropertyType } from "@medusajs/types"
import { DmlEntity } from "../entity" import { DmlEntity } from "../entity"
import { parseEntityName } from "./entity-builder/parse-entity-name" import { parseEntityName } from "./entity-builder/parse-entity-name"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
import { setGraphQLRelationship } from "./graphql-builder/set-relationship" import { setGraphQLRelationship } from "./graphql-builder/set-relationship"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
export function generateGraphQLFromEntity<T extends DmlEntity<any, any>>( export function generateGraphQLFromEntity<T extends DmlEntity<any, any>>(
entity: T entity: T
@@ -1,4 +1,5 @@
import type { import type {
Constructor,
DMLSchema, DMLSchema,
EntityConstructor, EntityConstructor,
IDmlEntity, IDmlEntity,
@@ -6,10 +7,11 @@ import type {
PropertyType, PropertyType,
} from "@medusajs/types" } from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core" import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity" import { DmlEntity } from "../entity"
import { DuplicateIdPropertyError } from "../errors"
import { IdProperty } from "../properties/id" import { IdProperty } from "../properties/id"
import { DuplicateIdPropertyError } from "../errors"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { applySearchable } from "./entity-builder/apply-searchable" import { applySearchable } from "./entity-builder/apply-searchable"
import { defineProperty } from "./entity-builder/define-property" import { defineProperty } from "./entity-builder/define-property"
import { defineRelationship } from "./entity-builder/define-relationship" import { defineRelationship } from "./entity-builder/define-relationship"
@@ -21,7 +23,7 @@ import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes"
* value is a function that can be used to convert DML entities * value is a function that can be used to convert DML entities
* to Mikro ORM entities. * to Mikro ORM entities.
*/ */
export function createMikrORMEntity() { function createMikrORMEntity() {
/** /**
* The following property is used to track many to many relationship * The following property is used to track many to many relationship
* between two entities. It is needed because we have to mark one * between two entities. It is needed because we have to mark one
@@ -35,20 +37,21 @@ export function createMikrORMEntity() {
* - [user.teams]: true // the teams relationship on user is an owner * - [user.teams]: true // the teams relationship on user is an owner
* - [team.users] // cannot be an owner * - [team.users] // cannot be an owner
*/ */
// TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder let MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> = {}
const MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> = {} let ENTITIES: Record<string, Constructor<any>> = {}
/** /**
* A helper function to define a Mikro ORM entity from a * A helper function to define a Mikro ORM entity from a
* DML entity. * DML entity.
*/ */
return function createEntity<T extends DmlEntity<any, any>>( function createEntity<T extends DmlEntity<any, any>>(entity: T): Infer<T> {
entity: T
): Infer<T> {
class MikroORMEntity {} class MikroORMEntity {}
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse() const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
const { modelName, tableName } = parseEntityName(entity) const { modelName, tableName } = parseEntityName(entity)
if (ENTITIES[modelName]) {
return ENTITIES[modelName] as Infer<T>
}
/** /**
* Assigning name to the class constructor, so that it matches * Assigning name to the class constructor, so that it matches
@@ -80,11 +83,14 @@ export function createMikrORMEntity() {
hasIdAlreadyDefined = true hasIdAlreadyDefined = true
} }
defineProperty(MikroORMEntity, name, property as PropertyType<any>) defineProperty(MikroORMEntity, property as PropertyType<any>, {
propertyName: name,
tableName,
})
applyIndexes(MikroORMEntity, tableName, field) applyIndexes(MikroORMEntity, tableName, field)
applySearchable(MikroORMEntity, field) applySearchable(MikroORMEntity, field)
} else { } else {
defineRelationship(MikroORMEntity, field, cascades, context) defineRelationship(MikroORMEntity, entity, field, cascades, context)
applySearchable(MikroORMEntity, field) applySearchable(MikroORMEntity, field)
} }
}) })
@@ -94,12 +100,31 @@ export function createMikrORMEntity() {
/** /**
* Converting class to a MikroORM entity * Converting class to a MikroORM entity
*/ */
return Entity({ tableName })( const RegisteredEntity = Entity({ tableName })(
Filter(mikroOrmSoftDeletableFilterOptions)(MikroORMEntity) Filter(mikroOrmSoftDeletableFilterOptions)(MikroORMEntity)
) as Infer<T> ) as Infer<T>
ENTITIES[modelName] = RegisteredEntity
return RegisteredEntity
} }
/**
* Clear the internally tracked entities and relationships
*/
createEntity.clear = function () {
MANY_TO_MANY_TRACKED_RELATIONS = {}
ENTITIES = {}
}
return createEntity
} }
/**
* Helper function to convert DML entities to MikroORM entity. Use
* "toMikroORMEntity" if you are ensure the input is a DML entity
* or not.
*/
export const mikroORMEntityBuilder = createMikrORMEntity()
/** /**
* Takes a DML entity and returns a Mikro ORM entity otherwise * Takes a DML entity and returns a Mikro ORM entity otherwise
* return the input idempotently * return the input idempotently
@@ -111,7 +136,7 @@ export const toMikroORMEntity = <T>(
let mikroOrmEntity: T | EntityConstructor<any> = entity let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) { if (DmlEntity.isDmlEntity(entity)) {
mikroOrmEntity = createMikrORMEntity()(entity) mikroOrmEntity = mikroORMEntityBuilder(entity)
} }
return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T
@@ -123,11 +148,9 @@ export const toMikroORMEntity = <T>(
* @param entities * @param entities
*/ */
export const toMikroOrmEntities = function <T extends any[]>(entities: T) { export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
const entityBuilder = createMikrORMEntity()
return entities.map((entity) => { return entities.map((entity) => {
if (DmlEntity.isDmlEntity(entity)) { if (DmlEntity.isDmlEntity(entity)) {
return entityBuilder(entity) return mikroORMEntityBuilder(entity)
} }
return entity return entity
@@ -16,6 +16,7 @@ import {
Utils, Utils,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { PrimaryKeyModifier } from "../../properties/primary-key" import { PrimaryKeyModifier } from "../../properties/primary-key"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
/** /**
* DML entity data types to PostgreSQL data types via * DML entity data types to PostgreSQL data types via
@@ -64,7 +65,8 @@ const PROPERTY_TYPES: {
const SPECIAL_PROPERTIES: { const SPECIAL_PROPERTIES: {
[propertyName: string]: ( [propertyName: string]: (
MikroORMEntity: EntityConstructor<any>, MikroORMEntity: EntityConstructor<any>,
field: PropertyMetadata field: PropertyMetadata,
tableName: string
) => void ) => void
} = { } = {
created_at: (MikroORMEntity, field) => { created_at: (MikroORMEntity, field) => {
@@ -88,6 +90,21 @@ const SPECIAL_PROPERTIES: {
onUpdate: () => new Date(), onUpdate: () => new Date(),
})(MikroORMEntity.prototype, field.fieldName) })(MikroORMEntity.prototype, field.fieldName)
}, },
deleted_at: (MikroORMEntity, field, tableName) => {
Property({
columnType: "timestamptz",
type: "date",
nullable: true,
fieldName: field.fieldName,
})(MikroORMEntity.prototype, field.fieldName)
applyEntityIndexes(MikroORMEntity, tableName, [
{
on: ["deleted_at"],
where: "deleted_at IS NULL",
},
])
},
} }
/** /**
@@ -95,8 +112,8 @@ const SPECIAL_PROPERTIES: {
*/ */
export function defineProperty( export function defineProperty(
MikroORMEntity: EntityConstructor<any>, MikroORMEntity: EntityConstructor<any>,
propertyName: string, property: PropertyType<any>,
property: PropertyType<any> { tableName, propertyName }: { tableName: string; propertyName: string }
) { ) {
const field = property.parse(propertyName) const field = property.parse(propertyName)
/** /**
@@ -112,10 +129,11 @@ export function defineProperty(
} }
if (SPECIAL_PROPERTIES[field.fieldName]) { if (SPECIAL_PROPERTIES[field.fieldName]) {
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field) SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field, tableName)
return return
} }
if (field.dataType.name === "bigNumber") {
/** /**
* Defining an big number property * Defining an big number property
* A big number property always comes with a raw_{{ fieldName }} column * A big number property always comes with a raw_{{ fieldName }} column
@@ -123,7 +141,6 @@ export function defineProperty(
* The `raw_` field is generated during DML schema generation as a json * The `raw_` field is generated during DML schema generation as a json
* dataType. * dataType.
*/ */
if (field.dataType.name === "bigNumber") {
MikroOrmBigNumberProperty({ MikroOrmBigNumberProperty({
nullable: field.nullable, nullable: field.nullable,
fieldName: field.fieldName, fieldName: field.fieldName,
@@ -16,17 +16,77 @@ import {
rel, rel,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common" import { camelToSnakeCase, pluralize } from "../../../common"
import { ForeignKey } from "../../../dal/mikro-orm/decorators/foreign-key"
import { DmlEntity } from "../../entity" import { DmlEntity } from "../../entity"
import { HasMany } from "../../relations/has-many" import { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one" import { HasOne } from "../../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many" import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
import { parseEntityName } from "./parse-entity-name" import { parseEntityName } from "./parse-entity-name"
type Context = { type Context = {
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
} }
function retrieveOtherSideRelationshipManyToMany({
relationship,
relatedEntity,
relatedModelName,
entity,
}: {
relationship: RelationshipMetadata
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>,
any
>
relatedModelName: string
entity: DmlEntity<any, any>
}): [string, RelationshipType<any>] {
if (relationship.mappedBy) {
return [
relationship.mappedBy,
relatedEntity.parse().schema[relationship.mappedBy],
] as [string, RelationshipType<any>]
}
/**
* Since we don't have the information about the other side of the
* relationship, we will try to find all the other side many to many that refers to the current entity.
* If there is any, we will try to find if at least one of them has a mappedBy.
*/
const potentialOtherSide = Object.entries(relatedEntity.schema)
.filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig))
.filter(([prop, propConfig]) => {
const parsedProp = propConfig.parse(prop) as RelationshipMetadata
const relatedEntity =
typeof parsedProp.entity === "function"
? parsedProp.entity()
: undefined
if (!relatedEntity) {
throw new Error(
`Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function`
)
}
return (
(parsedProp.mappedBy === relationship.name &&
parseEntityName(relatedEntity).modelName ===
parseEntityName(entity).modelName) ||
parseEntityName(relatedEntity).modelName ===
parseEntityName(entity).modelName
)
}) as unknown as [string, RelationshipType<any>][]
if (potentialOtherSide.length > 1) {
throw new Error(
`Invalid relationship reference for "${entity.name}.${relationship.name}". Make sure to set the mappedBy property on one side or the other or both.`
)
}
return potentialOtherSide[0] ?? []
}
/** /**
* Validates a many to many relationship without mappedBy and checks if the other side of the relationship is defined and possesses mappedBy. * Validates a many to many relationship without mappedBy and checks if the other side of the relationship is defined and possesses mappedBy.
* @param MikroORMEntity * @param MikroORMEntity
@@ -39,6 +99,7 @@ function validateManyToManyRelationshipWithoutMappedBy({
relationship, relationship,
relatedEntity, relatedEntity,
relatedModelName, relatedModelName,
entity,
}: { }: {
MikroORMEntity: EntityConstructor<any> MikroORMEntity: EntityConstructor<any>
relationship: RelationshipMetadata relationship: RelationshipMetadata
@@ -47,44 +108,25 @@ function validateManyToManyRelationshipWithoutMappedBy({
any any
> >
relatedModelName: string relatedModelName: string
entity: DmlEntity<any, any>
}) { }) {
/** /**
* Since we don't have the information about the other side of the * Since we don't have the information about the other side of the
* relationship, we will try to find all the other side many to many that refers to the current entity. * relationship, we will try to find all the other side many to many that refers to the current entity.
* If there is any, we will try to find if at least one of them has a mappedBy. * If there is any, we will try to find if at least one of them has a mappedBy.
*/ */
const potentialOtherSides = Object.entries(relatedEntity.schema) const [, potentialOtherSide] = retrieveOtherSideRelationshipManyToMany({
.filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig)) relationship,
.filter(([prop, propConfig]) => { relatedEntity,
const parsedProp = propConfig.parse(prop) as RelationshipMetadata relatedModelName,
const relatedEntity = entity,
typeof parsedProp.entity === "function" })
? parsedProp.entity()
: undefined
if (!relatedEntity) { if (!potentialOtherSide) {
throw new Error(
`Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function`
)
}
return parseEntityName(relatedEntity).modelName === MikroORMEntity.name
}) as unknown as [string, RelationshipType<any>][]
if (potentialOtherSides.length) {
const hasMappedBy = potentialOtherSides.some(
([, propConfig]) => !!propConfig.parse("").mappedBy
)
if (!hasMappedBy) {
throw new Error( throw new Error(
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.` `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.`
) )
} }
} else {
throw new Error(
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". The other side of the relationship is missing.`
)
}
} }
/** /**
@@ -140,6 +182,7 @@ export function defineHasManyRelationship(
*/ */
export function defineBelongsToRelationship( export function defineBelongsToRelationship(
MikroORMEntity: EntityConstructor<any>, MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata, relationship: RelationshipMetadata,
relatedEntity: DmlEntity< relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>, Record<string, PropertyType<any> | RelationshipType<any>>,
@@ -194,8 +237,7 @@ export function defineBelongsToRelationship(
return return
} }
this[relationship.name] ??= this[foreignKeyName] this[foreignKeyName] ??= this[relationship.name]?.id ?? null
this[foreignKeyName] ??= this[relationship.name]?.id
} }
/** /**
@@ -222,7 +264,6 @@ export function defineBelongsToRelationship(
nullable: relationship.nullable, nullable: relationship.nullable,
onDelete: shouldCascade ? "cascade" : undefined, onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, foreignKeyName) })(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
if (DmlManyToMany.isManyToMany(otherSideRelation)) { if (DmlManyToMany.isManyToMany(otherSideRelation)) {
Property({ Property({
@@ -239,6 +280,13 @@ export function defineBelongsToRelationship(
})(MikroORMEntity.prototype, relationship.name) })(MikroORMEntity.prototype, relationship.name)
} }
const { tableName } = parseEntityName(entity)
applyEntityIndexes(MikroORMEntity, tableName, [
{
on: [foreignKeyName],
where: "deleted_at IS NULL",
},
])
applyForeignKeyAssignationHooks(foreignKeyName) applyForeignKeyAssignationHooks(foreignKeyName)
return return
} }
@@ -270,7 +318,14 @@ export function defineBelongsToRelationship(
nullable: relationship.nullable, nullable: relationship.nullable,
persist: false, persist: false,
})(MikroORMEntity.prototype, foreignKeyName) })(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
const { tableName } = parseEntityName(entity)
applyEntityIndexes(MikroORMEntity, tableName, [
{
on: [foreignKeyName],
where: "deleted_at IS NULL",
},
])
applyForeignKeyAssignationHooks(foreignKeyName) applyForeignKeyAssignationHooks(foreignKeyName)
return return
@@ -289,6 +344,7 @@ export function defineBelongsToRelationship(
*/ */
export function defineManyToManyRelationship( export function defineManyToManyRelationship(
MikroORMEntity: EntityConstructor<any>, MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata, relationship: RelationshipMetadata,
relatedEntity: DmlEntity< relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>, Record<string, PropertyType<any> | RelationshipType<any>>,
@@ -296,59 +352,60 @@ export function defineManyToManyRelationship(
>, >,
{ {
relatedModelName, relatedModelName,
relatedTableName,
pgSchema, pgSchema,
}: { relatedModelName: string; pgSchema: string | undefined }, }: {
relatedModelName: string
pgSchema: string | undefined
relatedTableName: string
},
{ MANY_TO_MANY_TRACKED_RELATIONS }: Context { MANY_TO_MANY_TRACKED_RELATIONS }: Context
) { ) {
let mappedBy = relationship.mappedBy let mappedBy = relationship.mappedBy
let inversedBy: undefined | string let inversedBy: undefined | string
let pivotEntityName: undefined | string let pivotEntityName: undefined | string
let pivotTableName: undefined | string let pivotTableName: undefined | string
let joinColumn: undefined | string = relationship.options.joinColumn
let inverseJoinColumn: undefined | string =
relationship.options.inverseJoinColumn
const [otherSideRelationshipProperty, otherSideRelationship] =
retrieveOtherSideRelationshipManyToMany({
relationship,
relatedEntity,
relatedModelName,
entity,
})
/** /**
* Validating other side of relationship when mapped by is defined * Validating other side of relationship when mapped by is defined
*/ */
if (mappedBy) { if (mappedBy) {
const otherSideRelation = relatedEntity.parse().schema[mappedBy] if (!otherSideRelationship) {
if (!otherSideRelation) {
throw new Error( throw new Error(
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship` `Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
) )
} }
if (!DmlManyToMany.isManyToMany(otherSideRelation)) { if (!DmlManyToMany.isManyToMany(otherSideRelationship)) {
throw new Error( throw new Error(
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship` `Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship`
) )
} }
/**
* Check if the other side has defined a mapped by and if that
* mapping is already tracked as the owner.
*
* - If yes, we will inverse our mapped by
* - Otherwise, we will track ourselves as the owner.
*/
if (
otherSideRelation.parse(mappedBy).mappedBy &&
MANY_TO_MANY_TRACKED_RELATIONS[`${relatedModelName}.${mappedBy}`]
) {
inversedBy = mappedBy
mappedBy = undefined
} else {
MANY_TO_MANY_TRACKED_RELATIONS[
`${MikroORMEntity.name}.${relationship.name}`
] = true
}
} else { } else {
validateManyToManyRelationshipWithoutMappedBy({ validateManyToManyRelationshipWithoutMappedBy({
MikroORMEntity, MikroORMEntity,
relationship, relationship,
relatedEntity, relatedEntity,
relatedModelName, relatedModelName,
entity,
}) })
} }
MANY_TO_MANY_TRACKED_RELATIONS[
`${MikroORMEntity.name}.${relationship.name}`
] = true
/** /**
* Validating pivot entity when it is defined and computing * Validating pivot entity when it is defined and computing
* its name * its name
@@ -371,6 +428,10 @@ export function defineManyToManyRelationship(
} }
if (!pivotEntityName) { if (!pivotEntityName) {
const { tableName } = parseEntityName(entity)
let tableNameWithoutSchema: string
let relatedTableNameWithoutSchema: string
/** /**
* Pivot table name is created as follows (when not explicitly provided) * Pivot table name is created as follows (when not explicitly provided)
* *
@@ -379,20 +440,58 @@ export function defineManyToManyRelationship(
* - Converting them from camelCase to snake_case. * - Converting them from camelCase to snake_case.
* - And finally pluralizing the second entity name. * - And finally pluralizing the second entity name.
*/ */
let [schema, ...tableTokens] = tableName.split(".")
if (!tableTokens.length) {
tableNameWithoutSchema = schema
} else {
tableNameWithoutSchema = tableTokens.join(".")
}
const [relatedSchema, ...relatedTableTokens] = relatedTableName.split(".")
if (!relatedTableTokens.length) {
relatedTableNameWithoutSchema = relatedSchema
} else {
relatedTableNameWithoutSchema = relatedTableTokens.join(".")
}
pivotTableName = pivotTableName =
relationship.options.pivotTable ?? relationship.options.pivotTable ??
[MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()] otherSideRelationship.parse("").options.pivotTable ??
[tableNameWithoutSchema, relatedTableNameWithoutSchema]
.sort() .sort()
.map((token, index) => { .map((token, index) => {
if (index === 1) { if (index === 1) {
return pluralize(camelToSnakeCase(token)) return pluralize(token)
} }
return camelToSnakeCase(token) return token
}) })
.join("_") .join("_")
} }
const otherSideRelationOptions = otherSideRelationship.parse("").options
const isOwner =
!!joinColumn ||
!!inverseJoinColumn ||
!!relationship.options.pivotTable ||
/**
* We can't infer it from the current entity so lets
* look at the otherside configuration as well to make a choice
*/
(!otherSideRelationOptions.pivotTable &&
!otherSideRelationOptions.joinColumn &&
!otherSideRelationOptions.inverseJoinColumn &&
!MANY_TO_MANY_TRACKED_RELATIONS[
`${relatedModelName}.${otherSideRelationshipProperty}`
])
const mappedByProp = isOwner ? "inversedBy" : "mappedBy"
const mappedByPropValue =
mappedBy ?? inversedBy ?? otherSideRelationshipProperty
ManyToMany({ ManyToMany({
owner: isOwner,
entity: relatedModelName, entity: relatedModelName,
...(pivotTableName ...(pivotTableName
? { ? {
@@ -402,8 +501,9 @@ export function defineManyToManyRelationship(
} }
: {}), : {}),
...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}), ...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}),
...(mappedBy ? { mappedBy: mappedBy as any } : {}), ...({ [mappedByProp]: mappedByPropValue } as any),
...(inversedBy ? { inversedBy: inversedBy as any } : {}), ...(joinColumn ? { joinColumn } : {}),
...(inverseJoinColumn ? { inverseJoinColumn } : {}),
})(MikroORMEntity.prototype, relationship.name) })(MikroORMEntity.prototype, relationship.name)
} }
@@ -412,6 +512,7 @@ export function defineManyToManyRelationship(
*/ */
export function defineRelationship( export function defineRelationship(
MikroORMEntity: EntityConstructor<any>, MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata, relationship: RelationshipMetadata,
cascades: EntityCascades<string[]>, cascades: EntityCascades<string[]>,
context: Context context: Context
@@ -474,6 +575,7 @@ export function defineRelationship(
case "belongsTo": case "belongsTo":
defineBelongsToRelationship( defineBelongsToRelationship(
MikroORMEntity, MikroORMEntity,
entity,
relationship, relationship,
relatedEntity, relatedEntity,
relatedEntityInfo relatedEntityInfo
@@ -482,6 +584,7 @@ export function defineRelationship(
case "manyToMany": case "manyToMany":
defineManyToManyRelationship( defineManyToManyRelationship(
MikroORMEntity, MikroORMEntity,
entity,
relationship, relationship,
relatedEntity, relatedEntity,
relatedEntityInfo, relatedEntityInfo,
@@ -3,7 +3,6 @@ import {
EntityIndex, EntityIndex,
PropertyMetadata, PropertyMetadata,
} from "@medusajs/types" } from "@medusajs/types"
import { MetadataStorage } from "@mikro-orm/core"
import { createPsqlIndexStatementHelper } from "../../../common" import { createPsqlIndexStatementHelper } from "../../../common"
import { validateIndexFields } from "../mikro-orm/build-indexes" import { validateIndexFields } from "../mikro-orm/build-indexes"
@@ -38,8 +37,7 @@ export function applyEntityIndexes(
tableName: string, tableName: string,
entityIndexes: EntityIndex[] = [] entityIndexes: EntityIndex[] = []
) { ) {
const foreignKeyIndexes = applyForeignKeyIndexes(MikroORMEntity) const indexes = [...entityIndexes]
const indexes = [...entityIndexes, ...foreignKeyIndexes]
indexes.forEach((index) => { indexes.forEach((index) => {
validateIndexFields(MikroORMEntity, index) validateIndexFields(MikroORMEntity, index)
@@ -55,29 +53,3 @@ export function applyEntityIndexes(
entityIndexStatement.MikroORMIndex()(MikroORMEntity) entityIndexStatement.MikroORMIndex()(MikroORMEntity)
}) })
} }
/*
When a "oneToMany" relationship is found on the MikroORM entity, we create an index by default
on the foreign key property.
*/
function applyForeignKeyIndexes(MikroORMEntity: EntityConstructor<any>) {
const foreignKeyIndexes: EntityIndex[] = []
for (const foreignKey of getEntityForeignKeys(MikroORMEntity)) {
foreignKeyIndexes.push({
on: [foreignKey],
where: "deleted_at IS NULL",
})
}
return foreignKeyIndexes
}
function getEntityForeignKeys(MikroORMEntity: EntityConstructor<any>) {
const properties =
MetadataStorage.getMetadataFromDecorator(MikroORMEntity).properties
return Object.keys(properties).filter(
(propertyName) => properties[propertyName].isForeignKey
)
}
@@ -4,7 +4,10 @@ import {
MikroORM, MikroORM,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { model } from "../../entity-builder" import { model } from "../../entity-builder"
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" import {
mikroORMEntityBuilder,
toMikroOrmEntities,
} from "../../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god" import { createDatabase, dropDatabase } from "pg-god"
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types" import { EntityConstructor } from "@medusajs/types"
@@ -28,6 +31,7 @@ describe("EntityBuilder | enum", () => {
beforeEach(async () => { beforeEach(async () => {
MetadataStorage.clear() MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const user = model.define("user", { const user = model.define("user", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
@@ -1,6 +1,9 @@
import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { MetadataStorage, MikroORM } from "@mikro-orm/core"
import { model } from "../../entity-builder" import { model } from "../../entity-builder"
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" import {
mikroORMEntityBuilder,
toMikroOrmEntities,
} from "../../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god" import { createDatabase, dropDatabase } from "pg-god"
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types" import { EntityConstructor } from "@medusajs/types"
@@ -24,6 +27,7 @@ describe("hasOne - belongTo", () => {
beforeEach(async () => { beforeEach(async () => {
MetadataStorage.clear() MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", { const team = model.define("team", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
@@ -1,12 +1,17 @@
import { join } from "path"
import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { MetadataStorage, MikroORM } from "@mikro-orm/core"
import { model } from "../../entity-builder" import { model } from "../../entity-builder"
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" import {
mikroORMEntityBuilder,
toMikroOrmEntities,
} from "../../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god" import { createDatabase, dropDatabase } from "pg-god"
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal" import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types" import { EntityConstructor } from "@medusajs/types"
import { pgGodCredentials } from "../utils" import { pgGodCredentials } from "../utils"
import { FileSystem } from "../../../common" import { FileSystem } from "../../../common"
import { join } from "path"
jest.setTimeout(30000)
export const fileSystem = new FileSystem( export const fileSystem = new FileSystem(
join(__dirname, "../../integration-tests-migrations-many-to-many") join(__dirname, "../../integration-tests-migrations-many-to-many")
@@ -27,6 +32,7 @@ describe("manyToMany - manyToMany", () => {
beforeEach(async () => { beforeEach(async () => {
MetadataStorage.clear() MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", { const team = model.define("team", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
@@ -191,43 +197,108 @@ describe("manyToMany - manyToMany", () => {
}) })
}) })
it(`should fail to load the dml's if both side of the relation are missing the mappedBy options`, () => { it(`should not fail to load the dml's if both side of the relation are missing the mappedBy options`, () => {
mikroORMEntityBuilder.clear()
const team = model.define("team", { const team = model.define("team", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
name: model.text(), name: model.text(),
users: model.manyToMany(() => user, { users: model.manyToMany(() => user, {
pivotEntity: () => squad, pivot_table: "team_users",
}), }),
}) })
const squad = model.define("teamUsers", { const user = model.define("user", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
user: model.belongsTo(() => user, { mappedBy: "squads" }), username: model.text(),
squad: model.belongsTo(() => team, { mappedBy: "users" }), squads: model.manyToMany(() => team),
})
;[User, Team] = toMikroOrmEntities([user, team])
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
expect((teamMetaData.properties as any).users.mappedBy).toBe("squads")
expect((teamMetaData.properties as any).users.owner).toBe(false)
const userMetaData = MetadataStorage.getMetadataFromDecorator(User)
expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined()
expect((userMetaData.properties as any).squads.inversedBy).toBe("users")
expect((userMetaData.properties as any).squads.owner).toBe(true)
})
it(`should load the dml's correclty when both side of the relation are specifying the mappedBy options without pivot table`, () => {
mikroORMEntityBuilder.clear()
const team = model.define("team", {
id: model.id().primaryKey(),
name: model.text(),
users: model.manyToMany(() => user, {
mappedBy: "squads",
}),
}) })
const user = model.define("user", { const user = model.define("user", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
username: model.text(), username: model.text(),
squads: model.manyToMany(() => team, { squads: model.manyToMany(() => team, {
pivotEntity: () => squad, mappedBy: "users",
}), }),
}) })
let [User, Team] = toMikroOrmEntities([user, team])
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
expect((teamMetaData.properties as any).users.mappedBy).toBe("squads")
expect((teamMetaData.properties as any).users.owner).toBe(false)
expect((teamMetaData.properties as any).users.pivotTable).toBe("team_users")
const userMetaData = MetadataStorage.getMetadataFromDecorator(User)
expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined()
expect((userMetaData.properties as any).squads.inversedBy).toBe("users")
expect((userMetaData.properties as any).squads.owner).toBe(true)
expect((userMetaData.properties as any).squads.pivotTable).toBe(
"team_users"
)
})
it(`should fail to load the dml's if both side of the relation are missing the mappedBy options and multiple relations points to the same entity`, () => {
mikroORMEntityBuilder.clear()
const team = model.define("team", {
id: model.id().primaryKey(),
name: model.text(),
users: model.manyToMany(() => user, {
pivot_table: "team_users",
}),
users2: model.manyToMany(() => user, {
pivot_table: "team_users2",
}),
})
const user = model.define("user", {
id: model.id().primaryKey(),
username: model.text(),
squads: model.manyToMany(() => team),
squads2: model.manyToMany(() => team),
})
let error!: Error let error!: Error
try { try {
;[User, Squad, Team] = toMikroOrmEntities([user, squad, team]) ;[User, Team] = toMikroOrmEntities([user, team])
} catch (e) { } catch (e) {
error = e error = e
} }
expect(error).toBeTruthy() expect(error).toBeDefined()
expect(error.message).toEqual( expect(error?.message).toEqual(
'Invalid relationship reference for "User.squads". "mappedBy" should be defined on one side or the other.' 'Invalid relationship reference for "user.squads". Make sure to set the mappedBy property on one side or the other or both.'
) )
}) })
it(`should fail to load the dml's if the relation is defined only on one side`, () => { it(`should fail to load the dml's if the relation is defined only on one side`, () => {
mikroORMEntityBuilder.clear()
const team = model.define("team", { const team = model.define("team", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
name: model.text(), name: model.text(),
@@ -248,7 +319,7 @@ describe("manyToMany - manyToMany", () => {
expect(error).toBeTruthy() expect(error).toBeTruthy()
expect(error.message).toEqual( expect(error.message).toEqual(
'Invalid relationship reference for "Team.users". The other side of the relationship is missing.' 'Invalid relationship reference for "Team.users". "mappedBy" should be defined on one side or the other.'
) )
}) })
}) })
@@ -1,6 +1,9 @@
import { MetadataStorage, MikroORM } from "@mikro-orm/core" import { MetadataStorage, MikroORM } from "@mikro-orm/core"
import { model } from "../../entity-builder" import { model } from "../../entity-builder"
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity" import {
mikroORMEntityBuilder,
toMikroOrmEntities,
} from "../../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god" import { createDatabase, dropDatabase } from "pg-god"
import { import {
CustomTsMigrationGenerator, CustomTsMigrationGenerator,
@@ -30,6 +33,7 @@ describe("manyToOne - belongTo", () => {
beforeEach(async () => { beforeEach(async () => {
MetadataStorage.clear() MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", { const team = model.define("team", {
id: model.id().primaryKey(), id: model.id().primaryKey(),
@@ -4,9 +4,11 @@ import { MetadataStorage } from "@mikro-orm/core"
import { Migrations } from "../../index" import { Migrations } from "../../index"
import { FileSystem } from "../../../common" import { FileSystem } from "../../../common"
import { DmlEntity, model } from "../../../dml" import { DmlEntity, mikroORMEntityBuilder, model } from "../../../dml"
import { defineMikroOrmCliConfig } from "../../../modules-sdk" import { defineMikroOrmCliConfig } from "../../../modules-sdk"
jest.setTimeout(30000)
const DB_HOST = process.env.DB_HOST ?? "localhost" const DB_HOST = process.env.DB_HOST ?? "localhost"
const DB_USERNAME = process.env.DB_USERNAME ?? "" const DB_USERNAME = process.env.DB_USERNAME ?? ""
const DB_PASSWORD = process.env.DB_PASSWORD ?? " " const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
@@ -29,7 +31,8 @@ describe("Generate migrations", () => {
afterEach(async () => { afterEach(async () => {
await fs.cleanup() await fs.cleanup()
MetadataStorage.clear() MetadataStorage.clear()
}, 300 * 1000) mikroORMEntityBuilder.clear()
})
test("generate migrations for a single entity", async () => { test("generate migrations for a single entity", async () => {
const User = model.define("User", { const User = model.define("User", {
@@ -282,7 +282,7 @@ describe("joiner-config-builder", () => {
}) })
}) })
it.only("should return a full joiner configuration with custom aliases overriding defaults", () => { it("should return a full joiner configuration with custom aliases overriding defaults", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, { const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [FulfillmentSet], models: [FulfillmentSet],
alias: [ alias: [
@@ -1,4 +1,4 @@
import { DAL, FindConfig } from "@medusajs/types" import { DAL, FindConfig, InferRepositoryReturnType } from "@medusajs/types"
import { deduplicate, isObject } from "../common" import { deduplicate, isObject } from "../common"
import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter" import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter"
@@ -10,17 +10,19 @@ type FilterFlags = {
withDeleted?: boolean withDeleted?: boolean
} }
export function buildQuery<T = any, TDto = any>( export function buildQuery<const T = any>(
filters: Record<string, any> = {}, filters: Record<string, any> = {},
config: FindConfig<TDto> & { primaryKeyFields?: string | string[] } = {} config: FindConfig<InferRepositoryReturnType<T>> & {
primaryKeyFields?: string | string[]
} = {}
): Required<DAL.FindOptions<T>> { ): Required<DAL.FindOptions<T>> {
const where: DAL.FilterQuery<T> = {} const where = {} as DAL.FilterQuery<T>
const filterFlags: FilterFlags = {} const filterFlags: FilterFlags = {}
buildWhere(filters, where, filterFlags) buildWhere(filters, where, filterFlags)
delete config.primaryKeyFields delete config.primaryKeyFields
const findOptions: DAL.OptionsQuery<T, any> = { const findOptions: DAL.FindOptions<T>["options"] = {
populate: deduplicate(config.relations ?? []), populate: deduplicate(config.relations ?? []),
fields: config.select as string[], fields: config.select as string[],
limit: (Number.isSafeInteger(config.take) && config.take) || undefined, limit: (Number.isSafeInteger(config.take) && config.take) || undefined,
@@ -28,7 +30,9 @@ export function buildQuery<T = any, TDto = any>(
} }
if (config.order) { if (config.order) {
findOptions.orderBy = config.order as DAL.OptionsQuery<T>["orderBy"] findOptions.orderBy = config.order as Required<
DAL.FindOptions<T>
>["options"]["orderBy"]
} }
if (config.withDeleted || filterFlags.withDeleted) { if (config.withDeleted || filterFlags.withDeleted) {
@@ -50,7 +54,7 @@ export function buildQuery<T = any, TDto = any>(
Object.assign(findOptions, config.options) Object.assign(findOptions, config.options)
} }
return { where, options: findOptions } return { where, options: findOptions } as Required<DAL.FindOptions<T>>
} }
function buildWhere( function buildWhere(
@@ -314,6 +314,7 @@ ${serviceBObj.module}: {
const isModuleAPrimaryKeyValid = const isModuleAPrimaryKeyValid =
moduleAPrimaryKeys.includes(serviceAPrimaryKey) moduleAPrimaryKeys.includes(serviceAPrimaryKey)
if (!isModuleAPrimaryKeyValid) { if (!isModuleAPrimaryKeyValid) {
throw new Error( throw new Error(
`Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}` `Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}`
@@ -9,7 +9,6 @@ import * as path from "path"
import { dirname, join, normalize } from "path" import { dirname, join, normalize } from "path"
import { import {
camelToSnakeCase, camelToSnakeCase,
deduplicate,
getCallerFilePath, getCallerFilePath,
isObject, isObject,
lowerCaseFirst, lowerCaseFirst,
@@ -165,29 +164,40 @@ export function defineJoinerConfig(
} }
linkableKeys = mergedLinkableKeys linkableKeys = mergedLinkableKeys
if (!primaryKeys && modelDefinitions.size) { /**
* Merge custom primary keys from the joiner config with the infered primary keys
* from the models.
*
* TODO: Maybe worth looking into the real needs for primary keys.
* It can happen that we could just remove that but we need to investigate (looking at the
* lookups from the remote joiner to identify which entity a property refers to)
*/
primaryKeys ??= []
const finalPrimaryKeys = new Set(primaryKeys)
if (modelDefinitions.size) {
const linkConfig = buildLinkConfigFromModelObjects( const linkConfig = buildLinkConfigFromModelObjects(
serviceName, serviceName,
Object.fromEntries(modelDefinitions) Object.fromEntries(modelDefinitions)
) )
primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => { Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig as any) as any[]) return Object.values(
.filter((linkableConfig) => isObject(linkableConfig)) entityLinkConfig as Record<string, { primaryKey: string }>
.map((linkableConfig) => {
// @ts-ignore
return linkableConfig.primaryKey
})
})
) )
.filter((linkableConfig) => isObject(linkableConfig))
.forEach((linkableConfig) => {
finalPrimaryKeys.add(linkableConfig.primaryKey)
})
})
} }
primaryKeys = Array.from(finalPrimaryKeys.add("id"))
// TODO: In the context of DML add a validation on primary keys and linkable keys if the consumer provide them manually. follow up pr // TODO: In the context of DML add a validation on primary keys and linkable keys if the consumer provide them manually. follow up pr
return { return {
serviceName, serviceName,
primaryKeys: primaryKeys ?? ["id"], primaryKeys,
schema, schema,
linkableKeys: linkableKeys, linkableKeys: linkableKeys,
alias: [ alias: [
@@ -342,26 +352,44 @@ export function buildLinkableKeysFromMikroOrmObjects(
export function buildLinkConfigFromModelObjects< export function buildLinkConfigFromModelObjects<
const ServiceName extends string, const ServiceName extends string,
const T extends Record<string, IDmlEntity<any, any>> const T extends Record<string, IDmlEntity<any, any>>
>(serviceName: ServiceName, models: T): InfersLinksConfig<ServiceName, T> { >(
serviceName: ServiceName,
models: T,
linkableKeys: Record<string, string> = {}
): InfersLinksConfig<ServiceName, T> {
// In case some models have been provided to a custom joiner config, the linkable will be limited
// to that set of models. We dont want to expose models that should not be linkable.
const linkableModels = Object.values(linkableKeys)
const linkConfig = {} as InfersLinksConfig<ServiceName, T> const linkConfig = {} as InfersLinksConfig<ServiceName, T>
for (const model of Object.values(models) ?? []) { for (const model of Object.values(models) ?? []) {
if (!DmlEntity.isDmlEntity(model)) { const classLikeModelName = upperCaseFirst(model.name)
if (
!DmlEntity.isDmlEntity(model) ||
(linkableModels.length && !linkableModels.includes(classLikeModelName))
) {
continue continue
} }
const schema = model.schema const schema = model.schema
// @ts-ignore
/**
* When using a linkable, if a specific linkable property is not specified, the toJSON
* function will be called and return the first linkable available for this model.
*/
const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= { const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= {
toJSON: function () { toJSON: function () {
const linkables = Object.entries(this) const linkables = Object.entries(this)
.filter(([name]) => name !== "toJSON") .filter(([name]) => name !== "toJSON")
.map(([, object]) => object) .map(([, object]) => object)
const lastIndex = linkables.length - 1 return linkables[0]
return linkables[lastIndex]
}, },
}) })
/**
* Build all linkable properties for the model
*/
for (const [property, value] of Object.entries(schema)) { for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) { if (BaseRelationship.isRelationship(value)) {
continue continue
@@ -378,10 +406,46 @@ export function buildLinkConfigFromModelObjects<
primaryKey: property, primaryKey: property,
serviceName, serviceName,
field: lowerCaseFirst(model.name), field: lowerCaseFirst(model.name),
entity: upperCaseFirst(model.name), entity: classLikeModelName,
} }
} }
} }
/**
* If the joiner config specify some custom linkable keys, we merge them with the
* existing linkable keys infered from the model above.
*/
const linkableKeysPerModel = Object.entries(linkableKeys).reduce(
(acc, [key, entityName]) => {
acc[entityName] ??= []
acc[entityName].push(key)
return acc
},
{}
)
for (const linkableKey of linkableKeysPerModel[classLikeModelName] ?? []) {
const snakeCasedModelName = camelToSnakeCase(toCamelCase(model.name))
// Linkable keys by default are prepared with snake cased model name _id
// So to be able to compare only the property we have to remove the first part
const inferredReferenceProperty = linkableKey.replace(
`${snakeCasedModelName}_`,
""
)
if (modelLinkConfig[inferredReferenceProperty]) {
continue
}
modelLinkConfig[linkableKey] = {
linkable: linkableKey,
primaryKey: linkableKey,
serviceName,
field: lowerCaseFirst(model.name),
entity: upperCaseFirst(model.name),
}
}
} }
return linkConfig as InfersLinksConfig<ServiceName, T> return linkConfig as InfersLinksConfig<ServiceName, T>
@@ -2,9 +2,9 @@ import {
BaseFilterable, BaseFilterable,
Context, Context,
FilterQuery, FilterQuery,
FilterQuery as InternalFilterQuery,
FindConfig, FindConfig,
InferEntityType, InferEntityType,
FilterQuery as InternalFilterQuery,
ModulesSdkTypes, ModulesSdkTypes,
PerformedActions, PerformedActions,
UpsertWithReplaceConfig, UpsertWithReplaceConfig,
@@ -62,7 +62,7 @@ export function MedusaInternalService<
} }
static applyFreeTextSearchFilter( static applyFreeTextSearchFilter(
filters: FilterQuery, filters: FilterQuery & { q?: string },
config: FindConfig<any> config: FindConfig<any>
): void { ): void {
if (isDefined(filters?.q)) { if (isDefined(filters?.q)) {
@@ -49,15 +49,22 @@ export function Module<
DmlEntity.isDmlEntity(model) DmlEntity.isDmlEntity(model)
) )
// TODO: Custom joiner config should take precedence over the DML auto generated linkable
// Thats in the case of manually providing models in custom joiner config.
// TODO: Add support for non linkable modifier DML object to be skipped from the linkable generation
const linkableKeys = service.prototype.__joinerConfig().linkableKeys
if (dmlObjects.length) { if (dmlObjects.length) {
linkable = buildLinkConfigFromModelObjects<ServiceName, ModelObjects>( linkable = buildLinkConfigFromModelObjects<ServiceName, ModelObjects>(
serviceName, serviceName,
modelObjects modelObjects,
linkableKeys
) as Linkable ) as Linkable
} else { } else {
linkable = buildLinkConfigFromLinkableKeys( linkable = buildLinkConfigFromLinkableKeys(
serviceName, serviceName,
service.prototype.__joinerConfig().linkableKeys linkableKeys
) as Linkable ) as Linkable
} }
} }
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderClaim } from "@models" import { OrderClaim } from "@models"
import { setFindMethods } from "../utils/base-repository-find" import { setFindMethods } from "../utils/base-repository-find"
export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderClaim>( export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderClaim OrderClaim
) {} ) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderExchange } from "@models" import { OrderExchange } from "@models"
import { setFindMethods } from "../utils/base-repository-find" import { setFindMethods } from "../utils/base-repository-find"
export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderExchange>( export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderExchange OrderExchange
) {} ) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Order } from "@models" import { Order } from "@models"
import { setFindMethods } from "../utils/base-repository-find" import { setFindMethods } from "../utils/base-repository-find"
export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Order>( export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Order Order
) {} ) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Return } from "@models" import { Return } from "@models"
import { setFindMethods } from "../utils/base-repository-find" import { setFindMethods } from "../utils/base-repository-find"
export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Return>( export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Return Return
) {} ) {}
@@ -118,7 +118,7 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
klass.prototype.findAndCount = async function findAndCount( klass.prototype.findAndCount = async function findAndCount(
this: any, this: any,
findOptions: DAL.FindOptions<T> = { where: {} }, findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context: Context = {} context: Context = {}
): Promise<[T[], number]> { ): Promise<[T[], number]> {
const manager = this.getActiveManager(context) const manager = this.getActiveManager(context)
@@ -1,3 +1,5 @@
import { kebabCase } from "@medusajs/framework/utils"
export const productCategoriesData = [ export const productCategoriesData = [
{ {
id: "category-0", id: "category-0",
@@ -25,7 +27,12 @@ export const productCategoriesData = [
name: "category 1 b 1", name: "category 1 b 1",
parent_category_id: "category-1-b", parent_category_id: "category-1-b",
}, },
] ].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const productCategoriesRankData = [ export const productCategoriesRankData = [
{ {
@@ -64,7 +71,12 @@ export const productCategoriesRankData = [
parent_category_id: "category-0-0", parent_category_id: "category-0-0",
rank: 2, rank: 2,
}, },
] ].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const eletronicsCategoriesData = [ export const eletronicsCategoriesData = [
{ {
@@ -188,4 +200,9 @@ export const eletronicsCategoriesData = [
name: "Mini Models", name: "Mini Models",
parent_category_id: "iphones", parent_category_id: "iphones",
}, },
] ].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
@@ -2,16 +2,19 @@ export const categoriesData = [
{ {
id: "category-0", id: "category-0",
name: "category 0", name: "category 0",
handle: "category-0",
parent_category_id: null, parent_category_id: null,
}, },
{ {
id: "category-1", id: "category-1",
name: "category 1", name: "category 1",
handle: "category-1",
parent_category_id: "category-0", parent_category_id: "category-0",
}, },
{ {
id: "category-1-a", id: "category-1-a",
name: "category 1 a", name: "category 1 a",
handle: "category-1-a",
parent_category_id: "category-1", parent_category_id: "category-1",
}, },
] ]
@@ -1,10 +1,11 @@
import { ProductTypes } from "@medusajs/framework/types" import { ProductTypes } from "@medusajs/framework/types"
import { ProductStatus } from "@medusajs/framework/utils" import { ProductStatus, toHandle } from "@medusajs/framework/utils"
import { Image } from "@models" import { ProductImage } from "@models"
import faker from "faker" import faker from "faker"
export const buildProductOnlyData = ({ export const buildProductOnlyData = ({
title, title,
handle,
description, description,
subtitle, subtitle,
is_giftcard, is_giftcard,
@@ -14,6 +15,7 @@ export const buildProductOnlyData = ({
status, status,
}: { }: {
title?: string title?: string
handle?: string
description?: string description?: string
subtitle?: string subtitle?: string
is_giftcard?: boolean is_giftcard?: boolean
@@ -22,15 +24,17 @@ export const buildProductOnlyData = ({
images?: { id?: string; url: string }[] images?: { id?: string; url: string }[]
status?: ProductStatus status?: ProductStatus
} = {}) => { } = {}) => {
title ??= faker.commerce.productName()
return { return {
title: title ?? faker.commerce.productName(), title: title as string,
handle: handle ?? toHandle(title!),
description: description ?? faker.commerce.productName(), description: description ?? faker.commerce.productName(),
subtitle: subtitle ?? faker.commerce.productName(), subtitle: subtitle ?? faker.commerce.productName(),
is_giftcard: is_giftcard ?? false, is_giftcard: is_giftcard ?? false,
discountable: discountable ?? true, discountable: discountable ?? true,
thumbnail: thumbnail as string, thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED, status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[], images: (images ?? []) as ProductImage[],
} }
} }
@@ -48,7 +52,7 @@ export const buildProductAndRelationsData = ({
options, options,
variants, variants,
collection_id, collection_id,
}: Partial<ProductTypes.CreateProductDTO>) => { }: Partial<ProductTypes.CreateProductDTO> & { tags: { value: string }[] }) => {
const defaultOptionTitle = "test-option" const defaultOptionTitle = "test-option"
const defaultOptionValue = "test-value" const defaultOptionValue = "test-value"
@@ -60,7 +64,7 @@ export const buildProductAndRelationsData = ({
discountable: discountable ?? true, discountable: discountable ?? true,
thumbnail: thumbnail as string, thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED, status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[], images: (images ?? []) as ProductImage[],
type_id, type_id,
tags: tags ?? [{ value: "tag-1" }], tags: tags ?? [{ value: "tag-1" }],
collection_id, collection_id,
@@ -4,6 +4,7 @@ export const productsData = [
{ {
id: "test-1", id: "test-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
tags: [ tags: [
{ {
@@ -15,6 +16,7 @@ export const productsData = [
{ {
id: "test-2", id: "test-2",
title: "product", title: "product",
handle: "product",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
tags: [ tags: [
{ {
@@ -26,6 +28,7 @@ export const productsData = [
{ {
id: "test-3", id: "test-3",
title: "product 3", title: "product 3",
handle: "product-3",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
tags: [ tags: [
{ {
@@ -1,4 +1,8 @@
import { ProductStatus } from "@medusajs/framework/utils" import {
kebabCase,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { import {
Image, Image,
@@ -10,6 +14,7 @@ import {
} from "@models" } from "@models"
import ProductOption from "../../../src/models/product-option" import ProductOption from "../../../src/models/product-option"
import { InferEntityType } from "@medusajs/types"
export * from "./data/create-product" export * from "./data/create-product"
@@ -24,7 +29,7 @@ export async function createProductAndTags(
}[] }[]
) { ) {
const products: any[] = data.map((productData) => { const products: any[] = data.map((productData) => {
return manager.create(Product, productData) return manager.create(toMikroORMEntity(Product), productData)
}) })
await manager.persistAndFlush(products) await manager.persistAndFlush(products)
@@ -42,7 +47,7 @@ export async function createProductAndTypes(
}[] }[]
) { ) {
const products: any[] = data.map((productData) => { const products: any[] = data.map((productData) => {
return manager.create(Product, productData) return manager.create(toMikroORMEntity(Product), productData)
}) })
await manager.persistAndFlush(products) await manager.persistAndFlush(products)
@@ -55,7 +60,7 @@ export async function createProductVariants(
data: any[] data: any[]
) { ) {
const variants: any[] = data.map((variantsData) => { const variants: any[] = data.map((variantsData) => {
return manager.create(ProductVariant, variantsData) return manager.create(toMikroORMEntity(ProductVariant), variantsData)
}) })
await manager.persistAndFlush(variants) await manager.persistAndFlush(variants)
@@ -72,7 +77,10 @@ export async function createCollections(
}[] }[]
) { ) {
const collections: any[] = collectionData.map((collectionData) => { const collections: any[] = collectionData.map((collectionData) => {
return manager.create(ProductCollection, collectionData) if (!collectionData.handle && collectionData.title) {
collectionData.handle = kebabCase(collectionData.title)
}
return manager.create(toMikroORMEntity(ProductCollection), collectionData)
}) })
await manager.persistAndFlush(collections) await manager.persistAndFlush(collections)
@@ -88,7 +96,7 @@ export async function createTypes(
}[] }[]
) { ) {
const types: any[] = typesData.map((typesData) => { const types: any[] = typesData.map((typesData) => {
return manager.create(ProductType, typesData) return manager.create(toMikroORMEntity(ProductType), typesData)
}) })
await manager.persistAndFlush(types) await manager.persistAndFlush(types)
@@ -112,7 +120,7 @@ export async function createOptions(
}[] }[]
) { ) {
const options: any[] = optionsData.map((option) => { const options: any[] = optionsData.map((option) => {
return manager.create(ProductOption, option) return manager.create(toMikroORMEntity(ProductOption), option)
}) })
await manager.persistAndFlush(options) await manager.persistAndFlush(options)
@@ -125,7 +133,7 @@ export async function createImages(
imagesData: string[] imagesData: string[]
) { ) {
const images: any[] = imagesData.map((img) => { const images: any[] = imagesData.map((img) => {
return manager.create(Image, { url: img }) return manager.create(toMikroORMEntity(Image), { url: img })
}) })
await manager.persistAndFlush(images) await manager.persistAndFlush(images)
@@ -135,8 +143,8 @@ export async function createImages(
export async function assignCategoriesToProduct( export async function assignCategoriesToProduct(
manager: SqlEntityManager, manager: SqlEntityManager,
product: Product, product: InferEntityType<Product>,
categories: ProductCategory[] categories: InferEntityType<ProductCategory>[]
) { ) {
product.categories.add(categories) product.categories.add(categories)
@@ -175,7 +175,7 @@ moduleIntegrationTestRunner<Service>({
include_descendants_tree: true, include_descendants_tree: true,
}, },
{ {
select: ["id", "handle"], select: ["id", "handle", "rank"],
} }
) )
@@ -190,12 +190,14 @@ moduleIntegrationTestRunner<Service>({
mpath: "category-0.category-1.category-1-a", mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1", parent_category_id: "category-1",
category_children: [], category_children: [],
rank: 0,
}, },
{ {
id: "category-1-b", id: "category-1-b",
handle: "category-1-b", handle: "category-1-b",
mpath: "category-0.category-1.category-1-b", mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1", parent_category_id: "category-1",
rank: 1,
category_children: [ category_children: [
expect.objectContaining({ expect.objectContaining({
id: "category-1-b-1", id: "category-1-b-1",
@@ -220,7 +222,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true, include_ancestors_tree: true,
}, },
{ {
select: ["id", "handle"], select: ["id", "handle", "rank"],
} }
) )
@@ -235,8 +237,10 @@ moduleIntegrationTestRunner<Service>({
mpath: mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming", "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance", parent_category_id: "high-performance",
rank: 1,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "high-performance", id: "high-performance",
rank: 1,
parent_category_id: "gaming-laptops", parent_category_id: "gaming-laptops",
handle: "high-performance-gaming-laptops", handle: "high-performance-gaming-laptops",
mpath: mpath:
@@ -245,6 +249,7 @@ moduleIntegrationTestRunner<Service>({
id: "gaming-laptops", id: "gaming-laptops",
handle: "gaming-laptops", handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops", mpath: "electronics.computers.laptops.gaming-laptops",
rank: 0,
parent_category_id: "laptops", parent_category_id: "laptops",
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "laptops", id: "laptops",
@@ -282,7 +287,7 @@ moduleIntegrationTestRunner<Service>({
include_descendants_tree: true, include_descendants_tree: true,
}, },
{ {
select: ["id", "handle"], select: ["id", "handle", "rank"],
} }
) )
@@ -296,6 +301,7 @@ moduleIntegrationTestRunner<Service>({
handle: "gaming-laptops", handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops", mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops", parent_category_id: "laptops",
rank: 0,
category_children: [ category_children: [
expect.objectContaining({ expect.objectContaining({
id: "budget-gaming", id: "budget-gaming",
@@ -304,6 +310,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.budget-gaming", "electronics.computers.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops", parent_category_id: "gaming-laptops",
category_children: [], category_children: [],
rank: 0,
}), }),
expect.objectContaining({ expect.objectContaining({
id: "high-performance", id: "high-performance",
@@ -311,6 +318,7 @@ moduleIntegrationTestRunner<Service>({
mpath: mpath:
"electronics.computers.laptops.gaming-laptops.high-performance", "electronics.computers.laptops.gaming-laptops.high-performance",
parent_category_id: "gaming-laptops", parent_category_id: "gaming-laptops",
rank: 1,
category_children: expect.arrayContaining([ category_children: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: "vr-ready", id: "vr-ready",
@@ -319,6 +327,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.vr-ready", "electronics.computers.laptops.gaming-laptops.high-performance.vr-ready",
parent_category_id: "high-performance", parent_category_id: "high-performance",
category_children: [], category_children: [],
rank: 0,
}), }),
expect.objectContaining({ expect.objectContaining({
id: "4k-gaming", id: "4k-gaming",
@@ -327,6 +336,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming", "electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance", parent_category_id: "high-performance",
category_children: [], category_children: [],
rank: 1,
}), }),
]), ]),
}), }),
@@ -407,7 +417,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true, include_ancestors_tree: true,
}, },
{ {
select: ["id", "handle"], select: ["id", "handle", "rank"],
} }
) )
@@ -421,12 +431,15 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a", handle: "category-1-a",
mpath: "category-0.category-1.category-1-a", mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1", parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-1", id: "category-1",
handle: "category-1", handle: "category-1",
mpath: "category-0.category-1", mpath: "category-0.category-1",
parent_category_id: "category-0", parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
rank: 0,
id: "category-0", id: "category-0",
handle: "category-0", handle: "category-0",
mpath: "category-0", mpath: "category-0",
@@ -440,17 +453,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b", handle: "category-1-b",
mpath: "category-0.category-1.category-1-b", mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1", parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-1", id: "category-1",
handle: "category-1", handle: "category-1",
mpath: "category-0.category-1", mpath: "category-0.category-1",
parent_category_id: "category-0", parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-0", id: "category-0",
handle: "category-0", handle: "category-0",
mpath: "category-0", mpath: "category-0",
parent_category_id: null, parent_category_id: null,
parent_category: null, parent_category: null,
rank: 0,
}), }),
}), }),
}, },
@@ -465,7 +481,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true, include_ancestors_tree: true,
}, },
{ {
select: ["id", "handle"], select: ["id", "handle", "rank"],
} }
) )
@@ -475,17 +491,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a", handle: "category-1-a",
mpath: "category-0.category-1.category-1-a", mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1", parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-1", id: "category-1",
handle: "category-1", handle: "category-1",
mpath: "category-0.category-1", mpath: "category-0.category-1",
parent_category_id: "category-0", parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-0", id: "category-0",
handle: "category-0", handle: "category-0",
mpath: "category-0", mpath: "category-0",
parent_category_id: null, parent_category_id: null,
parent_category: null, parent_category: null,
rank: 0,
}), }),
}), }),
category_children: [], category_children: [],
@@ -495,17 +514,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b", handle: "category-1-b",
mpath: "category-0.category-1.category-1-b", mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1", parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-1", id: "category-1",
handle: "category-1", handle: "category-1",
mpath: "category-0.category-1", mpath: "category-0.category-1",
parent_category_id: "category-0", parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({ parent_category: expect.objectContaining({
id: "category-0", id: "category-0",
handle: "category-0", handle: "category-0",
mpath: "category-0", mpath: "category-0",
parent_category_id: null, parent_category_id: null,
parent_category: null, parent_category: null,
rank: 0,
}), }),
}), }),
category_children: [ category_children: [
@@ -514,6 +536,7 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b-1", handle: "category-1-b-1",
mpath: "category-0.category-1.category-1-b.category-1-b-1", mpath: "category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b", parent_category_id: "category-1-b",
rank: 0,
}), }),
], ],
}, },
@@ -879,6 +902,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([ await service.create([
{ {
name: "New Category", name: "New Category",
handle: "new-category",
parent_category_id: null, parent_category_id: null,
}, },
]) ])
@@ -904,11 +928,13 @@ moduleIntegrationTestRunner<Service>({
await service.create([ await service.create([
{ {
name: "New Category", name: "New Category",
handle: "new-category",
parent_category_id: null, parent_category_id: null,
rank: 0, rank: 0,
}, },
{ {
name: "New Category 2", name: "New Category 2",
handle: "new-category-2",
parent_category_id: null, parent_category_id: null,
}, },
]) ])
@@ -932,6 +958,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([ await service.create([
{ {
name: "New Category 2.1", name: "New Category 2.1",
handle: "new-category-2-1",
parent_category_id: productCategoryNew.id, parent_category_id: productCategoryNew.id,
}, },
]) ])
@@ -5,6 +5,7 @@ import {
Modules, Modules,
ProductEvents, ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Product, ProductCategory } from "@models" import { Product, ProductCategory } from "@models"
import { import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => { beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager() const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
productTwo = testManager.create(Product, { productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2", id: "product-2",
title: "product 2", title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
@@ -5,6 +5,7 @@ import {
Modules, Modules,
ProductEvents, ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Product, ProductCollection } from "@models" import { Product, ProductCollection } from "@models"
import { import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => { beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager() const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
productTwo = testManager.create(Product, { productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2", id: "product-2",
title: "product 2", title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
@@ -47,11 +50,13 @@ moduleIntegrationTestRunner<IProductModuleService>({
{ {
id: "test-1", id: "test-1",
title: "collection 1", title: "collection 1",
handle: "collection-1",
products: [productOne], products: [productOne],
}, },
{ {
id: "test-2", id: "test-2",
title: "collection", title: "collection",
handle: "collection",
products: [productTwo], products: [productTwo],
}, },
] ]
@@ -1,5 +1,9 @@
import { IProductModuleService } from "@medusajs/framework/types" import { IProductModuleService } from "@medusajs/framework/types"
import { Modules, ProductStatus } from "@medusajs/framework/utils" import {
Modules,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductOption } from "@models" import { Product, ProductOption } from "@models"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
@@ -16,25 +20,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => { beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager() const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
productTwo = testManager.create(Product, { productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2", id: "product-2",
title: "product 2", title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
optionOne = testManager.create(ProductOption, { optionOne = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-1", id: "option-1",
title: "option 1", title: "option 1",
product: productOne, product: productOne,
}) })
optionTwo = testManager.create(ProductOption, { optionTwo = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-2", id: "option-2",
title: "option 1", title: "option 1",
product: productTwo, product: productTwo,
@@ -198,7 +204,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
it("should return requested attributes when requested through config", async () => { it("should return requested attributes when requested through config", async () => {
const option = await service.retrieveProductOption(optionOne.id, { const option = await service.retrieveProductOption(optionOne.id, {
select: ["id", "product.title"], select: ["id", "product.handle", "product.title"],
relations: ["product"], relations: ["product"],
}) })
@@ -5,6 +5,7 @@ import {
Modules, Modules,
ProductEvents, ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Product, ProductTag } from "@models" import { Product, ProductTag } from "@models"
import { import {
@@ -35,25 +36,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => { beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager() const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
productTwo = testManager.create(Product, { productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2", id: "product-2",
title: "product 2", title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
tagOne = testManager.create(ProductTag, { tagOne = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-1", id: "tag-1",
value: "tag 1", value: "tag 1",
products: [productOne], products: [productOne],
}) })
tagTwo = testManager.create(ProductTag, { tagTwo = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-2", id: "tag-2",
value: "tag", value: "tag",
products: [productTwo], products: [productTwo],
@@ -1,7 +1,7 @@
import { IProductModuleService } from "@medusajs/framework/types" import { IProductModuleService } from "@medusajs/framework/types"
import { ProductType } from "@models" import { ProductType } from "@models"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules } from "@medusajs/framework/utils" import { Modules, toMikroORMEntity } from "@medusajs/framework/utils"
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -15,12 +15,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => { beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager() const testManager = await MikroOrmWrapper.forkManager()
typeOne = testManager.create(ProductType, { typeOne = testManager.create(toMikroORMEntity(ProductType), {
id: "type-1", id: "type-1",
value: "type 1", value: "type 1",
}) })
typeTwo = testManager.create(ProductType, { typeTwo = testManager.create(toMikroORMEntity(ProductType), {
id: "type-2", id: "type-2",
value: "type", value: "type",
}) })
@@ -125,7 +125,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
productCategoryTwo = categories[1] productCategoryTwo = categories[1]
productOne = service.createProducts({ productOne = service.createProducts({
id: "product-1",
title: "product 1", title: "product 1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
options: [ options: [
@@ -136,7 +135,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
], ],
variants: [ variants: [
{ {
id: "variant-1",
title: "variant 1", title: "variant 1",
options: { "opt-title": "val-1" }, options: { "opt-title": "val-1" },
}, },
@@ -144,12 +142,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
}) })
productTwo = service.createProducts({ productTwo = service.createProducts({
id: "product-2",
title: "product 2", title: "product 2",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
categories: [{ id: productCategoryOne.id }],
collection_id: productCollectionOne.id, collection_id: productCollectionOne.id,
tags: [{ id: tags[0].id }], category_ids: [productCategoryOne.id],
tag_ids: [tags[0].id],
options: [ options: [
{ {
title: "size", title: "size",
@@ -162,7 +159,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
], ],
variants: [ variants: [
{ {
id: "variant-2",
title: "variant 2", title: "variant 2",
options: { options: {
size: "large", size: "large",
@@ -170,7 +166,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
}, },
}, },
{ {
id: "variant-3",
title: "variant 3", title: "variant 3",
options: { options: {
size: "small", size: "small",
@@ -389,11 +384,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
const existingVariant1 = product.variants.find( const existingVariant1 = product.variants.find(
(v) => v.title === "new variant 1" (v) => v.title === "new variant 1"
) )!
const existingVariant2 = product.variants.find( const existingVariant2 = product.variants.find(
(v) => v.title === "new variant 2" (v) => v.title === "new variant 2"
) )!
await service.upsertProductVariants([ await service.upsertProductVariants([
{ {
@@ -793,10 +788,14 @@ moduleIntegrationTestRunner<IProductModuleService>({
}) })
it("should do a partial update on the options of a variant successfully", async () => { it("should do a partial update on the options of a variant successfully", async () => {
const variantToUpdate = productTwo.variants.find(
(variant) => variant.title === "variant 3"
)!
await service.updateProducts(productTwo.id, { await service.updateProducts(productTwo.id, {
variants: [ variants: [
{ {
id: "variant-3", id: variantToUpdate.id,
options: { size: "small", color: "blue" }, options: { size: "small", color: "blue" },
}, },
], ],
@@ -7,12 +7,17 @@ import {
createProductVariants, createProductVariants,
} from "../__fixtures__/product" } from "../__fixtures__/product"
import { IProductModuleService, ProductDTO } from "@medusajs/framework/types" import {
InferEntityType,
IProductModuleService,
ProductDTO,
} from "@medusajs/framework/types"
import { import {
Module, Module,
Modules, Modules,
ProductStatus, ProductStatus,
kebabCase, kebabCase,
toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils" import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
@@ -27,7 +32,7 @@ import {
variantsData, variantsData,
} from "../__fixtures__/product/data" } from "../__fixtures__/product/data"
jest.setTimeout(30000) jest.setTimeout(300000)
type Service = IProductModuleService & { type Service = IProductModuleService & {
productService_: ProductService productService_: ProductService
@@ -50,17 +55,19 @@ moduleIntegrationTestRunner<Service>({
service: ProductModuleService, service: ProductModuleService,
}).linkable }).linkable
expect(Object.keys(linkable)).toEqual([ expect(Object.keys(linkable)).toHaveLength(8)
expect(Object.keys(linkable)).toEqual(
expect.arrayContaining([
"product", "product",
"productVariant", "productVariant",
"productOption", "productOption",
"productOptionValue", "productOptionValue",
"productType", "productType",
"productImage",
"productTag", "productTag",
"productCollection", "productCollection",
"productCategory", "productCategory",
]) ])
)
Object.keys(linkable).forEach((key) => { Object.keys(linkable).forEach((key) => {
delete linkable[key].toJSON delete linkable[key].toJSON
@@ -119,15 +126,6 @@ moduleIntegrationTestRunner<Service>({
field: "productType", field: "productType",
}, },
}, },
productImage: {
id: {
linkable: "product_image_id",
entity: "ProductImage",
primaryKey: "id",
serviceName: "product",
field: "productImage",
},
},
productTag: { productTag: {
id: { id: {
linkable: "product_tag_id", linkable: "product_tag_id",
@@ -160,16 +158,17 @@ moduleIntegrationTestRunner<Service>({
describe("Product Service", () => { describe("Product Service", () => {
let testManager: SqlEntityManager let testManager: SqlEntityManager
let products!: Product[] let products!: InferEntityType<typeof Product>[]
let productOne: Product let productOne: InferEntityType<typeof Product>
let categories!: ProductCategory[] let categories!: InferEntityType<typeof ProductCategory>[]
describe("retrieve", () => { describe("retrieve", () => {
beforeEach(async () => { beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager() testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
@@ -243,9 +242,10 @@ moduleIntegrationTestRunner<Service>({
beforeEach(async () => { beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager() testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, { productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1", id: "product-1",
title: "product 1", title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED, status: ProductStatus.PUBLISHED,
}) })
@@ -435,23 +435,23 @@ moduleIntegrationTestRunner<Service>({
}) })
describe("relation: categories", () => { describe("relation: categories", () => {
let workingProduct: Product let workingProduct: InferEntityType<typeof Product>
let workingCategory: ProductCategory let workingCategory: InferEntityType<typeof ProductCategory>
beforeEach(async () => { beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager() testManager = await MikroOrmWrapper.forkManager()
products = await createProductAndTags(testManager, productsData) products = await createProductAndTags(testManager, productsData)
workingProduct = products.find((p) => p.id === "test-1") as Product workingProduct = products.find((p) => p.id === "test-1")!
categories = [] categories = []
for (const entry of categoriesData) { for (const entry of categoriesData) {
categories.push((await categoryService.create([entry]))[0]) categories.push((await categoryService.create([entry]))[0])
} }
workingCategory = (await testManager.findOne( workingCategory = (await testManager.findOne(
ProductCategory, toMikroORMEntity(ProductCategory),
"category-1" "category-1"
)) as ProductCategory ))!
workingProduct = await assignCategoriesToProduct( workingProduct = await assignCategoriesToProduct(
testManager, testManager,
@@ -477,9 +477,7 @@ moduleIntegrationTestRunner<Service>({
} }
) )
const product = products.find( const product = products.find((p) => p.id === workingProduct.id)!
(p) => p.id === workingProduct.id
) as unknown as Product
expect(product).toEqual( expect(product).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -534,32 +532,34 @@ moduleIntegrationTestRunner<Service>({
}) })
describe("relation: collections", () => { describe("relation: collections", () => {
let workingProduct: Product let workingProduct: InferEntityType<typeof Product>
let workingProductTwo: Product let workingProductTwo: InferEntityType<typeof Product>
let workingCollection: ProductCollection let workingCollection: InferEntityType<typeof ProductCollection>
let workingCollectionTwo: ProductCollection let workingCollectionTwo: InferEntityType<typeof ProductCollection>
const collectionData = [ const collectionData = [
{ {
id: "test-1", id: "test-1",
title: "col 1", title: "col 1",
handle: "col-1",
}, },
{ {
id: "test-2", id: "test-2",
title: "col 2", title: "col 2",
handle: "col-2",
}, },
] ]
beforeEach(async () => { beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager() testManager = await MikroOrmWrapper.forkManager()
await createCollections(testManager, collectionData) await createCollections(testManager, collectionData)
workingCollection = (await testManager.findOne( workingCollection = await testManager.findOne(
ProductCollection, toMikroORMEntity(ProductCollection),
"test-1" "test-1"
)) as ProductCollection )
workingCollectionTwo = (await testManager.findOne( workingCollectionTwo = (await testManager.findOne(
ProductCollection, toMikroORMEntity(ProductCollection),
"test-2" "test-2"
)) as ProductCollection ))!
products = await createProductAndTags(testManager, [ products = await createProductAndTags(testManager, [
{ {
@@ -575,10 +575,8 @@ moduleIntegrationTestRunner<Service>({
}, },
]) ])
workingProduct = products.find((p) => p.id === "test-1") as Product workingProduct = products.find((p) => p.id === "test-1")!
workingProductTwo = products.find( workingProductTwo = products.find((p) => p.id === "test-2")!
(p) => p.id === "test-2"
) as Product
}) })
it("should filter by collection relation and scope fields", async () => { it("should filter by collection relation and scope fields", async () => {
@@ -588,7 +586,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: workingCollection.id, collection_id: workingCollection.id,
}, },
{ {
select: ["title", "collection.title"], select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"], relations: ["collection"],
} }
) )
@@ -618,7 +621,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: [workingCollection.id, workingCollectionTwo.id], collection_id: [workingCollection.id, workingCollectionTwo.id],
}, },
{ {
select: ["title", "collection.title"], select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"], relations: ["collection"],
} }
) )
@@ -9,7 +9,6 @@ import {
ProductType, ProductType,
ProductVariant, ProductVariant,
} from "@models" } from "@models"
import ProductImage from "./models/product-image"
import { default as schema } from "./schema" import { default as schema } from "./schema"
export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, { export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
@@ -20,7 +19,6 @@ export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
ProductOption, ProductOption,
ProductOptionValue, ProductOptionValue,
ProductType, ProductType,
ProductImage,
ProductTag, ProductTag,
ProductCollection, ProductCollection,
ProductCategory, ProductCategory,
@@ -82,6 +82,15 @@
"default": "0", "default": "0",
"mappedType": "integer" "mappedType": "integer"
}, },
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"parent_category_id": { "parent_category_id": {
"name": "parent_category_id", "name": "parent_category_id",
"type": "text", "type": "text",
@@ -122,28 +131,42 @@
"nullable": true, "nullable": true,
"length": 6, "length": 6,
"mappedType": "datetime" "mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
} }
}, },
"name": "product_category", "name": "product_category",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_product_category_parent_category_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_category_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_parent_category_id\" ON \"product_category\" (parent_category_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_category_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_deleted_at\" ON \"product_category\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_category_path",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_path\" ON \"product_category\" (mpath) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_category_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_category_handle_unique\" ON \"product_category\" (handle) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_category_pkey", "keyName": "product_category_pkey",
@@ -247,13 +270,20 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_collection_deleted_at", "keyName": "IDX_product_collection_deleted_at",
"columnNames": [],
"composite": false,
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_deleted_at\" ON \"product_collection\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_collection_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_collection_handle_unique\" ON \"product_collection\" (handle) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_collection_pkey", "keyName": "product_collection_pkey",
@@ -334,13 +364,20 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_tag_deleted_at", "keyName": "IDX_product_tag_deleted_at",
"columnNames": [],
"composite": false,
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_tag_deleted_at\" ON \"product_tag\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_tag_value_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tag_value_unique\" ON \"product_tag\" (value) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_tag_pkey", "keyName": "product_tag_pkey",
@@ -377,7 +414,7 @@
}, },
"metadata": { "metadata": {
"name": "metadata", "name": "metadata",
"type": "json", "type": "jsonb",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
@@ -421,13 +458,20 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_type_deleted_at", "keyName": "IDX_product_type_deleted_at",
"columnNames": [],
"composite": false,
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_deleted_at\" ON \"product_type\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_type_value_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_type_value_unique\" ON \"product_type\" (value) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_type_pkey", "keyName": "product_type_pkey",
@@ -506,6 +550,7 @@
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"default": "'draft'",
"enumItems": [ "enumItems": [
"draft", "draft",
"proposed", "proposed",
@@ -595,24 +640,6 @@
"nullable": true, "nullable": true,
"mappedType": "text" "mappedType": "text"
}, },
"collection_id": {
"name": "collection_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"type_id": {
"name": "type_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"discountable": { "discountable": {
"name": "discountable", "name": "discountable",
"type": "boolean", "type": "boolean",
@@ -632,6 +659,33 @@
"nullable": true, "nullable": true,
"mappedType": "text" "mappedType": "text"
}, },
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"type_id": {
"name": "type_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"collection_id": {
"name": "collection_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamptz", "type": "timestamptz",
@@ -663,28 +717,42 @@
"nullable": true, "nullable": true,
"length": 6, "length": 6,
"mappedType": "datetime" "mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
} }
}, },
"name": "product", "name": "product",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_product_type_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_id\" ON \"product\" (type_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_collection_id",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_id\" ON \"product\" (collection_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_deleted_at\" ON \"product\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_handle_unique\" ON \"product\" (handle) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_pkey", "keyName": "product_pkey",
@@ -698,19 +766,6 @@
], ],
"checks": [], "checks": [],
"foreignKeys": { "foreignKeys": {
"product_collection_id_foreign": {
"constraintName": "product_collection_id_foreign",
"columnNames": [
"collection_id"
],
"localTableName": "public.product",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_collection",
"deleteRule": "set null",
"updateRule": "cascade"
},
"product_type_id_foreign": { "product_type_id_foreign": {
"constraintName": "product_type_id_foreign", "constraintName": "product_type_id_foreign",
"columnNames": [ "columnNames": [
@@ -723,6 +778,19 @@
"referencedTableName": "public.product_type", "referencedTableName": "public.product_type",
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
},
"product_collection_id_foreign": {
"constraintName": "product_collection_id_foreign",
"columnNames": [
"collection_id"
],
"localTableName": "public.product",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_collection",
"deleteRule": "set null",
"updateRule": "cascade"
} }
} }
}, },
@@ -746,15 +814,6 @@
"nullable": false, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": { "metadata": {
"name": "metadata", "name": "metadata",
"type": "jsonb", "type": "jsonb",
@@ -764,6 +823,15 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamptz", "type": "timestamptz",
@@ -801,13 +869,28 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_product_option_product_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_product_id\" ON \"product_option\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_option_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_deleted_at\" ON \"product_option\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_option_product_id_title_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_product_id_title_unique\" ON \"product_option\" (product_id, title) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_option_pkey", "keyName": "product_option_pkey",
@@ -856,15 +939,6 @@
"nullable": false, "nullable": false,
"mappedType": "text" "mappedType": "text"
}, },
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": { "metadata": {
"name": "metadata", "name": "metadata",
"type": "jsonb", "type": "jsonb",
@@ -874,6 +948,15 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamptz", "type": "timestamptz",
@@ -911,13 +994,28 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_product_option_value_option_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_value_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_option_id\" ON \"product_option_value\" (option_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_option_value_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_deleted_at\" ON \"product_option_value\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_option_value_option_id_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_value_option_id_unique\" ON \"product_option_value\" (option_id, value) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_option_value_pkey", "keyName": "product_option_value_pkey",
@@ -975,6 +1073,25 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"rank": {
"name": "rank",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamptz", "type": "timestamptz",
@@ -1006,38 +1123,34 @@
"nullable": true, "nullable": true,
"length": 6, "length": 6,
"mappedType": "datetime" "mappedType": "datetime"
},
"rank": {
"name": "rank",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
} }
}, },
"name": "image", "name": "image",
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_image_product_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_image_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_product_id\" ON \"image\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_image_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_deleted_at\" ON \"image\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_image_url",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_image_url\" ON \"image\" (url) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "image_pkey", "keyName": "image_pkey",
@@ -1310,39 +1423,39 @@
}, },
"weight": { "weight": {
"name": "weight", "name": "weight",
"type": "numeric", "type": "integer",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "decimal" "mappedType": "integer"
}, },
"length": { "length": {
"name": "length", "name": "length",
"type": "numeric", "type": "integer",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "decimal" "mappedType": "integer"
}, },
"height": { "height": {
"name": "height", "name": "height",
"type": "numeric", "type": "integer",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "decimal" "mappedType": "integer"
}, },
"width": { "width": {
"name": "width", "name": "width",
"type": "numeric", "type": "integer",
"unsigned": false, "unsigned": false,
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "decimal" "mappedType": "integer"
}, },
"metadata": { "metadata": {
"name": "metadata", "name": "metadata",
@@ -1409,13 +1522,52 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": [ "keyName": "IDX_product_variant_product_id",
"deleted_at" "columnNames": [],
],
"composite": false, "composite": false,
"keyName": "IDX_product_variant_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_product_id\" ON \"product_variant\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_deleted_at\" ON \"product_variant\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_sku_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_sku_unique\" ON \"product_variant\" (sku) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_barcode_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_barcode_unique\" ON \"product_variant\" (barcode) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_ean_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_ean_unique\" ON \"product_variant\" (ean) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_upc_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_upc_unique\" ON \"product_variant\" (upc) WHERE deleted_at IS NULL"
}, },
{ {
"keyName": "product_variant_pkey", "keyName": "product_variant_pkey",
@@ -0,0 +1,173 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20241125090957 extends Migration {
async up(): Promise<void> {
this.addSql(
'alter table if exists "product" drop constraint if exists "product_status_check";'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_category_parent_category_id" ON "product_category" (parent_category_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_category_path" ON "product_category" (mpath) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_category_handle_unique" ON "product_category" (handle) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_collection_handle_unique" ON "product_collection" (handle) WHERE deleted_at IS NULL;'
)
this.addSql('drop index if exists "IDX_product_image_deleted_at";')
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_image_deleted_at" ON "image" (deleted_at) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_image_url" ON "image" (url) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tag_value_unique" ON "product_tag" (value) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_type_value_unique" ON "product_type" (value) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product" alter column "status" type text using ("status"::text);'
)
this.addSql(
"alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));"
)
this.addSql(
'alter table if exists "product" alter column "status" set default \'draft\';'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_type_id" ON "product" (type_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_collection_id" ON "product" (collection_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_handle_unique" ON "product" (handle) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" set not null;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_option_product_id" ON "product_option" (product_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_option_product_id_title_unique" ON "product_option" (product_id, title) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_option_value_option_id" ON "product_option_value" (option_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_option_value_option_id_unique" ON "product_option_value" (option_id, value) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product_variant" alter column "weight" type integer using ("weight"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "length" type integer using ("length"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "height" type integer using ("height"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "width" type integer using ("width"::integer);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_variant_product_id" ON "product_variant" (product_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_sku_unique" ON "product_variant" (sku) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_barcode_unique" ON "product_variant" (barcode) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_ean_unique" ON "product_variant" (ean) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_upc_unique" ON "product_variant" (upc) WHERE deleted_at IS NULL;'
)
}
async down(): Promise<void> {
this.addSql(
'alter table if exists "product" drop constraint if exists "product_status_check";'
)
this.addSql(
'drop index if exists "IDX_product_category_parent_category_id";'
)
this.addSql('drop index if exists "IDX_product_category_path";')
this.addSql('drop index if exists "IDX_category_handle_unique";')
this.addSql('drop index if exists "IDX_collection_handle_unique";')
this.addSql('drop index if exists "IDX_image_deleted_at";')
this.addSql('drop index if exists "IDX_product_image_url";')
this.addSql(
'create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");'
)
this.addSql('drop index if exists "IDX_tag_value_unique";')
this.addSql('drop index if exists "IDX_type_value_unique";')
this.addSql(
'alter table if exists "product" alter column "status" drop default;'
)
this.addSql(
'alter table if exists "product" alter column "status" type text using ("status"::text);'
)
this.addSql(
"alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));"
)
this.addSql('drop index if exists "IDX_product_type_id";')
this.addSql('drop index if exists "IDX_product_collection_id";')
this.addSql('drop index if exists "IDX_product_handle_unique";')
this.addSql(
'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" drop not null;'
)
this.addSql('drop index if exists "IDX_product_option_product_id";')
this.addSql('drop index if exists "IDX_option_product_id_title_unique";')
this.addSql('drop index if exists "IDX_product_option_value_option_id";')
this.addSql('drop index if exists "IDX_option_value_option_id_unique";')
this.addSql(
'alter table if exists "product_variant" alter column "weight" type numeric using ("weight"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "length" type numeric using ("length"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "height" type numeric using ("height"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "width" type numeric using ("width"::numeric);'
)
this.addSql('drop index if exists "IDX_product_variant_product_id";')
this.addSql('drop index if exists "IDX_product_variant_sku_unique";')
this.addSql('drop index if exists "IDX_product_variant_barcode_unique";')
this.addSql('drop index if exists "IDX_product_variant_ean_unique";')
this.addSql('drop index if exists "IDX_product_variant_upc_unique";')
}
}
@@ -1,140 +1,45 @@
import { import { model } from "@medusajs/framework/utils"
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
kebabCase,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
EventArgs,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
OneToMany,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Product from "./product" import Product from "./product"
const categoryHandleIndexName = "IDX_category_handle_unique" const ProductCategory = model
const categoryHandleIndexStatement = createPsqlIndexStatementHelper({ .define("ProductCategory", {
name: categoryHandleIndexName, id: model.id({ prefix: "pcat" }).primaryKey(),
tableName: "product_category", name: model.text().searchable(),
columns: ["handle"], description: model.text().searchable().default(""),
unique: true, handle: model.text().searchable(),
where: "deleted_at IS NULL", mpath: model.text(),
}) is_active: model.boolean().default(false),
is_internal: model.boolean().default(false),
const categoryMpathIndexName = "IDX_product_category_path" rank: model.number().default(0),
const categoryMpathIndexStatement = createPsqlIndexStatementHelper({ metadata: model.json().nullable(),
name: categoryMpathIndexName, parent_category: model
tableName: "product_category", .belongsTo(() => ProductCategory, {
columns: ["mpath"], mappedBy: "category_children",
})
.nullable(),
category_children: model.hasMany(() => ProductCategory, {
mappedBy: "parent_category",
}),
products: model.manyToMany(() => Product, {
mappedBy: "categories",
}),
})
.cascades({
delete: ["category_children"],
})
.indexes([
{
name: "IDX_product_category_path",
on: ["mpath"],
unique: false, unique: false,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
{
categoryMpathIndexStatement.MikroORMIndex() name: "IDX_category_handle_unique",
categoryHandleIndexStatement.MikroORMIndex() on: ["handle"],
@Entity({ tableName: "product_category" }) unique: true,
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) where: "deleted_at IS NULL",
class ProductCategory { },
@PrimaryKey({ columnType: "text" }) ])
id!: string
@Searchable()
@Property({ columnType: "text", nullable: false })
name?: string
@Searchable()
@Property({ columnType: "text", default: "", nullable: false })
description?: string
@Searchable()
@Property({ columnType: "text", nullable: false })
handle?: string
@Property({ columnType: "text", nullable: false })
mpath?: string
@Property({ columnType: "boolean", default: false })
is_active?: boolean
@Property({ columnType: "boolean", default: false })
is_internal?: boolean
@Property({
columnType: "integer",
nullable: false,
default: 0,
})
rank: number
@ManyToOne(() => ProductCategory, {
columnType: "text",
fieldName: "parent_category_id",
nullable: true,
mapToPk: true,
onDelete: "cascade",
})
parent_category_id?: string | null
@ManyToOne(() => ProductCategory, { nullable: true, persist: false })
parent_category?: ProductCategory
@OneToMany({
entity: () => ProductCategory,
mappedBy: (productCategory) => productCategory.parent_category,
})
category_children = new Collection<ProductCategory>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at?: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at?: Date
@Index({ name: "IDX_product_category_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToMany(() => Product, (product) => product.categories)
products = new Collection<Product>(this)
@OnInit()
async onInit() {
this.id = generateEntityId(this.id, "pcat")
this.parent_category_id ??= this.parent_category?.id ?? null
}
@BeforeCreate()
async onCreate(args: EventArgs<ProductCategory>) {
this.id = generateEntityId(this.id, "pcat")
this.parent_category_id ??= this.parent_category?.id ?? null
if (!this.handle && this.name) {
this.handle = kebabCase(this.name)
}
this.mpath = `${this.mpath ? this.mpath + "." : ""}${this.id}`
}
}
export default ProductCategory export default ProductCategory
@@ -1,81 +1,23 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate,
Collection,
Entity,
Filter,
Index,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
kebabCase,
Searchable,
} from "@medusajs/framework/utils"
import Product from "./product" import Product from "./product"
const collectionHandleIndexName = "IDX_collection_handle_unique" const ProductCollection = model
const collectionHandleIndexStatement = createPsqlIndexStatementHelper({ .define("ProductCollection", {
name: collectionHandleIndexName, id: model.id({ prefix: "pcol" }).primaryKey(),
tableName: "product_collection", title: model.text().searchable(),
columns: ["handle"], handle: model.text(),
metadata: model.json().nullable(),
products: model.hasMany(() => Product, {
mappedBy: "collection",
}),
})
.indexes([
{
name: "IDX_collection_handle_unique",
on: ["handle"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
collectionHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_collection" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductCollection {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
handle?: string
@OneToMany(() => Product, (product) => product.collection)
products = new Collection<Product>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_collection_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "pcol")
if (!this.handle && this.title) {
this.handle = kebabCase(this.title)
}
}
}
export default ProductCollection export default ProductCollection
@@ -1,88 +1,26 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate,
Entity,
Filter,
Index,
ManyToOne,
OnInit,
PrimaryKey,
Property,
Rel,
} from "@mikro-orm/core"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import Product from "./product" import Product from "./product"
const imageUrlIndexName = "IDX_product_image_url" const ProductImage = model
const imageUrlIndexStatement = createPsqlIndexStatementHelper({ .define(
name: imageUrlIndexName, { tableName: "image", name: "ProductImage" },
tableName: "image", {
columns: ["url"], id: model.id({ prefix: "img" }).primaryKey(),
url: model.text(),
metadata: model.json().nullable(),
rank: model.number().default(0),
product: model.belongsTo(() => Product, {
mappedBy: "images",
}),
}
)
.indexes([
{
name: "IDX_product_image_url",
on: ["url"],
unique: false, unique: false,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
imageUrlIndexStatement.MikroORMIndex()
@Entity({ tableName: "image" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductImage {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
url: string
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_image_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "integer", default: 0 })
rank: number
@ManyToOne(() => Product, {
columnType: "text",
onDelete: "cascade",
fieldName: "product_id",
mapToPk: true,
})
product_id: string
@ManyToOne(() => Product, {
persist: false,
})
product: Rel<Product>
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "img")
}
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "img")
}
}
export default ProductImage export default ProductImage
@@ -1,87 +1,27 @@
import { import { model } from "@medusajs/framework/utils"
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { ProductOption, ProductVariant } from "./index" import { ProductOption, ProductVariant } from "./index"
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique" const ProductOptionValue = model
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({ .define("ProductOptionValue", {
name: optionValueOptionIdIndexName, id: model.id({ prefix: "optval" }).primaryKey(),
tableName: "product_option_value", value: model.text(),
columns: ["option_id", "value"], metadata: model.json().nullable(),
option: model
.belongsTo(() => ProductOption, {
mappedBy: "values",
})
.nullable(),
variants: model.manyToMany(() => ProductVariant, {
mappedBy: "options",
}),
})
.indexes([
{
name: "IDX_option_value_option_id_unique",
on: ["option_id", "value"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
optionValueOptionIdIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_option_value" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOptionValue {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
value: string
@ManyToOne(() => ProductOption, {
columnType: "text",
fieldName: "option_id",
mapToPk: true,
nullable: true,
onDelete: "cascade",
})
option_id: string | null
@ManyToOne(() => ProductOption, {
nullable: true,
persist: false,
})
option: ProductOption | null
@ManyToMany(() => ProductVariant, (variant) => variant.options)
variants = new Collection<ProductVariant>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_option_value_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "optval")
this.option_id ??= this.option?.id ?? null
}
}
export default ProductOptionValue export default ProductOptionValue
@@ -1,93 +1,29 @@
import { import { model } from "@medusajs/framework/utils"
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToOne,
OnInit,
OneToMany,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { Product } from "./index" import { Product } from "./index"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique" const ProductOption = model
const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({ .define("ProductOption", {
name: optionProductIdTitleIndexName, id: model.id({ prefix: "opt" }).primaryKey(),
tableName: "product_option", title: model.text().searchable(),
columns: ["product_id", "title"], metadata: model.json().nullable(),
product: model.belongsTo(() => Product, {
mappedBy: "options",
}),
values: model.hasMany(() => ProductOptionValue, {
mappedBy: "option",
}),
})
.cascades({
delete: ["values"],
})
.indexes([
{
name: "IDX_option_product_id_title_unique",
on: ["product_id", "title"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
optionProductIdTitleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_option" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOption {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@ManyToOne(() => Product, {
columnType: "text",
fieldName: "product_id",
mapToPk: true,
nullable: true,
onDelete: "cascade",
})
product_id: string | null
@ManyToOne(() => Product, {
persist: false,
nullable: true,
})
product: Product | null
@OneToMany(() => ProductOptionValue, (value) => value.option, {
cascade: [Cascade.PERSIST, "soft-remove" as any],
})
values = new Collection<ProductOptionValue>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_option_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "opt")
this.product_id ??= this.product?.id ?? null
}
}
export default ProductOption export default ProductOption
@@ -1,73 +1,25 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import Product from "./product" import Product from "./product"
const tagValueIndexName = "IDX_tag_value_unique" const ProductTag = model
const tagValueIndexStatement = createPsqlIndexStatementHelper({ .define(
name: tagValueIndexName, { tableName: "product_tag", name: "ProductTag" },
tableName: "product_tag", {
columns: ["value"], id: model.id({ prefix: "ptag" }).primaryKey(),
value: model.text().searchable(),
metadata: model.json().nullable(),
products: model.manyToMany(() => Product, {
mappedBy: "tags",
}),
}
)
.indexes([
{
name: "IDX_tag_value_unique",
on: ["value"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
tagValueIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_tag" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductTag {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
value: string
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_tag_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@ManyToMany(() => Product, (product) => product.tags)
products = new Collection<Product>(this)
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "ptag")
}
}
export default ProductTag export default ProductTag
@@ -1,67 +1,22 @@
import { import { model } from "@medusajs/framework/utils"
BeforeCreate, import { Product } from "@models"
Entity,
Filter,
Index,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { const ProductType = model
DALUtils, .define("ProductType", {
Searchable, id: model.id({ prefix: "ptyp" }).primaryKey(),
createPsqlIndexStatementHelper, value: model.text().searchable(),
generateEntityId, metadata: model.json().nullable(),
} from "@medusajs/framework/utils" product: model.hasMany(() => Product, {
mappedBy: "type",
const typeValueIndexName = "IDX_type_value_unique" }),
const typeValueIndexStatement = createPsqlIndexStatementHelper({ })
name: typeValueIndexName, .indexes([
tableName: "product_type", {
columns: ["value"], name: "IDX_type_value_unique",
on: ["value"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
typeValueIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_type" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductType {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
value: string
@Property({ columnType: "json", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_type_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "ptyp")
}
}
export default ProductType export default ProductType
@@ -1,187 +1,69 @@
import { import { model } from "@medusajs/framework/utils"
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { Product, ProductOptionValue } from "@models" import { Product, ProductOptionValue } from "@models"
const variantSkuIndexName = "IDX_product_variant_sku_unique" const ProductVariant = model
const variantSkuIndexStatement = createPsqlIndexStatementHelper({ .define("ProductVariant", {
name: variantSkuIndexName, id: model.id({ prefix: "variant" }).primaryKey(),
tableName: "product_variant", title: model.text().searchable(),
columns: ["sku"], sku: model.text().searchable().nullable(),
unique: true, barcode: model.text().searchable().nullable(),
where: "deleted_at IS NULL", ean: model.text().searchable().nullable(),
}) upc: model.text().searchable().nullable(),
allow_backorder: model.boolean().default(false),
const variantBarcodeIndexName = "IDX_product_variant_barcode_unique" manage_inventory: model.boolean().default(true),
const variantBarcodeIndexStatement = createPsqlIndexStatementHelper({ hs_code: model.text().nullable(),
name: variantBarcodeIndexName, origin_country: model.text().nullable(),
tableName: "product_variant", mid_code: model.text().nullable(),
columns: ["barcode"], material: model.text().nullable(),
unique: true, weight: model.number().nullable(),
where: "deleted_at IS NULL", length: model.number().nullable(),
}) height: model.number().nullable(),
width: model.number().nullable(),
const variantEanIndexName = "IDX_product_variant_ean_unique" metadata: model.json().nullable(),
const variantEanIndexStatement = createPsqlIndexStatementHelper({ variant_rank: model.number().default(0).nullable(),
name: variantEanIndexName, product: model
tableName: "product_variant", .belongsTo(() => Product, {
columns: ["ean"], mappedBy: "variants",
unique: true,
where: "deleted_at IS NULL",
})
const variantUpcIndexName = "IDX_product_variant_upc_unique"
const variantUpcIndexStatement = createPsqlIndexStatementHelper({
name: variantUpcIndexName,
tableName: "product_variant",
columns: ["upc"],
unique: true,
where: "deleted_at IS NULL",
})
const variantProductIdIndexName = "IDX_product_variant_product_id"
const variantProductIdIndexStatement = createPsqlIndexStatementHelper({
name: variantProductIdIndexName,
tableName: "product_variant",
columns: ["product_id"],
unique: false,
where: "deleted_at IS NULL",
})
variantProductIdIndexStatement.MikroORMIndex()
variantSkuIndexStatement.MikroORMIndex()
variantBarcodeIndexStatement.MikroORMIndex()
variantEanIndexStatement.MikroORMIndex()
variantUpcIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_variant" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductVariant {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Searchable()
@Property({ columnType: "text", nullable: true })
sku?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
barcode?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
ean?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
upc?: string | null
@Property({ columnType: "boolean", default: false })
allow_backorder?: boolean = false
@Property({ columnType: "boolean", default: true })
manage_inventory?: boolean = true
@Property({ columnType: "text", nullable: true })
hs_code?: string | null
@Property({ columnType: "text", nullable: true })
origin_country?: string | null
@Property({ columnType: "text", nullable: true })
mid_code?: string | null
@Property({ columnType: "text", nullable: true })
material?: string | null
@Property({ columnType: "numeric", nullable: true })
weight?: number | null
@Property({ columnType: "numeric", nullable: true })
length?: number | null
@Property({ columnType: "numeric", nullable: true })
height?: number | null
@Property({ columnType: "numeric", nullable: true })
width?: number | null
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
columnType: "integer",
nullable: true,
default: 0,
}) })
variant_rank?: number | null .nullable(),
options: model.manyToMany(() => ProductOptionValue, {
@ManyToOne(() => Product, {
columnType: "text",
nullable: true,
onDelete: "cascade",
fieldName: "product_id",
mapToPk: true,
})
product_id: string | null
@ManyToOne(() => Product, {
persist: false,
nullable: true,
})
product: Product | null
@ManyToMany(() => ProductOptionValue, "variants", {
owner: true,
pivotTable: "product_variant_option", pivotTable: "product_variant_option",
mappedBy: "variants",
joinColumn: "variant_id", joinColumn: "variant_id",
inverseJoinColumn: "option_value_id", inverseJoinColumn: "option_value_id",
}),
}) })
options = new Collection<ProductOptionValue>(this) .indexes([
{
@Property({ name: "IDX_product_variant_product_id",
onCreate: () => new Date(), on: ["product_id"],
columnType: "timestamptz", unique: false,
defaultRaw: "now()", where: "deleted_at IS NULL",
}) },
created_at: Date {
name: "IDX_product_variant_sku_unique",
@Property({ on: ["sku"],
onCreate: () => new Date(), unique: true,
onUpdate: () => new Date(), where: "deleted_at IS NULL",
columnType: "timestamptz", },
defaultRaw: "now()", {
}) name: "IDX_product_variant_barcode_unique",
updated_at: Date on: ["barcode"],
unique: true,
@Index({ name: "IDX_product_variant_deleted_at" }) where: "deleted_at IS NULL",
@Property({ columnType: "timestamptz", nullable: true }) },
deleted_at?: Date {
name: "IDX_product_variant_ean_unique",
@OnInit() on: ["ean"],
@BeforeCreate() unique: true,
onInit() { where: "deleted_at IS NULL",
this.id = generateEntityId(this.id, "variant") },
this.product_id ??= this.product?.id ?? null {
} name: "IDX_product_variant_upc_unique",
} on: ["upc"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductVariant export default ProductVariant
+73 -209
View File
@@ -1,222 +1,86 @@
import { import { model, ProductUtils } from "@medusajs/framework/utils"
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Filter,
Index,
ManyToMany,
ManyToOne,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
ProductUtils,
Searchable,
toHandle,
} from "@medusajs/framework/utils"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
import ProductImage from "./product-image"
import ProductOption from "./product-option"
import ProductTag from "./product-tag" import ProductTag from "./product-tag"
import ProductType from "./product-type" import ProductType from "./product-type"
import ProductImage from "./product-image"
import ProductOption from "./product-option"
import ProductVariant from "./product-variant" import ProductVariant from "./product-variant"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
const productHandleIndexName = "IDX_product_handle_unique" const Product = model
const productHandleIndexStatement = createPsqlIndexStatementHelper({ .define("Product", {
name: productHandleIndexName, id: model.id({ prefix: "prod" }).primaryKey(),
tableName: "product", title: model.text().searchable(),
columns: ["handle"], handle: model.text(),
subtitle: model.text().searchable().nullable(),
description: model.text().searchable().nullable(),
is_giftcard: model.boolean().default(false),
status: model
.enum(ProductUtils.ProductStatus)
.default(ProductUtils.ProductStatus.DRAFT),
thumbnail: model.text().nullable(),
weight: model.text().nullable(),
length: model.text().nullable(),
height: model.text().nullable(),
width: model.text().nullable(),
origin_country: model.text().nullable(),
hs_code: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
discountable: model.boolean().default(true),
external_id: model.text().nullable(),
metadata: model.json().nullable(),
variants: model.hasMany(() => ProductVariant, {
mappedBy: "product",
}),
type: model
.belongsTo(() => ProductType, {
mappedBy: "product",
})
.nullable(),
tags: model.manyToMany(() => ProductTag, {
mappedBy: "products",
pivotTable: "product_tags",
}),
options: model.hasMany(() => ProductOption, {
mappedBy: "product",
}),
images: model.hasMany(() => ProductImage, {
mappedBy: "product",
}),
collection: model
.belongsTo(() => ProductCollection, {
mappedBy: "products",
})
.nullable(),
categories: model.manyToMany(() => ProductCategory, {
pivotTable: "product_category_product",
mappedBy: "products",
}),
})
.cascades({
delete: ["variants", "options", "images"],
})
.indexes([
{
name: "IDX_product_handle_unique",
on: ["handle"],
unique: true, unique: true,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
{
const productTypeIndexName = "IDX_product_type_id" name: "IDX_product_type_id",
const productTypeIndexStatement = createPsqlIndexStatementHelper({ on: ["type_id"],
name: productTypeIndexName,
tableName: "product",
columns: ["type_id"],
unique: false, unique: false,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
{
const productCollectionIndexName = "IDX_product_collection_id" name: "IDX_product_collection_id",
const productCollectionIndexStatement = createPsqlIndexStatementHelper({ on: ["collection_id"],
name: productCollectionIndexName,
tableName: "product",
columns: ["collection_id"],
unique: false, unique: false,
where: "deleted_at IS NULL", where: "deleted_at IS NULL",
}) },
])
productTypeIndexStatement.MikroORMIndex()
productCollectionIndexStatement.MikroORMIndex()
productHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class Product {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
handle?: string
@Searchable()
@Property({ columnType: "text", nullable: true })
subtitle?: string | null
@Searchable()
@Property({
columnType: "text",
nullable: true,
})
description?: string | null
@Property({ columnType: "boolean", default: false })
is_giftcard!: boolean
@Enum(() => ProductUtils.ProductStatus)
@Property({ default: ProductUtils.ProductStatus.DRAFT })
status!: ProductUtils.ProductStatus
@Property({ columnType: "text", nullable: true })
thumbnail?: string | null
@OneToMany(() => ProductOption, (o) => o.product, {
cascade: ["soft-remove"] as any,
})
options = new Collection<ProductOption>(this)
@Searchable()
@OneToMany(() => ProductVariant, (variant) => variant.product, {
cascade: ["soft-remove"] as any,
})
variants = new Collection<ProductVariant>(this)
@Property({ columnType: "text", nullable: true })
weight?: number | null
@Property({ columnType: "text", nullable: true })
length?: number | null
@Property({ columnType: "text", nullable: true })
height?: number | null
@Property({ columnType: "text", nullable: true })
width?: number | null
@Property({ columnType: "text", nullable: true })
origin_country?: string | null
@Property({ columnType: "text", nullable: true })
hs_code?: string | null
@Property({ columnType: "text", nullable: true })
mid_code?: string | null
@Property({ columnType: "text", nullable: true })
material?: string | null
@Searchable()
@ManyToOne(() => ProductCollection, {
columnType: "text",
nullable: true,
fieldName: "collection_id",
mapToPk: true,
onDelete: "set null",
})
collection_id: string | null
@ManyToOne(() => ProductCollection, {
nullable: true,
persist: false,
})
collection: ProductCollection | null
@ManyToOne(() => ProductType, {
columnType: "text",
nullable: true,
fieldName: "type_id",
mapToPk: true,
onDelete: "set null",
})
type_id: string | null
@ManyToOne(() => ProductType, {
nullable: true,
persist: false,
})
type: ProductType | null
@ManyToMany(() => ProductTag, "products", {
owner: true,
pivotTable: "product_tags",
index: "IDX_product_tag_id",
})
tags = new Collection<ProductTag>(this)
@OneToMany(() => ProductImage, (image) => image.product_id, {
cascade: [Cascade.PERSIST, Cascade.REMOVE],
})
images = new Collection<ProductImage>(this)
@ManyToMany(() => ProductCategory, "products", {
owner: true,
pivotTable: "product_category_product",
})
categories = new Collection<ProductCategory>(this)
@Property({ columnType: "boolean", default: true })
discountable: boolean
@Property({ columnType: "text", nullable: true })
external_id?: string | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "prod")
this.type_id ??= this.type?.id ?? null
this.collection_id ??= this.collection?.id ?? null
if (!this.handle && this.title) {
this.handle = toHandle(this.title)
}
}
}
export default Product export default Product
@@ -1,30 +1,30 @@
import { import {
Context, Context,
DAL, DAL,
InferEntityType,
ProductCategoryTransformOptions, ProductCategoryTransformOptions,
ProductTypes, ProductTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils" import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
import { import { LoadStrategy, FindOptions as MikroOptions } from "@mikro-orm/core"
EntityDTO,
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
RequiredEntityData,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql" import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models" import { ProductCategory } from "@models"
import { UpdateCategoryInput } from "@types" import { UpdateCategoryInput } from "@types"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> { export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<
typeof ProductCategory
> {
buildFindOptions( buildFindOptions(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }, findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
familyOptions: ProductCategoryTransformOptions = {} familyOptions: ProductCategoryTransformOptions = {}
) { ) {
const findOptions_ = { ...findOptions } const findOptions_ = { ...findOptions }
findOptions_.options ??= { findOptions_.options ??= {}
orderBy: { rank: "ASC" }, findOptions_.options.orderBy = {
id: "ASC",
rank: "ASC",
...findOptions_.options.orderBy,
} }
const fields = (findOptions_.options.fields ??= []) const fields = (findOptions_.options.fields ??= [])
@@ -69,17 +69,19 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
} }
async find( async find(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }, findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {}, transformOptions: ProductCategoryTransformOptions = {},
context: Context = {} context: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions) const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const productCategories = await manager.find( const productCategories = await manager.find<
ProductCategory, InferEntityType<typeof ProductCategory>
findOptions_.where as MikroFilterQuery<ProductCategory>, >(
{ ...findOptions_.options } as MikroOptions<ProductCategory> ProductCategory.name,
findOptions_.where,
{ ...findOptions_.options } as any // TODO
) )
if ( if (
@@ -102,7 +104,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return this.sortCategoriesByRank(categoriesTree) return this.sortCategoriesByRank(categoriesTree)
} }
sortCategoriesByRank(categories: ProductCategory[]): ProductCategory[] { sortCategoriesByRank(
categories: InferEntityType<typeof ProductCategory>[]
): InferEntityType<typeof ProductCategory>[] {
const sortedCategories = categories.sort((a, b) => a.rank - b.rank) const sortedCategories = categories.sort((a, b) => a.rank - b.rank)
for (const category of sortedCategories) { for (const category of sortedCategories) {
@@ -122,12 +126,12 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
descendants?: boolean descendants?: boolean
ancestors?: boolean ancestors?: boolean
}, },
productCategories: ProductCategory[], productCategories: InferEntityType<typeof ProductCategory>[],
findOptions: DAL.FindOptions<ProductCategory> & { findOptions: DAL.FindOptions<typeof ProductCategory> & {
serialize?: boolean serialize?: boolean
} = { where: {} }, } = { where: {} },
context: Context = {} context: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
const { serialize = true } = findOptions const { serialize = true } = findOptions
delete findOptions.serialize delete findOptions.serialize
@@ -173,17 +177,17 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
...findOptions.options, ...findOptions.options,
limit: undefined, limit: undefined,
offset: 0, offset: 0,
} as MikroOptions<ProductCategory> } as MikroOptions<any>
delete where.id delete where.id
delete where.mpath delete where.mpath
delete where.parent_category_id delete where.parent_category_id
const categoriesInTree = serialize const categoriesInTree = serialize
? await this.serialize<ProductCategory[]>( ? await this.serialize<InferEntityType<typeof ProductCategory>[]>(
await manager.find(ProductCategory, where, options) await manager.find(ProductCategory.name, where, options)
) )
: await manager.find(ProductCategory, where, options) : await manager.find(ProductCategory.name, where, options)
const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat])) const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat]))
@@ -235,18 +239,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
} }
async findAndCount( async findAndCount(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }, findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {}, transformOptions: ProductCategoryTransformOptions = {},
context: Context = {} context: Context = {}
): Promise<[ProductCategory[], number]> { ): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions) const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const [productCategories, count] = await manager.findAndCount( const [productCategories, count] = (await manager.findAndCount(
ProductCategory, ProductCategory.name,
findOptions_.where as MikroFilterQuery<ProductCategory>, findOptions_.where,
findOptions_.options as MikroOptions<ProductCategory> findOptions_.options as any
) )) as unknown as [InferEntityType<typeof ProductCategory>[], number]
if ( if (
!transformOptions.includeDescendantsTree && !transformOptions.includeDescendantsTree &&
@@ -271,19 +275,23 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async delete(ids: string[], context: Context = {}): Promise<void> { async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context) await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory, { id: ids }, {}) await manager.nativeDelete(ProductCategory.name, { id: ids }, {})
} }
async softDelete( async softDelete(
ids: string[], ids: string[],
context: Context = {} context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> { ): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context) await this.baseDelete(ids, context)
const categories = await Promise.all( const categories = await Promise.all(
ids.map(async (id) => { ids.map(async (id) => {
const productCategory = await manager.findOne(ProductCategory, { const productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id, id,
}) })
@@ -306,11 +314,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async restore( async restore(
ids: string[], ids: string[],
context: Context = {} context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> { ): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all( const categories = await Promise.all(
ids.map(async (id) => { ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(ProductCategory, { const productCategory = await manager.findOneOrFail<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id, id,
}) })
manager.assign(productCategory, { deleted_at: null }) manager.assign(productCategory, { deleted_at: null })
@@ -327,12 +339,14 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await Promise.all( await Promise.all(
ids.map(async (id) => { ids.map(async (id) => {
const productCategory = await manager.findOne( const productCategory = await manager.findOne<
ProductCategory, InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{ id }, { id },
{ {
populate: ["category_children"], populate: ["category_children"],
} } as any // TODO
) )
if (!productCategory) { if (!productCategory) {
@@ -357,13 +371,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async create( async create(
data: ProductTypes.CreateProductCategoryDTO[], data: ProductTypes.CreateProductCategoryDTO[],
context: Context = {} context: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all( const categories = await Promise.all(
data.map(async (entry, i) => { data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry } const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
const siblingsCount = await manager.count(ProductCategory, { ...entry,
}
const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: categoryData?.parent_category_id || null, parent_category_id: categoryData?.parent_category_id || null,
}) })
@@ -378,14 +394,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
} }
// Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath. // Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath.
let parentCategory: InferEntityType<typeof ProductCategory> | null =
null
const parentCategoryId = const parentCategoryId =
categoryData.parent_category_id ?? categoryData.parent_category?.id categoryData.parent_category_id ?? categoryData.parent_category?.id
if (parentCategoryId) { if (parentCategoryId) {
const parentCategory = await manager.findOne( parentCategory = await manager.findOne<
ProductCategory, InferEntityType<typeof ProductCategory>
parentCategoryId >(ProductCategory.name, parentCategoryId)
)
if (!parentCategory) { if (!parentCategory) {
throw new MedusaError( throw new MedusaError(
@@ -393,14 +410,28 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
`Parent category with id: '${parentCategoryId}' does not exist` `Parent category with id: '${parentCategoryId}' does not exist`
) )
} }
categoryData.mpath = parentCategory.mpath
} }
return manager.create( const result = await manager.create<
ProductCategory, InferEntityType<typeof ProductCategory>
categoryData as RequiredEntityData<ProductCategory> >(
ProductCategory.name,
categoryData as unknown as InferEntityType<typeof ProductCategory>
) )
/**
* Since "mpath" calculation relies on the id of the created
* category, we have to compute it after calling manager.create. So
* that we can access the "category.id" which is under the hood
* defined by DML.
*/
manager.assign(result, {
mpath: parentCategory
? `${parentCategory.mpath}.${result.id}`
: result.id,
})
return result
}) })
) )
@@ -411,14 +442,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async update( async update(
data: UpdateCategoryInput[], data: UpdateCategoryInput[],
context: Context = {} context: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context) const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all( const categories = await Promise.all(
data.map(async (entry, i) => { data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry } const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
let productCategory = (await manager.findOne(ProductCategory, { ...entry,
}
let productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id: categoryData.id, id: categoryData.id,
})) as ProductCategory })
if (!productCategory) { if (!productCategory) {
throw new MedusaError( throw new MedusaError(
@@ -465,10 +500,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
) )
)[0] )[0]
const newParentCategory = await manager.findOne( const newParentCategory = await manager.findOne<
ProductCategory, InferEntityType<typeof ProductCategory>
categoryData.parent_category_id >(ProductCategory.name, categoryData.parent_category_id)
)
if (!newParentCategory) { if (!newParentCategory) {
throw new MedusaError( throw new MedusaError(
@@ -487,13 +521,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
) )
function updateMpathRecursively( function updateMpathRecursively(
category: ProductCategory, category: InferEntityType<typeof ProductCategory>,
newBaseMpath: string newBaseMpath: string
) { ) {
const newMpath = `${newBaseMpath}.${category.id}` const newMpath = `${newBaseMpath}.${category.id}`
category.mpath = newMpath category.mpath = newMpath
for (let child of category.category_children) { for (let child of category.category_children) {
child = manager.getReference(ProductCategory, child.id) child = manager.getReference(ProductCategory.name, child.id)
manager.assign( manager.assign(
child, child,
categoryDataChildrenMap.get(child.id) ?? {} categoryDataChildrenMap.get(child.id) ?? {}
@@ -502,12 +536,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
} }
} }
updateMpathRecursively(productCategory!, newParentCategory.mpath!) updateMpathRecursively(
productCategory!,
(newParentCategory as any).mpath!
)
// categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}` // categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
} }
// Rerank the siblings in the new parent // Rerank the siblings in the new parent
const siblingsCount = await manager.count(ProductCategory, { const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: categoryData.parent_category_id, parent_category_id: categoryData.parent_category_id,
}) })
@@ -534,7 +571,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return productCategory return productCategory
// If only the rank changed, we need to rerank all siblings. // If only the rank changed, we need to rerank all siblings.
} else if (isDefined(categoryData.rank)) { } else if (isDefined(categoryData.rank)) {
const siblingsCount = await manager.count(ProductCategory, { const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: productCategory.parent_category_id, parent_category_id: productCategory.parent_category_id,
}) })
@@ -546,7 +583,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await this.rerankAllSiblings( await this.rerankAllSiblings(
manager, manager,
productCategory, productCategory,
categoryData as Partial<EntityDTO<ProductCategory>> & { categoryData as Partial<InferEntityType<typeof ProductCategory>> & {
rank: number rank: number
} }
) )
@@ -569,9 +606,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterDeletion( protected async rerankSiblingsAfterDeletion(
manager: SqlEntityManager, manager: SqlEntityManager,
removedSibling: Partial<ProductCategory> removedSibling: Partial<InferEntityType<typeof ProductCategory>>
) { ) {
const affectedSiblings = await manager.find(ProductCategory, { const affectedSiblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
parent_category_id: removedSibling.parent_category_id, parent_category_id: removedSibling.parent_category_id,
rank: { $gt: removedSibling.rank }, rank: { $gt: removedSibling.rank },
}) })
@@ -586,9 +625,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterCreation( protected async rerankSiblingsAfterCreation(
manager: SqlEntityManager, manager: SqlEntityManager,
addedSibling: Partial<EntityDTO<ProductCategory>> addedSibling: Partial<InferEntityType<typeof ProductCategory>>
) { ) {
const affectedSiblings = await manager.find(ProductCategory, { const affectedSiblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
parent_category_id: addedSibling.parent_category_id, parent_category_id: addedSibling.parent_category_id,
rank: { $gte: addedSibling.rank }, rank: { $gte: addedSibling.rank },
}) })
@@ -603,16 +644,22 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankAllSiblings( protected async rerankAllSiblings(
manager: SqlEntityManager, manager: SqlEntityManager,
originalSibling: Partial<ProductCategory> & { rank: number }, originalSibling: Partial<InferEntityType<typeof ProductCategory>> & {
updatedSibling: Partial<EntityDTO<ProductCategory>> & { rank: number } rank: number
},
updatedSibling: Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
}
) { ) {
if (originalSibling.rank === updatedSibling.rank) { if (originalSibling.rank === updatedSibling.rank) {
return return
} }
if (originalSibling.rank < updatedSibling.rank) { if (originalSibling.rank < updatedSibling.rank) {
const siblings = await manager.find( const siblings = await manager.find<
ProductCategory, InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{ {
parent_category_id: originalSibling.parent_category_id, parent_category_id: originalSibling.parent_category_id,
rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank }, rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank },
@@ -627,8 +674,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
manager.persist(updatedSiblings) manager.persist(updatedSiblings)
} else { } else {
const siblings = await manager.find( const siblings = await manager.find<
ProductCategory, InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{ {
parent_category_id: originalSibling.parent_category_id, parent_category_id: originalSibling.parent_category_id,
rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank }, rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank },
@@ -1,11 +1,11 @@
import { Product } from "@models" import { Product } from "@models"
import { Context, DAL } from "@medusajs/framework/types" import { Context, DAL } from "@medusajs/framework/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { DALUtils } from "@medusajs/framework/utils" import { DALUtils } from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Product>( export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Product Product
) { ) {
constructor(...args: any[]) { constructor(...args: any[]) {
@@ -19,7 +19,9 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
* first find all products that are in the categories, and then exclude them * first find all products that are in the categories, and then exclude them
*/ */
protected async mutateNotInCategoriesConstraints( protected async mutateNotInCategoriesConstraints(
findOptions: DAL.FindOptions<Product> = { where: {} }, findOptions: DAL.FindOptions<typeof Product> = {
where: {},
},
context: Context = {} context: Context = {}
): Promise<void> { ): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -29,7 +31,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
findOptions.where.categories?.id?.["$nin"] findOptions.where.categories?.id?.["$nin"]
) { ) {
const productsInCategories = await manager.find( const productsInCategories = await manager.find(
Product, this.entity,
{ {
categories: { categories: {
id: { $in: findOptions.where.categories.id["$nin"] }, id: { $in: findOptions.where.categories.id["$nin"] },
@@ -2,6 +2,7 @@ import {
Context, Context,
DAL, DAL,
FindConfig, FindConfig,
InferEntityType,
ProductTypes, ProductTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -33,7 +34,7 @@ export default class ProductCategoryService {
productCategoryId: string, productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> { ): Promise<InferEntityType<typeof ProductCategory>> {
if (!isDefined(productCategoryId)) { if (!isDefined(productCategoryId)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
@@ -41,7 +42,7 @@ export default class ProductCategoryService {
) )
} }
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>( const queryOptions = ModulesSdkUtils.buildQuery(
{ {
id: productCategoryId, id: productCategoryId,
}, },
@@ -67,7 +68,7 @@ export default class ProductCategoryService {
) )
} }
return productCategories[0] as ProductCategory return productCategories[0]
} }
@InjectManager("productCategoryRepository_") @InjectManager("productCategoryRepository_")
@@ -75,7 +76,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {}, filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
const transformOptions = { const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false, includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false, includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -94,17 +95,14 @@ export default class ProductCategoryService {
delete filters.q delete filters.q
} }
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>( const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
filters,
config
)
queryOptions.where ??= {} queryOptions.where ??= {}
return (await this.productCategoryRepository_.find( return await this.productCategoryRepository_.find(
queryOptions, queryOptions,
transformOptions, transformOptions,
sharedContext sharedContext
)) as ProductCategory[] )
} }
@InjectManager("productCategoryRepository_") @InjectManager("productCategoryRepository_")
@@ -112,7 +110,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {}, filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<[ProductCategory[], number]> { ): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const transformOptions = { const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false, includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false, includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -131,37 +129,34 @@ export default class ProductCategoryService {
delete filters.q delete filters.q
} }
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>( const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
filters,
config
)
queryOptions.where ??= {} queryOptions.where ??= {}
return (await this.productCategoryRepository_.findAndCount( return await this.productCategoryRepository_.findAndCount(
queryOptions, queryOptions,
transformOptions, transformOptions,
sharedContext sharedContext
)) as [ProductCategory[], number] )
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
async create( async create(
data: ProductTypes.CreateProductCategoryDTO[], data: ProductTypes.CreateProductCategoryDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
return (await ( return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository this.productCategoryRepository_ as unknown as ProductCategoryRepository
).create(data, sharedContext)) as ProductCategory[] ).create(data, sharedContext)
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
async update( async update(
data: UpdateCategoryInput[], data: UpdateCategoryInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> { ): Promise<InferEntityType<typeof ProductCategory>[]> {
return (await ( return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository this.productCategoryRepository_ as unknown as ProductCategoryRepository
).update(data, sharedContext)) as ProductCategory[] ).update(data, sharedContext)
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
@@ -3,6 +3,7 @@ import {
DAL, DAL,
FindConfig, FindConfig,
IEventBusModuleService, IEventBusModuleService,
InferEntityType,
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleJoinerConfig, ModuleJoinerConfig,
ModulesSdkTypes, ModulesSdkTypes,
@@ -105,15 +106,31 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService implements ProductTypes.IProductModuleService
{ {
protected baseRepository_: DAL.RepositoryService protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<Product> protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<ProductVariant> InferEntityType<typeof Product>
>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductVariant>
>
protected readonly productCategoryService_: ProductCategoryService protected readonly productCategoryService_: ProductCategoryService
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<ProductTag> protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<
protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<ProductCollection> InferEntityType<typeof ProductTag>
protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService<ProductImage> >
protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService<ProductType> protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<
protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService<ProductOption> InferEntityType<typeof ProductCollection>
protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService<ProductOptionValue> >
protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductImage>
>
protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductType>
>
protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductOption>
>
protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductOptionValue>
>
protected readonly eventBusModuleService_?: IEventBusModuleService protected readonly eventBusModuleService_?: IEventBusModuleService
constructor( constructor(
@@ -211,12 +228,16 @@ export default class ProductModuleService
return { return {
...config, ...config,
order: { order: {
id: "ASC",
...config?.order, ...config?.order,
...(hasImagesRelation ? { ...(hasImagesRelation
? {
images: { images: {
rank: "ASC", rank: "ASC",
...((config?.order?.images as object) ?? {}),
}, },
} : {}), }
: {}),
}, },
} }
} }
@@ -256,7 +277,7 @@ export default class ProductModuleService
protected async createVariants_( protected async createVariants_(
data: ProductTypes.CreateProductVariantDTO[], data: ProductTypes.CreateProductVariantDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> { ): Promise<InferEntityType<typeof ProductVariant>[]> {
if (data.some((v) => !v.product_id)) { if (data.some((v) => !v.product_id)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -332,8 +353,8 @@ export default class ProductModuleService
(variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id (variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id
) )
let created: ProductVariant[] = [] let created: InferEntityType<typeof ProductVariant>[] = []
let updated: ProductVariant[] = [] let updated: InferEntityType<typeof ProductVariant>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createVariants_(forCreate, sharedContext) created = await this.createVariants_(forCreate, sharedContext)
@@ -400,7 +421,7 @@ export default class ProductModuleService
protected async updateVariants_( protected async updateVariants_(
data: UpdateProductVariantInput[], data: UpdateProductVariantInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> { ): Promise<InferEntityType<typeof ProductVariant>[]> {
// Validation step // Validation step
const variantIdsToUpdate = data.map(({ id }) => id) const variantIdsToUpdate = data.map(({ id }) => id)
const variants = await this.productVariantService_.list( const variants = await this.productVariantService_.list(
@@ -535,8 +556,8 @@ export default class ProductModuleService
(tag): tag is ProductTypes.CreateProductTagDTO => !tag.id (tag): tag is ProductTypes.CreateProductTagDTO => !tag.id
) )
let created: ProductTag[] = [] let created: InferEntityType<typeof ProductTag>[] = []
let updated: ProductTag[] = [] let updated: InferEntityType<typeof ProductTag>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.productTagService_.create(forCreate, sharedContext) created = await this.productTagService_.create(forCreate, sharedContext)
@@ -665,8 +686,8 @@ export default class ProductModuleService
(type): type is ProductTypes.CreateProductTypeDTO => !type.id (type): type is ProductTypes.CreateProductTypeDTO => !type.id
) )
let created: ProductType[] = [] let created: InferEntityType<typeof ProductType>[] = []
let updated: ProductType[] = [] let updated: InferEntityType<typeof ProductType>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.productTypeService_.create(forCreate, sharedContext) created = await this.productTypeService_.create(forCreate, sharedContext)
@@ -763,7 +784,7 @@ export default class ProductModuleService
protected async createOptions_( protected async createOptions_(
data: ProductTypes.CreateProductOptionDTO[], data: ProductTypes.CreateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> { ): Promise<InferEntityType<typeof ProductOption>[]> {
if (data.some((v) => !v.product_id)) { if (data.some((v) => !v.product_id)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -810,8 +831,8 @@ export default class ProductModuleService
(option): option is ProductTypes.CreateProductOptionDTO => !option.id (option): option is ProductTypes.CreateProductOptionDTO => !option.id
) )
let created: ProductOption[] = [] let created: InferEntityType<typeof ProductOption>[] = []
let updated: ProductOption[] = [] let updated: InferEntityType<typeof ProductOption>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createOptions_(forCreate, sharedContext) created = await this.createOptions_(forCreate, sharedContext)
@@ -876,7 +897,7 @@ export default class ProductModuleService
protected async updateOptions_( protected async updateOptions_(
data: UpdateProductOptionInput[], data: UpdateProductOptionInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> { ): Promise<InferEntityType<typeof ProductOption>[]> {
// Validation step // Validation step
if (data.some((option) => !option.id)) { if (data.some((option) => !option.id)) {
throw new MedusaError( throw new MedusaError(
@@ -985,7 +1006,7 @@ export default class ProductModuleService
async createCollections_( async createCollections_(
data: ProductTypes.CreateProductCollectionDTO[], data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> { ): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map( const normalizedInput = data.map(
ProductModuleService.normalizeCreateProductCollectionInput ProductModuleService.normalizeCreateProductCollectionInput
) )
@@ -1030,8 +1051,8 @@ export default class ProductModuleService
!collection.id !collection.id
) )
let created: ProductCollection[] = [] let created: InferEntityType<typeof ProductCollection>[] = []
let updated: ProductCollection[] = [] let updated: InferEntityType<typeof ProductCollection>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createCollections_(forCreate, sharedContext) created = await this.createCollections_(forCreate, sharedContext)
@@ -1125,7 +1146,7 @@ export default class ProductModuleService
protected async updateCollections_( protected async updateCollections_(
data: UpdateCollectionInput[], data: UpdateCollectionInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> { ): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map( const normalizedInput = data.map(
ProductModuleService.normalizeUpdateProductCollectionInput ProductModuleService.normalizeUpdateProductCollectionInput
) as UpdateCollectionInput[] ) as UpdateCollectionInput[]
@@ -1139,7 +1160,7 @@ export default class ProductModuleService
sharedContext sharedContext
) )
const collections: ProductCollection[] = [] const collections: InferEntityType<typeof ProductCollection>[] = []
const updateSelectorAndData = updatedCollections.flatMap( const updateSelectorAndData = updatedCollections.flatMap(
(collectionData) => { (collectionData) => {
@@ -1178,7 +1199,7 @@ export default class ProductModuleService
collections.push({ collections.push({
...collectionData, ...collectionData,
products: productsToUpdate ?? [], products: productsToUpdate ?? [],
} as ProductCollection) } as InferEntityType<typeof ProductCollection>)
return result return result
} }
@@ -1208,7 +1229,12 @@ export default class ProductModuleService
): Promise< ): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO
> { > {
const input = Array.isArray(data) ? data : [data] const input = (Array.isArray(data) ? data : [data]).map(
(productCategory) => {
productCategory.handle ??= kebabCase(productCategory.name)
return productCategory
}
)
const categories = await this.productCategoryService_.create( const categories = await this.productCategoryService_.create(
input, input,
@@ -1255,8 +1281,8 @@ export default class ProductModuleService
!category.id !category.id
) )
let created: ProductCategory[] = [] let created: InferEntityType<typeof ProductCategory>[] = []
let updated: ProductCategory[] = [] let updated: InferEntityType<typeof ProductCategory>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.productCategoryService_.create( created = await this.productCategoryService_.create(
@@ -1406,8 +1432,8 @@ export default class ProductModuleService
(product): product is ProductTypes.CreateProductDTO => !product.id (product): product is ProductTypes.CreateProductDTO => !product.id
) )
let created: Product[] = [] let created: InferEntityType<typeof Product>[] = []
let updated: Product[] = [] let updated: InferEntityType<typeof Product>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createProducts_(forCreate, sharedContext) created = await this.createProducts_(forCreate, sharedContext)
@@ -1494,7 +1520,7 @@ export default class ProductModuleService
protected async createProducts_( protected async createProducts_(
data: ProductTypes.CreateProductDTO[], data: ProductTypes.CreateProductDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<Product[]> { ): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll( const normalizedInput = await promiseAll(
data.map(async (d) => { data.map(async (d) => {
const normalized = await this.normalizeCreateProductInput( const normalized = await this.normalizeCreateProductInput(
@@ -1581,7 +1607,7 @@ export default class ProductModuleService
protected async updateProducts_( protected async updateProducts_(
data: UpdateProductInput[], data: UpdateProductInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<Product[]> { ): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll( const normalizedInput = await promiseAll(
data.map(async (d) => { data.map(async (d) => {
const normalized = await this.normalizeUpdateProductInput( const normalized = await this.normalizeUpdateProductInput(
@@ -1733,6 +1759,13 @@ export default class ProductModuleService
) { ) {
this.validateProductPayload(productData) this.validateProductPayload(productData)
if (!productData.title) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Product title is required`
)
}
const options = productData.options const options = productData.options
const missingOptionsVariants: string[] = [] const missingOptionsVariants: string[] = []
@@ -1872,7 +1905,7 @@ export default class ProductModuleService
variants: variants:
| ProductTypes.CreateProductVariantDTO[] | ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[], | ProductTypes.UpdateProductVariantDTO[],
options: ProductOption[] options: InferEntityType<typeof ProductOption>[]
): ):
| ProductTypes.CreateProductVariantDTO[] | ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[] { | ProductTypes.UpdateProductVariantDTO[] {
@@ -1945,7 +1978,7 @@ export default class ProductModuleService
| ProductTypes.CreateProductVariantDTO | ProductTypes.CreateProductVariantDTO
| UpdateProductVariantInput | UpdateProductVariantInput
) & { options: { id: string }[]; product_id: string })[], ) & { options: { id: string }[]; product_id: string })[],
variants: ProductVariant[] variants: InferEntityType<typeof ProductVariant>[]
) { ) {
for (const variantData of data) { for (const variantData of data) {
const existingVariant = variants.find((v) => { const existingVariant = variants.find((v) => {