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
+2252 -1876

No files matched your search

@@ -74,7 +74,8 @@ medusaIntegrationTestRunner({
// BREAKING: Type input changed from {type: {value: string}} to {type_id: string}
type_id: baseType.id,
tags: [{ id: baseTag1.id }, { id: baseTag2.id }],
images: [{
images: [
{
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 () => {
const res = await api
.get("/admin/products", adminHeaders)
@@ -991,7 +991,10 @@ medusaIntegrationTestRunner({
})
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.arrayContaining([
@@ -1436,7 +1436,7 @@ medusaIntegrationTestRunner({
},
{
url: "image-two",
}
},
],
})
@@ -1487,7 +1487,10 @@ medusaIntegrationTestRunner({
})
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.arrayContaining([
+6 -4
View File
@@ -1,3 +1,4 @@
import { InferEntityType } from "../dml"
import { Dictionary, FilterQuery, Order } from "./utils"
export { FilterQuery, OperatorMap } from "./utils"
@@ -22,7 +23,7 @@ export interface BaseFilterable<T> {
/**
* 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.
*/
@@ -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)
*/
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.
*/
where: FilterQuery<T> & BaseFilterable<FilterQuery<T>>
where: FilterQuery<InferEntityType<T>> &
BaseFilterable<FilterQuery<InferEntityType<T>>>
/**
* 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 { Context } from "../shared-context"
import {
BaseFilterable,
FilterQuery,
FilterQuery as InternalFilterQuery,
FindOptions,
UpsertWithReplaceConfig,
} from "./index"
import { EntityClass } from "@mikro-orm/core"
import { IDmlEntity, InferTypeOf } from "../dml"
type EntityClassName = 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 = {
created: 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
* ORM directly and allows to switch to another ORM without changing the business logic.
*/
interface BaseRepositoryService<T = any> {
interface BaseRepositoryService {
transaction<TManager = unknown>(
task: (transactionManager: TManager) => Promise<any>,
context?: {
@@ -42,22 +49,28 @@ interface BaseRepositoryService<T = any> {
): Promise<TOutput>
}
export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
find(options?: FindOptions<T>, context?: Context): Promise<T[]>
export interface RepositoryService<T = any> extends BaseRepositoryService {
find(
options?: FindOptions<T>,
context?: Context
): Promise<InferRepositoryReturnType<T>[]>
findAndCount(
options?: FindOptions<T>,
context?: Context
): Promise<[T[], number]>
): Promise<[InferRepositoryReturnType<T>[], number]>
create(data: any[], context?: Context): Promise<T[]>
update(data: { entity; update }[], context?: Context): Promise<T[]>
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
create(
data: any[],
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.
@@ -74,37 +87,45 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
| InternalFilterQuery
| InternalFilterQuery[],
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]>
restore(
idsOrFilter: string[] | InternalFilterQuery,
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(
data: any[],
config?: UpsertWithReplaceConfig<T>,
config?: UpsertWithReplaceConfig<InferRepositoryReturnType<T>>,
context?: Context
): Promise<{ entities: T[]; performedActions: PerformedActions }>
): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}>
}
export interface TreeRepositoryService<T = any>
extends BaseRepositoryService<T> {
export interface TreeRepositoryService<T = any> extends BaseRepositoryService {
find(
options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<T[]>
): Promise<InferRepositoryReturnType<T>[]>
findAndCount(
options?: FindOptions<T>,
transformOptions?: RepositoryTransformOptions,
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>
}
+9 -3
View File
@@ -1,3 +1,5 @@
import { Constructor } from "../modules-sdk"
type ExpandProperty<T> = T extends (infer U)[] ? NonNullable<U> : NonNullable<T>
export type Dictionary<T = any> = {
@@ -84,9 +86,7 @@ type FilterValue<T> =
type PrevLimit = [never, 0, 1, 2]
export type FilterQuery<T = any, Prev extends number = 3> = Prev extends never
? never
: {
export type FilterQueryProperties<T, Prev extends number = 3> = {
[Key in keyof T]?: T[Key] extends
| boolean
| number
@@ -104,6 +104,12 @@ export type FilterQuery<T = any, Prev extends number = 3> = Prev extends 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"
type QueryOrderKeysFlat = QueryOrder | 1 | -1
+16 -9
View File
@@ -55,6 +55,13 @@ export type RelationshipTypes =
| "belongsTo"
| "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
*/
@@ -135,14 +142,12 @@ export interface EntityConstructor<Props> extends Function {
* "belongsTo" relation meaning "hasOne" and "ManyToOne"
*/
export type InferForeignKeys<Schema extends DMLSchema> = {
[K in keyof Schema as Schema[K] extends { type: infer Type }
? Type extends RelationshipTypes
[K in keyof Schema as Schema[K] extends { type: "belongsTo" }
? `${K & string}_id`
: never
: never]: Schema[K] extends { type: infer Type }
? Type extends RelationshipTypes
? string
: never
: never]: Schema[K] extends { type: "belongsTo" }
? null extends Schema[K]["$dataType"]
? string | null
: string
: never
}
@@ -182,7 +187,8 @@ export type InferManyToManyFields<Relation> = InferHasManyFields<Relation>
* Inferring the types of the schema fields from the DML
* entity
*/
export type InferSchemaFields<Schema extends DMLSchema> = Prettify<{
export type InferSchemaFields<Schema extends DMLSchema> = Prettify<
{
[K in keyof Schema]: Schema[K] extends RelationshipType<any>
? Schema[K]["type"] extends "belongsTo"
? InferBelongsToFields<Schema[K]["$dataType"]>
@@ -194,7 +200,8 @@ export type InferSchemaFields<Schema extends DMLSchema> = Prettify<{
? InferManyToManyFields<Schema[K]["$dataType"]>
: never
: Schema[K]["$dataType"]
}>
} & InferForeignKeys<Schema>
>
/**
* Helper to infer the schema type of a DmlEntity
+1 -1
View File
@@ -69,7 +69,7 @@
"scripts": {
"build": "rimraf dist && tsc --build",
"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"
}
}
@@ -120,9 +120,9 @@ class Entity3 {
}
}
const Entity1Repository = mikroOrmBaseRepositoryFactory<Entity1>(Entity1)
const Entity2Repository = mikroOrmBaseRepositoryFactory<Entity2>(Entity2)
const Entity3Repository = mikroOrmBaseRepositoryFactory<Entity3>(Entity3)
const Entity1Repository = mikroOrmBaseRepositoryFactory(Entity1)
const Entity2Repository = mikroOrmBaseRepositoryFactory(Entity2)
const Entity3Repository = mikroOrmBaseRepositoryFactory(Entity3)
describe("mikroOrmRepository", () => {
let orm!: MikroORM
@@ -1,8 +1,10 @@
import {
BaseFilterable,
Context,
DAL,
FilterQuery,
FindOptions,
InferEntityType,
InferRepositoryReturnType,
FilterQuery as InternalFilterQuery,
PerformedActions,
RepositoryService,
@@ -15,11 +17,10 @@ import {
EntityName,
EntityProperty,
EntitySchema,
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
LoadStrategy,
ReferenceType,
RequiredEntityData,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
@@ -28,6 +29,7 @@ import {
MedusaError,
promiseAll,
} from "../../common"
import { toMikroORMEntity } from "../../dml"
import { buildQuery } from "../../modules-sdk/build-query"
import {
getSoftDeletedCascadedEntitiesIdsMappedBy,
@@ -37,7 +39,7 @@ import { dbErrorMapper } from "./db-error-mapper"
import { mikroOrmSerializer } from "./mikro-orm-serializer"
import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
export class MikroOrmBase<T = any> {
export class MikroOrmBase {
readonly manager_: any
protected constructor({ manager }) {
@@ -90,10 +92,12 @@ export class MikroOrmBase<T = any> {
* related ones.
*/
export class MikroOrmBaseRepository<T extends object = object>
extends MikroOrmBase<T>
export class MikroOrmBaseRepository<const T extends object = object>
extends MikroOrmBase
implements RepositoryService<T>
{
entity: EntityClass<InferEntityType<T>>
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
@@ -144,43 +148,55 @@ export class MikroOrmBaseRepository<T extends object = object>
})
}
create(data: unknown[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.")
}
update(data: { entity; update }[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.")
}
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
create(
data: unknown[],
context?: Context
): Promise<void> {
): Promise<InferRepositoryReturnType<T>[]> {
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.")
}
findAndCount(
options?: DAL.FindOptions<T>,
context?: Context
): Promise<[T[], number]> {
): Promise<[InferRepositoryReturnType<T>[], number]> {
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.")
}
upsertWithReplace(
data: unknown[],
config: UpsertWithReplaceConfig<T> = {
config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
relations: [],
},
context: Context = {}
): Promise<{ entities: T[]; performedActions: PerformedActions }> {
): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}> {
throw new Error("Method not implemented.")
}
@@ -188,10 +204,10 @@ export class MikroOrmBaseRepository<T extends object = object>
filters:
| string
| string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
| DAL.FindOptions<T>["where"]
| DAL.FindOptions<T>["where"][],
sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
const entities = await this.find({ where: filters as any }, sharedContext)
const date = new Date()
@@ -212,8 +228,8 @@ export class MikroOrmBaseRepository<T extends object = object>
async restore(
idsOrFilter: string[] | InternalFilterQuery,
sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
const query = buildQuery(idsOrFilter, {
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
const query = buildQuery<T>(idsOrFilter, {
withDeleted: true,
})
@@ -245,13 +261,13 @@ export class MikroOrmBaseRepository<T extends object = object>
findOptions.where = {
$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<
T extends object = object
> extends MikroOrmBase<T> {
const T extends object = object
> extends MikroOrmBase {
constructor() {
// @ts-ignore
super(...arguments)
@@ -261,7 +277,7 @@ export class MikroOrmBaseTreeRepository<
options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<T[]> {
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.")
}
@@ -269,15 +285,21 @@ export class MikroOrmBaseTreeRepository<
options?: DAL.FindOptions,
transformOptions?: RepositoryTransformOptions,
context?: Context
): Promise<[T[], number]> {
): Promise<[InferRepositoryReturnType<T>[], number]> {
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.")
}
update(data: unknown[], context?: Context): Promise<T[]> {
update(
data: unknown[],
context?: Context
): Promise<InferRepositoryReturnType<T>[]> {
throw new Error("Method not implemented.")
}
@@ -286,12 +308,18 @@ export class MikroOrmBaseTreeRepository<
}
}
export function mikroOrmBaseRepositoryFactory<T extends object = object>(
entity: any
export function mikroOrmBaseRepositoryFactory<const T extends object>(
entity: T
): {
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
} {
const mikroOrmEntity = toMikroORMEntity(entity) as EntityClass<
InferEntityType<T>
>
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
entity = mikroOrmEntity
// @ts-ignore
constructor(...args: any[]) {
// @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 entities = data.map((data_) => {
return manager.create(
entity as EntityName<T>,
data_ as RequiredEntityData<T>
)
return manager.create(this.entity, data_)
})
manager.persist(entities)
return entities
return entities as InferRepositoryReturnType<T>[]
}
/**
@@ -350,7 +378,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const relations = manager
.getDriver()
.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
// 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)
await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
@@ -411,17 +442,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
}
async delete(
filters: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
filters: FindOptions<T>["where"],
context?: Context
): Promise<void> {
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(
options: DAL.FindOptions<T> = { where: {} },
options: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context?: Context
): Promise<T[]> {
): Promise<InferRepositoryReturnType<T>[]> {
const manager = this.getActiveManager<EntityManager>(context)
const findOptions_ = { ...options }
@@ -443,17 +474,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
findOptions: findOptions_,
})
return await manager.find(
entity as EntityName<T>,
return (await manager.find(
this.entity as EntityName<T>,
findOptions_.where as MikroFilterQuery<T>,
findOptions_.options as MikroOptions<T>
)
)) as InferRepositoryReturnType<T>[]
}
async findAndCount(
findOptions: DAL.FindOptions<T> = { where: {} },
findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context: Context = {}
): Promise<[T[], number]> {
): Promise<[InferRepositoryReturnType<T>[], number]> {
const manager = this.getActiveManager<EntityManager>(context)
const findOptions_ = { ...findOptions }
@@ -467,18 +498,22 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
findOptions: findOptions_,
})
return await manager.findAndCount(
entity as EntityName<T>,
findOptions_.where as MikroFilterQuery<T>,
findOptions_.options as MikroOptions<T>
)
return (await manager.findAndCount(
this.entity,
findOptions_.where,
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 primaryKeys =
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
this.entity
)
let primaryKeysCriteria: { [key: string]: any }[] = []
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) {
allEntities = await Promise.all(
@@ -513,7 +548,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const existingEntities = allEntities.flat()
const existingEntitiesMap = new Map<string, T>()
const existingEntitiesMap = new Map<
string,
InferRepositoryReturnType<T>
>()
existingEntities.forEach((entity) => {
if (entity) {
const key =
@@ -525,9 +563,9 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
}
})
const upsertedEntities: T[] = []
const createdEntities: T[] = []
const updatedEntities: T[] = []
const upsertedEntities: InferRepositoryReturnType<T>[] = []
const createdEntities: InferRepositoryReturnType<T>[] = []
const updatedEntities: InferRepositoryReturnType<T>[] = []
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 -
@@ -542,8 +580,8 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const updatedType = manager.assign(existingEntity, data_)
updatedEntities.push(updatedType)
} else {
const newEntity = manager.create<T>(entity, data_)
createdEntities.push(newEntity)
const newEntity = manager.create(this.entity, data_)
createdEntities.push(newEntity as InferRepositoryReturnType<T>)
}
})
@@ -558,7 +596,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
}
// TODO return the all, created, updated entities
return upsertedEntities
return upsertedEntities as InferRepositoryReturnType<T>[]
}
// 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
async upsertWithReplace(
data: any[],
config: UpsertWithReplaceConfig<T> = {
config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
relations: [],
},
context: Context = {}
): Promise<{ entities: T[]; performedActions: PerformedActions }> {
): Promise<{
entities: InferRepositoryReturnType<T>[]
performedActions: PerformedActions
}> {
const performedActions: PerformedActions = {
created: {},
updated: {},
@@ -593,7 +634,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
const allRelations = manager
.getDriver()
.getMetadata()
.get(entity.name).relations
.get(this.entity.name).relations
const nonexistentRelations = arrayDifference(
(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 })
originalDataMap.set(mainEntity.id, entry)
@@ -634,7 +679,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
let {
orderedEntities: upsertedTopLevelEntities,
performedActions: performedActions_,
} = await this.upsertMany_(manager, entity.name, toUpsert)
} = await this.upsertMany_(manager, this.entity.name, toUpsert)
this.mergePerformedActions(performedActions, performedActions_)
@@ -954,10 +999,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters:
| string
| string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
| DAL.FindOptions<T>["where"]
| DAL.FindOptions<T>["where"][],
sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
if (Array.isArray(filters) && !filters.filter(Boolean).length) {
return [[], {}]
}
@@ -975,10 +1020,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters:
| string
| string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
| DAL.FindOptions<T>["where"]
| DAL.FindOptions<T>["where"][],
sharedContext: Context = {}
): Promise<[T[], Record<string, unknown[]>]> {
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
if (Array.isArray(filters) && !filters.filter(Boolean).length) {
return [[], {}]
}
@@ -996,11 +1041,12 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
filters:
| string
| string[]
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[]
) {
const primaryKeys =
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
| DAL.FindOptions<T>["where"]
| DAL.FindOptions<T>["where"][]
): DAL.FindOptions<T>["where"] {
const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
this.entity
)
const filterArray = Array.isArray(filters) ? filters : [filters]
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 { DuplicateIdPropertyError } from "../errors"
import {
createMikrORMEntity,
toMikroOrmEntities,
mikroORMEntityBuilder,
toMikroORMEntity,
toMikroOrmEntities,
} from "../helpers/create-mikro-orm-entity"
describe("Entity builder", () => {
beforeEach(() => {
MetadataStorage.clear()
mikroORMEntityBuilder.clear()
})
const defaultColumnMetadata = {
@@ -992,12 +993,14 @@ describe("Entity builder", () => {
})
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
expectTypeOf(new User()).toEqualTypeOf<{
id: number
username: string
email: string
role: "moderator" | "admin" | "guest"
deleted_at: Date | null
created_at: Date
updated_at: Date
}>()
const metaData = MetadataStorage.getMetadataFromDecorator(User)
@@ -1939,6 +1942,11 @@ describe("Entity builder", () => {
expression:
'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({
@@ -2050,6 +2058,11 @@ describe("Entity builder", () => {
expression:
'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({
@@ -2160,6 +2173,11 @@ describe("Entity builder", () => {
expression:
'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({
@@ -2819,7 +2837,6 @@ describe("Entity builder", () => {
reference: "scalar",
setter: false,
type: "string",
isForeignKey: true,
persist: false,
},
created_at: {
@@ -2943,12 +2960,21 @@ describe("Entity builder", () => {
nullable: false,
onDelete: undefined,
reference: "m:1",
isForeignKey: true,
},
...defaultColumnMetadata,
})
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:
'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',
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([
{
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:
'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',
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',
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)
@@ -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',
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,
onDelete: "cascade",
reference: "m:1",
isForeignKey: true,
},
created_at: {
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<{
email: string
isVerified: boolean
@@ -3631,6 +3671,7 @@ describe("Entity builder", () => {
}
}
}>()
expectTypeOf(new Email().user_id).toEqualTypeOf<string>()
const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User")
@@ -3740,7 +3781,6 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
persist: false,
},
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<{
email: string
isVerified: boolean
@@ -3822,6 +3867,7 @@ describe("Entity builder", () => {
}
} | null
}>()
expectTypeOf(new Email().user_id).toEqualTypeOf<string | null>()
const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User")
@@ -3931,7 +3977,6 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
persist: false,
},
created_at: {
@@ -4121,7 +4166,6 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -4310,7 +4354,6 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: true,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -4566,7 +4609,6 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
persist: false,
},
created_at: {
@@ -4765,7 +4807,6 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
persist: false,
},
created_at: {
@@ -4872,7 +4913,6 @@ describe("Entity builder", () => {
mapToPk: true,
nullable: false,
onDelete: undefined,
isForeignKey: true,
},
children: {
cascade: undefined,
@@ -4983,7 +5023,6 @@ describe("Entity builder", () => {
name: "parent_id",
type: "string",
columnType: "text",
isForeignKey: true,
persist: false,
reference: "scalar",
getter: false,
@@ -5110,8 +5149,9 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: true,
pivotTable: "team_users",
mappedBy: "users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
@@ -5177,7 +5217,9 @@ describe("Entity builder", () => {
users: {
reference: "m:n",
name: "users",
mappedBy: "teams",
entity: "User",
owner: false,
pivotTable: "team_users",
},
created_at: {
@@ -5288,8 +5330,9 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: true,
pivotTable: "team_users",
mappedBy: "users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
@@ -5356,6 +5399,8 @@ describe("Entity builder", () => {
reference: "m:n",
name: "users",
entity: "User",
owner: false,
mappedBy: "teams",
pivotTable: "team_users",
},
created_at: {
@@ -5500,6 +5545,188 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
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",
mappedBy: "users",
},
@@ -5568,11 +5795,8 @@ describe("Entity builder", () => {
reference: "m:n",
name: "users",
entity: "User",
owner: true,
pivotTable: "team_users",
/**
* The other side should be inversed in order for Mikro ORM
* to work. Both sides cannot have mappedBy.
*/
inversedBy: "teams",
},
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", () => {
const team = model.define("team", {
id: model.number(),
@@ -5816,9 +5856,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
const Team = entityBuilder(team)
const User = entityBuilder(user)
const Team = toMikroORMEntity(team)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -5886,19 +5925,17 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: false,
pivotTable: "team_users",
/**
* The other side should be inversed in order for Mikro ORM
* to work. Both sides cannot have mappedBy.
*/
inversedBy: "users",
mappedBy: "users",
},
activeTeams: {
reference: "m:n",
name: "activeTeams",
entity: "Team",
owner: false,
pivotTable: "team_users",
inversedBy: "activeTeamsUsers",
mappedBy: "activeTeamsUsers",
},
created_at: {
reference: "scalar",
@@ -5965,15 +6002,17 @@ describe("Entity builder", () => {
reference: "m:n",
name: "users",
entity: "User",
owner: true,
pivotTable: "team_users",
mappedBy: "teams",
inversedBy: "teams",
},
activeTeamsUsers: {
reference: "m:n",
name: "activeTeamsUsers",
entity: "User",
owner: true,
pivotTable: "team_users",
mappedBy: "activeTeams",
inversedBy: "activeTeams",
},
created_at: {
reference: "scalar",
@@ -6086,8 +6125,9 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: true,
pivotTable: "platform.team_users",
mappedBy: "users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
@@ -6155,6 +6195,8 @@ describe("Entity builder", () => {
reference: "m:n",
name: "users",
entity: "User",
owner: false,
mappedBy: "teams",
pivotTable: "platform.team_users",
},
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", () => {
const team = model.define("team", {
id: model.number(),
name: model.text(),
users: model.manyToMany(() => user, {
pivotTable: "users_teams",
}),
users: model.manyToMany(() => user),
})
const user = model.define("user", {
@@ -6270,8 +6467,9 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: true,
pivotTable: "users_teams",
mappedBy: "users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
@@ -6337,7 +6535,9 @@ describe("Entity builder", () => {
users: {
reference: "m:n",
name: "users",
owner: false,
entity: "User",
mappedBy: "teams",
pivotTable: "users_teams",
},
created_at: {
@@ -6454,7 +6654,6 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: false,
isForeignKey: true,
},
user: {
reference: "scalar",
@@ -6473,7 +6672,6 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "team_id",
nullable: false,
isForeignKey: true,
},
team: {
reference: "scalar",
@@ -6549,8 +6747,9 @@ describe("Entity builder", () => {
reference: "m:n",
name: "teams",
entity: "Team",
owner: true,
pivotEntity: "TeamUsers",
mappedBy: "users",
inversedBy: "users",
},
created_at: {
reference: "scalar",
@@ -6617,6 +6816,8 @@ describe("Entity builder", () => {
reference: "m:n",
name: "users",
entity: "User",
owner: false,
mappedBy: "teams",
pivotEntity: "TeamUsers",
},
created_at: {
@@ -56,6 +56,14 @@ export type ManyToManyOptions = RelationshipOptions &
* @ignore
*/
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 { DmlEntity } from "../entity"
import { parseEntityName } from "./entity-builder/parse-entity-name"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
import { setGraphQLRelationship } from "./graphql-builder/set-relationship"
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
export function generateGraphQLFromEntity<T extends DmlEntity<any, any>>(
entity: T
@@ -1,4 +1,5 @@
import type {
Constructor,
DMLSchema,
EntityConstructor,
IDmlEntity,
@@ -6,10 +7,11 @@ import type {
PropertyType,
} from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity"
import { DuplicateIdPropertyError } from "../errors"
import { IdProperty } from "../properties/id"
import { DuplicateIdPropertyError } from "../errors"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { applySearchable } from "./entity-builder/apply-searchable"
import { defineProperty } from "./entity-builder/define-property"
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
* to Mikro ORM entities.
*/
export function createMikrORMEntity() {
function createMikrORMEntity() {
/**
* The following property is used to track many to many relationship
* 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
* - [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
const MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> = {}
let 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
* DML entity.
*/
return function createEntity<T extends DmlEntity<any, any>>(
entity: T
): Infer<T> {
function createEntity<T extends DmlEntity<any, any>>(entity: T): Infer<T> {
class MikroORMEntity {}
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
const { modelName, tableName } = parseEntityName(entity)
if (ENTITIES[modelName]) {
return ENTITIES[modelName] as Infer<T>
}
/**
* Assigning name to the class constructor, so that it matches
@@ -80,11 +83,14 @@ export function createMikrORMEntity() {
hasIdAlreadyDefined = true
}
defineProperty(MikroORMEntity, name, property as PropertyType<any>)
defineProperty(MikroORMEntity, property as PropertyType<any>, {
propertyName: name,
tableName,
})
applyIndexes(MikroORMEntity, tableName, field)
applySearchable(MikroORMEntity, field)
} else {
defineRelationship(MikroORMEntity, field, cascades, context)
defineRelationship(MikroORMEntity, entity, field, cascades, context)
applySearchable(MikroORMEntity, field)
}
})
@@ -94,11 +100,30 @@ export function createMikrORMEntity() {
/**
* Converting class to a MikroORM entity
*/
return Entity({ tableName })(
const RegisteredEntity = Entity({ tableName })(
Filter(mikroOrmSoftDeletableFilterOptions)(MikroORMEntity)
) 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
@@ -111,7 +136,7 @@ export const toMikroORMEntity = <T>(
let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) {
mikroOrmEntity = createMikrORMEntity()(entity)
mikroOrmEntity = mikroORMEntityBuilder(entity)
}
return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T
@@ -123,11 +148,9 @@ export const toMikroORMEntity = <T>(
* @param entities
*/
export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
const entityBuilder = createMikrORMEntity()
return entities.map((entity) => {
if (DmlEntity.isDmlEntity(entity)) {
return entityBuilder(entity)
return mikroORMEntityBuilder(entity)
}
return entity
@@ -16,6 +16,7 @@ import {
Utils,
} from "@mikro-orm/core"
import { PrimaryKeyModifier } from "../../properties/primary-key"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
/**
* DML entity data types to PostgreSQL data types via
@@ -64,7 +65,8 @@ const PROPERTY_TYPES: {
const SPECIAL_PROPERTIES: {
[propertyName: string]: (
MikroORMEntity: EntityConstructor<any>,
field: PropertyMetadata
field: PropertyMetadata,
tableName: string
) => void
} = {
created_at: (MikroORMEntity, field) => {
@@ -88,6 +90,21 @@ const SPECIAL_PROPERTIES: {
onUpdate: () => new Date(),
})(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(
MikroORMEntity: EntityConstructor<any>,
propertyName: string,
property: PropertyType<any>
property: PropertyType<any>,
{ tableName, propertyName }: { tableName: string; propertyName: string }
) {
const field = property.parse(propertyName)
/**
@@ -112,10 +129,11 @@ export function defineProperty(
}
if (SPECIAL_PROPERTIES[field.fieldName]) {
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field)
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field, tableName)
return
}
if (field.dataType.name === "bigNumber") {
/**
* Defining an big number property
* 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
* dataType.
*/
if (field.dataType.name === "bigNumber") {
MikroOrmBigNumberProperty({
nullable: field.nullable,
fieldName: field.fieldName,
@@ -16,17 +16,77 @@ import {
rel,
} from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common"
import { ForeignKey } from "../../../dal/mikro-orm/decorators/foreign-key"
import { DmlEntity } from "../../entity"
import { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
import { parseEntityName } from "./parse-entity-name"
type Context = {
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.
* @param MikroORMEntity
@@ -39,6 +99,7 @@ function validateManyToManyRelationshipWithoutMappedBy({
relationship,
relatedEntity,
relatedModelName,
entity,
}: {
MikroORMEntity: EntityConstructor<any>
relationship: RelationshipMetadata
@@ -47,44 +108,25 @@ function validateManyToManyRelationshipWithoutMappedBy({
any
>
relatedModelName: string
entity: DmlEntity<any, 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 potentialOtherSides = 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
const [, potentialOtherSide] = retrieveOtherSideRelationshipManyToMany({
relationship,
relatedEntity,
relatedModelName,
entity,
})
if (!relatedEntity) {
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) {
if (!potentialOtherSide) {
throw new Error(
`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(
MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>,
@@ -194,8 +237,7 @@ export function defineBelongsToRelationship(
return
}
this[relationship.name] ??= this[foreignKeyName]
this[foreignKeyName] ??= this[relationship.name]?.id
this[foreignKeyName] ??= this[relationship.name]?.id ?? null
}
/**
@@ -222,7 +264,6 @@ export function defineBelongsToRelationship(
nullable: relationship.nullable,
onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
if (DmlManyToMany.isManyToMany(otherSideRelation)) {
Property({
@@ -239,6 +280,13 @@ export function defineBelongsToRelationship(
})(MikroORMEntity.prototype, relationship.name)
}
const { tableName } = parseEntityName(entity)
applyEntityIndexes(MikroORMEntity, tableName, [
{
on: [foreignKeyName],
where: "deleted_at IS NULL",
},
])
applyForeignKeyAssignationHooks(foreignKeyName)
return
}
@@ -270,7 +318,14 @@ export function defineBelongsToRelationship(
nullable: relationship.nullable,
persist: false,
})(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
const { tableName } = parseEntityName(entity)
applyEntityIndexes(MikroORMEntity, tableName, [
{
on: [foreignKeyName],
where: "deleted_at IS NULL",
},
])
applyForeignKeyAssignationHooks(foreignKeyName)
return
@@ -289,6 +344,7 @@ export function defineBelongsToRelationship(
*/
export function defineManyToManyRelationship(
MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>,
@@ -296,59 +352,60 @@ export function defineManyToManyRelationship(
>,
{
relatedModelName,
relatedTableName,
pgSchema,
}: { relatedModelName: string; pgSchema: string | undefined },
}: {
relatedModelName: string
pgSchema: string | undefined
relatedTableName: string
},
{ MANY_TO_MANY_TRACKED_RELATIONS }: Context
) {
let mappedBy = relationship.mappedBy
let inversedBy: undefined | string
let pivotEntityName: 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
*/
if (mappedBy) {
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
if (!otherSideRelation) {
if (!otherSideRelationship) {
throw new Error(
`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(
`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 {
validateManyToManyRelationshipWithoutMappedBy({
MikroORMEntity,
relationship,
relatedEntity,
relatedModelName,
entity,
})
}
MANY_TO_MANY_TRACKED_RELATIONS[
`${MikroORMEntity.name}.${relationship.name}`
] = true
/**
* Validating pivot entity when it is defined and computing
* its name
@@ -371,6 +428,10 @@ export function defineManyToManyRelationship(
}
if (!pivotEntityName) {
const { tableName } = parseEntityName(entity)
let tableNameWithoutSchema: string
let relatedTableNameWithoutSchema: string
/**
* 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.
* - 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 =
relationship.options.pivotTable ??
[MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()]
otherSideRelationship.parse("").options.pivotTable ??
[tableNameWithoutSchema, relatedTableNameWithoutSchema]
.sort()
.map((token, index) => {
if (index === 1) {
return pluralize(camelToSnakeCase(token))
return pluralize(token)
}
return camelToSnakeCase(token)
return token
})
.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({
owner: isOwner,
entity: relatedModelName,
...(pivotTableName
? {
@@ -402,8 +501,9 @@ export function defineManyToManyRelationship(
}
: {}),
...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}),
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
...({ [mappedByProp]: mappedByPropValue } as any),
...(joinColumn ? { joinColumn } : {}),
...(inverseJoinColumn ? { inverseJoinColumn } : {}),
})(MikroORMEntity.prototype, relationship.name)
}
@@ -412,6 +512,7 @@ export function defineManyToManyRelationship(
*/
export function defineRelationship(
MikroORMEntity: EntityConstructor<any>,
entity: DmlEntity<any, any>,
relationship: RelationshipMetadata,
cascades: EntityCascades<string[]>,
context: Context
@@ -474,6 +575,7 @@ export function defineRelationship(
case "belongsTo":
defineBelongsToRelationship(
MikroORMEntity,
entity,
relationship,
relatedEntity,
relatedEntityInfo
@@ -482,6 +584,7 @@ export function defineRelationship(
case "manyToMany":
defineManyToManyRelationship(
MikroORMEntity,
entity,
relationship,
relatedEntity,
relatedEntityInfo,
@@ -3,7 +3,6 @@ import {
EntityIndex,
PropertyMetadata,
} from "@medusajs/types"
import { MetadataStorage } from "@mikro-orm/core"
import { createPsqlIndexStatementHelper } from "../../../common"
import { validateIndexFields } from "../mikro-orm/build-indexes"
@@ -38,8 +37,7 @@ export function applyEntityIndexes(
tableName: string,
entityIndexes: EntityIndex[] = []
) {
const foreignKeyIndexes = applyForeignKeyIndexes(MikroORMEntity)
const indexes = [...entityIndexes, ...foreignKeyIndexes]
const indexes = [...entityIndexes]
indexes.forEach((index) => {
validateIndexFields(MikroORMEntity, index)
@@ -55,29 +53,3 @@ export function applyEntityIndexes(
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,
} from "@mikro-orm/core"
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 { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types"
@@ -28,6 +31,7 @@ describe("EntityBuilder | enum", () => {
beforeEach(async () => {
MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const user = model.define("user", {
id: model.id().primaryKey(),
@@ -1,6 +1,9 @@
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
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 { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types"
@@ -24,6 +27,7 @@ describe("hasOne - belongTo", () => {
beforeEach(async () => {
MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", {
id: model.id().primaryKey(),
@@ -1,12 +1,17 @@
import { join } from "path"
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
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 { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
import { EntityConstructor } from "@medusajs/types"
import { pgGodCredentials } from "../utils"
import { FileSystem } from "../../../common"
import { join } from "path"
jest.setTimeout(30000)
export const fileSystem = new FileSystem(
join(__dirname, "../../integration-tests-migrations-many-to-many")
@@ -27,6 +32,7 @@ describe("manyToMany - manyToMany", () => {
beforeEach(async () => {
MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", {
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", {
id: model.id().primaryKey(),
name: model.text(),
users: model.manyToMany(() => user, {
pivotEntity: () => squad,
pivot_table: "team_users",
}),
})
const squad = model.define("teamUsers", {
const user = model.define("user", {
id: model.id().primaryKey(),
user: model.belongsTo(() => user, { mappedBy: "squads" }),
squad: model.belongsTo(() => team, { mappedBy: "users" }),
username: model.text(),
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", {
id: model.id().primaryKey(),
username: model.text(),
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
try {
;[User, Squad, Team] = toMikroOrmEntities([user, squad, team])
;[User, Team] = toMikroOrmEntities([user, team])
} catch (e) {
error = e
}
expect(error).toBeTruthy()
expect(error.message).toEqual(
'Invalid relationship reference for "User.squads". "mappedBy" should be defined on one side or the other.'
expect(error).toBeDefined()
expect(error?.message).toEqual(
'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`, () => {
mikroORMEntityBuilder.clear()
const team = model.define("team", {
id: model.id().primaryKey(),
name: model.text(),
@@ -248,7 +319,7 @@ describe("manyToMany - manyToMany", () => {
expect(error).toBeTruthy()
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 { 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 {
CustomTsMigrationGenerator,
@@ -30,6 +33,7 @@ describe("manyToOne - belongTo", () => {
beforeEach(async () => {
MetadataStorage.clear()
mikroORMEntityBuilder.clear()
const team = model.define("team", {
id: model.id().primaryKey(),
@@ -4,9 +4,11 @@ import { MetadataStorage } from "@mikro-orm/core"
import { Migrations } from "../../index"
import { FileSystem } from "../../../common"
import { DmlEntity, model } from "../../../dml"
import { DmlEntity, mikroORMEntityBuilder, model } from "../../../dml"
import { defineMikroOrmCliConfig } from "../../../modules-sdk"
jest.setTimeout(30000)
const DB_HOST = process.env.DB_HOST ?? "localhost"
const DB_USERNAME = process.env.DB_USERNAME ?? ""
const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
@@ -29,7 +31,8 @@ describe("Generate migrations", () => {
afterEach(async () => {
await fs.cleanup()
MetadataStorage.clear()
}, 300 * 1000)
mikroORMEntityBuilder.clear()
})
test("generate migrations for a single entity", async () => {
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, {
models: [FulfillmentSet],
alias: [
@@ -1,4 +1,4 @@
import { DAL, FindConfig } from "@medusajs/types"
import { DAL, FindConfig, InferRepositoryReturnType } from "@medusajs/types"
import { deduplicate, isObject } from "../common"
import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter"
@@ -10,17 +10,19 @@ type FilterFlags = {
withDeleted?: boolean
}
export function buildQuery<T = any, TDto = any>(
export function buildQuery<const T = any>(
filters: Record<string, any> = {},
config: FindConfig<TDto> & { primaryKeyFields?: string | string[] } = {}
config: FindConfig<InferRepositoryReturnType<T>> & {
primaryKeyFields?: string | string[]
} = {}
): Required<DAL.FindOptions<T>> {
const where: DAL.FilterQuery<T> = {}
const where = {} as DAL.FilterQuery<T>
const filterFlags: FilterFlags = {}
buildWhere(filters, where, filterFlags)
delete config.primaryKeyFields
const findOptions: DAL.OptionsQuery<T, any> = {
const findOptions: DAL.FindOptions<T>["options"] = {
populate: deduplicate(config.relations ?? []),
fields: config.select as string[],
limit: (Number.isSafeInteger(config.take) && config.take) || undefined,
@@ -28,7 +30,9 @@ export function buildQuery<T = any, TDto = any>(
}
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) {
@@ -50,7 +54,7 @@ export function buildQuery<T = any, TDto = any>(
Object.assign(findOptions, config.options)
}
return { where, options: findOptions }
return { where, options: findOptions } as Required<DAL.FindOptions<T>>
}
function buildWhere(
@@ -314,6 +314,7 @@ ${serviceBObj.module}: {
const isModuleAPrimaryKeyValid =
moduleAPrimaryKeys.includes(serviceAPrimaryKey)
if (!isModuleAPrimaryKeyValid) {
throw new Error(
`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 {
camelToSnakeCase,
deduplicate,
getCallerFilePath,
isObject,
lowerCaseFirst,
@@ -165,29 +164,40 @@ export function defineJoinerConfig(
}
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(
serviceName,
Object.fromEntries(modelDefinitions)
)
primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig as any) as any[])
.filter((linkableConfig) => isObject(linkableConfig))
.map((linkableConfig) => {
// @ts-ignore
return linkableConfig.primaryKey
})
})
return Object.values(
entityLinkConfig as Record<string, { primaryKey: string }>
)
.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
return {
serviceName,
primaryKeys: primaryKeys ?? ["id"],
primaryKeys,
schema,
linkableKeys: linkableKeys,
alias: [
@@ -342,26 +352,44 @@ export function buildLinkableKeysFromMikroOrmObjects(
export function buildLinkConfigFromModelObjects<
const ServiceName extends string,
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>
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
}
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)] ??= {
toJSON: function () {
const linkables = Object.entries(this)
.filter(([name]) => name !== "toJSON")
.map(([, object]) => object)
const lastIndex = linkables.length - 1
return linkables[lastIndex]
return linkables[0]
},
})
/**
* Build all linkable properties for the model
*/
for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) {
continue
@@ -378,10 +406,46 @@ export function buildLinkConfigFromModelObjects<
primaryKey: property,
serviceName,
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>
@@ -2,9 +2,9 @@ import {
BaseFilterable,
Context,
FilterQuery,
FilterQuery as InternalFilterQuery,
FindConfig,
InferEntityType,
FilterQuery as InternalFilterQuery,
ModulesSdkTypes,
PerformedActions,
UpsertWithReplaceConfig,
@@ -62,7 +62,7 @@ export function MedusaInternalService<
}
static applyFreeTextSearchFilter(
filters: FilterQuery,
filters: FilterQuery & { q?: string },
config: FindConfig<any>
): void {
if (isDefined(filters?.q)) {
@@ -49,15 +49,22 @@ export function Module<
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) {
linkable = buildLinkConfigFromModelObjects<ServiceName, ModelObjects>(
serviceName,
modelObjects
modelObjects,
linkableKeys
) as Linkable
} else {
linkable = buildLinkConfigFromLinkableKeys(
serviceName,
service.prototype.__joinerConfig().linkableKeys
linkableKeys
) as Linkable
}
}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderClaim } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderClaim>(
export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderClaim
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderExchange } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderExchange>(
export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderExchange
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Order } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Order>(
export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Order
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Return } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Return>(
export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Return
) {}
@@ -118,7 +118,7 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
klass.prototype.findAndCount = async function findAndCount(
this: any,
findOptions: DAL.FindOptions<T> = { where: {} },
findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context: Context = {}
): Promise<[T[], number]> {
const manager = this.getActiveManager(context)
@@ -1,3 +1,5 @@
import { kebabCase } from "@medusajs/framework/utils"
export const productCategoriesData = [
{
id: "category-0",
@@ -25,7 +27,12 @@ export const productCategoriesData = [
name: "category 1 b 1",
parent_category_id: "category-1-b",
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const productCategoriesRankData = [
{
@@ -64,7 +71,12 @@ export const productCategoriesRankData = [
parent_category_id: "category-0-0",
rank: 2,
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const eletronicsCategoriesData = [
{
@@ -188,4 +200,9 @@ export const eletronicsCategoriesData = [
name: "Mini Models",
parent_category_id: "iphones",
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
@@ -2,16 +2,19 @@ export const categoriesData = [
{
id: "category-0",
name: "category 0",
handle: "category-0",
parent_category_id: null,
},
{
id: "category-1",
name: "category 1",
handle: "category-1",
parent_category_id: "category-0",
},
{
id: "category-1-a",
name: "category 1 a",
handle: "category-1-a",
parent_category_id: "category-1",
},
]
@@ -1,10 +1,11 @@
import { ProductTypes } from "@medusajs/framework/types"
import { ProductStatus } from "@medusajs/framework/utils"
import { Image } from "@models"
import { ProductStatus, toHandle } from "@medusajs/framework/utils"
import { ProductImage } from "@models"
import faker from "faker"
export const buildProductOnlyData = ({
title,
handle,
description,
subtitle,
is_giftcard,
@@ -14,6 +15,7 @@ export const buildProductOnlyData = ({
status,
}: {
title?: string
handle?: string
description?: string
subtitle?: string
is_giftcard?: boolean
@@ -22,15 +24,17 @@ export const buildProductOnlyData = ({
images?: { id?: string; url: string }[]
status?: ProductStatus
} = {}) => {
title ??= faker.commerce.productName()
return {
title: title ?? faker.commerce.productName(),
title: title as string,
handle: handle ?? toHandle(title!),
description: description ?? faker.commerce.productName(),
subtitle: subtitle ?? faker.commerce.productName(),
is_giftcard: is_giftcard ?? false,
discountable: discountable ?? true,
thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[],
images: (images ?? []) as ProductImage[],
}
}
@@ -48,7 +52,7 @@ export const buildProductAndRelationsData = ({
options,
variants,
collection_id,
}: Partial<ProductTypes.CreateProductDTO>) => {
}: Partial<ProductTypes.CreateProductDTO> & { tags: { value: string }[] }) => {
const defaultOptionTitle = "test-option"
const defaultOptionValue = "test-value"
@@ -60,7 +64,7 @@ export const buildProductAndRelationsData = ({
discountable: discountable ?? true,
thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[],
images: (images ?? []) as ProductImage[],
type_id,
tags: tags ?? [{ value: "tag-1" }],
collection_id,
@@ -4,6 +4,7 @@ export const productsData = [
{
id: "test-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
tags: [
{
@@ -15,6 +16,7 @@ export const productsData = [
{
id: "test-2",
title: "product",
handle: "product",
status: ProductStatus.PUBLISHED,
tags: [
{
@@ -26,6 +28,7 @@ export const productsData = [
{
id: "test-3",
title: "product 3",
handle: "product-3",
status: ProductStatus.PUBLISHED,
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 {
Image,
@@ -10,6 +14,7 @@ import {
} from "@models"
import ProductOption from "../../../src/models/product-option"
import { InferEntityType } from "@medusajs/types"
export * from "./data/create-product"
@@ -24,7 +29,7 @@ export async function createProductAndTags(
}[]
) {
const products: any[] = data.map((productData) => {
return manager.create(Product, productData)
return manager.create(toMikroORMEntity(Product), productData)
})
await manager.persistAndFlush(products)
@@ -42,7 +47,7 @@ export async function createProductAndTypes(
}[]
) {
const products: any[] = data.map((productData) => {
return manager.create(Product, productData)
return manager.create(toMikroORMEntity(Product), productData)
})
await manager.persistAndFlush(products)
@@ -55,7 +60,7 @@ export async function createProductVariants(
data: any[]
) {
const variants: any[] = data.map((variantsData) => {
return manager.create(ProductVariant, variantsData)
return manager.create(toMikroORMEntity(ProductVariant), variantsData)
})
await manager.persistAndFlush(variants)
@@ -72,7 +77,10 @@ export async function createCollections(
}[]
) {
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)
@@ -88,7 +96,7 @@ export async function createTypes(
}[]
) {
const types: any[] = typesData.map((typesData) => {
return manager.create(ProductType, typesData)
return manager.create(toMikroORMEntity(ProductType), typesData)
})
await manager.persistAndFlush(types)
@@ -112,7 +120,7 @@ export async function createOptions(
}[]
) {
const options: any[] = optionsData.map((option) => {
return manager.create(ProductOption, option)
return manager.create(toMikroORMEntity(ProductOption), option)
})
await manager.persistAndFlush(options)
@@ -125,7 +133,7 @@ export async function createImages(
imagesData: string[]
) {
const images: any[] = imagesData.map((img) => {
return manager.create(Image, { url: img })
return manager.create(toMikroORMEntity(Image), { url: img })
})
await manager.persistAndFlush(images)
@@ -135,8 +143,8 @@ export async function createImages(
export async function assignCategoriesToProduct(
manager: SqlEntityManager,
product: Product,
categories: ProductCategory[]
product: InferEntityType<Product>,
categories: InferEntityType<ProductCategory>[]
) {
product.categories.add(categories)
@@ -175,7 +175,7 @@ moduleIntegrationTestRunner<Service>({
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",
parent_category_id: "category-1",
category_children: [],
rank: 0,
},
{
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
category_children: [
expect.objectContaining({
id: "category-1-b-1",
@@ -220,7 +222,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -235,8 +237,10 @@ moduleIntegrationTestRunner<Service>({
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
rank: 1,
parent_category: expect.objectContaining({
id: "high-performance",
rank: 1,
parent_category_id: "gaming-laptops",
handle: "high-performance-gaming-laptops",
mpath:
@@ -245,6 +249,7 @@ moduleIntegrationTestRunner<Service>({
id: "gaming-laptops",
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops",
rank: 0,
parent_category_id: "laptops",
parent_category: expect.objectContaining({
id: "laptops",
@@ -282,7 +287,7 @@ moduleIntegrationTestRunner<Service>({
include_descendants_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -296,6 +301,7 @@ moduleIntegrationTestRunner<Service>({
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops",
rank: 0,
category_children: [
expect.objectContaining({
id: "budget-gaming",
@@ -304,6 +310,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops",
category_children: [],
rank: 0,
}),
expect.objectContaining({
id: "high-performance",
@@ -311,6 +318,7 @@ moduleIntegrationTestRunner<Service>({
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance",
parent_category_id: "gaming-laptops",
rank: 1,
category_children: expect.arrayContaining([
expect.objectContaining({
id: "vr-ready",
@@ -319,6 +327,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.vr-ready",
parent_category_id: "high-performance",
category_children: [],
rank: 0,
}),
expect.objectContaining({
id: "4k-gaming",
@@ -327,6 +336,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
category_children: [],
rank: 1,
}),
]),
}),
@@ -407,7 +417,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -421,12 +431,15 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
rank: 0,
id: "category-0",
handle: "category-0",
mpath: "category-0",
@@ -440,17 +453,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
},
@@ -465,7 +481,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -475,17 +491,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
category_children: [],
@@ -495,17 +514,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
category_children: [
@@ -514,6 +536,7 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b-1",
mpath: "category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
rank: 0,
}),
],
},
@@ -879,6 +902,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category",
handle: "new-category",
parent_category_id: null,
},
])
@@ -904,11 +928,13 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category",
handle: "new-category",
parent_category_id: null,
rank: 0,
},
{
name: "New Category 2",
handle: "new-category-2",
parent_category_id: null,
},
])
@@ -932,6 +958,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category 2.1",
handle: "new-category-2-1",
parent_category_id: productCategoryNew.id,
},
])
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductCategory } from "@models"
import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductCollection } from "@models"
import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
@@ -47,11 +50,13 @@ moduleIntegrationTestRunner<IProductModuleService>({
{
id: "test-1",
title: "collection 1",
handle: "collection-1",
products: [productOne],
},
{
id: "test-2",
title: "collection",
handle: "collection",
products: [productTwo],
},
]
@@ -1,5 +1,9 @@
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 { moduleIntegrationTestRunner } from "@medusajs/test-utils"
@@ -16,25 +20,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
optionOne = testManager.create(ProductOption, {
optionOne = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-1",
title: "option 1",
product: productOne,
})
optionTwo = testManager.create(ProductOption, {
optionTwo = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-2",
title: "option 1",
product: productTwo,
@@ -198,7 +204,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
it("should return requested attributes when requested through config", async () => {
const option = await service.retrieveProductOption(optionOne.id, {
select: ["id", "product.title"],
select: ["id", "product.handle", "product.title"],
relations: ["product"],
})
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductTag } from "@models"
import {
@@ -35,25 +36,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
tagOne = testManager.create(ProductTag, {
tagOne = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-1",
value: "tag 1",
products: [productOne],
})
tagTwo = testManager.create(ProductTag, {
tagTwo = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-2",
value: "tag",
products: [productTwo],
@@ -1,7 +1,7 @@
import { IProductModuleService } from "@medusajs/framework/types"
import { ProductType } from "@models"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules } from "@medusajs/framework/utils"
import { Modules, toMikroORMEntity } from "@medusajs/framework/utils"
jest.setTimeout(30000)
@@ -15,12 +15,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
typeOne = testManager.create(ProductType, {
typeOne = testManager.create(toMikroORMEntity(ProductType), {
id: "type-1",
value: "type 1",
})
typeTwo = testManager.create(ProductType, {
typeTwo = testManager.create(toMikroORMEntity(ProductType), {
id: "type-2",
value: "type",
})
@@ -125,7 +125,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
productCategoryTwo = categories[1]
productOne = service.createProducts({
id: "product-1",
title: "product 1",
status: ProductStatus.PUBLISHED,
options: [
@@ -136,7 +135,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
],
variants: [
{
id: "variant-1",
title: "variant 1",
options: { "opt-title": "val-1" },
},
@@ -144,12 +142,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
productTwo = service.createProducts({
id: "product-2",
title: "product 2",
status: ProductStatus.PUBLISHED,
categories: [{ id: productCategoryOne.id }],
collection_id: productCollectionOne.id,
tags: [{ id: tags[0].id }],
category_ids: [productCategoryOne.id],
tag_ids: [tags[0].id],
options: [
{
title: "size",
@@ -162,7 +159,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
],
variants: [
{
id: "variant-2",
title: "variant 2",
options: {
size: "large",
@@ -170,7 +166,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
},
},
{
id: "variant-3",
title: "variant 3",
options: {
size: "small",
@@ -389,11 +384,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
const existingVariant1 = product.variants.find(
(v) => v.title === "new variant 1"
)
)!
const existingVariant2 = product.variants.find(
(v) => v.title === "new variant 2"
)
)!
await service.upsertProductVariants([
{
@@ -793,10 +788,14 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
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, {
variants: [
{
id: "variant-3",
id: variantToUpdate.id,
options: { size: "small", color: "blue" },
},
],
@@ -7,12 +7,17 @@ import {
createProductVariants,
} from "../__fixtures__/product"
import { IProductModuleService, ProductDTO } from "@medusajs/framework/types"
import {
InferEntityType,
IProductModuleService,
ProductDTO,
} from "@medusajs/framework/types"
import {
Module,
Modules,
ProductStatus,
kebabCase,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
@@ -27,7 +32,7 @@ import {
variantsData,
} from "../__fixtures__/product/data"
jest.setTimeout(30000)
jest.setTimeout(300000)
type Service = IProductModuleService & {
productService_: ProductService
@@ -50,17 +55,19 @@ moduleIntegrationTestRunner<Service>({
service: ProductModuleService,
}).linkable
expect(Object.keys(linkable)).toEqual([
expect(Object.keys(linkable)).toHaveLength(8)
expect(Object.keys(linkable)).toEqual(
expect.arrayContaining([
"product",
"productVariant",
"productOption",
"productOptionValue",
"productType",
"productImage",
"productTag",
"productCollection",
"productCategory",
])
)
Object.keys(linkable).forEach((key) => {
delete linkable[key].toJSON
@@ -119,15 +126,6 @@ moduleIntegrationTestRunner<Service>({
field: "productType",
},
},
productImage: {
id: {
linkable: "product_image_id",
entity: "ProductImage",
primaryKey: "id",
serviceName: "product",
field: "productImage",
},
},
productTag: {
id: {
linkable: "product_tag_id",
@@ -160,16 +158,17 @@ moduleIntegrationTestRunner<Service>({
describe("Product Service", () => {
let testManager: SqlEntityManager
let products!: Product[]
let productOne: Product
let categories!: ProductCategory[]
let products!: InferEntityType<typeof Product>[]
let productOne: InferEntityType<typeof Product>
let categories!: InferEntityType<typeof ProductCategory>[]
describe("retrieve", () => {
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
@@ -243,9 +242,10 @@ moduleIntegrationTestRunner<Service>({
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
@@ -435,23 +435,23 @@ moduleIntegrationTestRunner<Service>({
})
describe("relation: categories", () => {
let workingProduct: Product
let workingCategory: ProductCategory
let workingProduct: InferEntityType<typeof Product>
let workingCategory: InferEntityType<typeof ProductCategory>
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
products = await createProductAndTags(testManager, productsData)
workingProduct = products.find((p) => p.id === "test-1") as Product
workingProduct = products.find((p) => p.id === "test-1")!
categories = []
for (const entry of categoriesData) {
categories.push((await categoryService.create([entry]))[0])
}
workingCategory = (await testManager.findOne(
ProductCategory,
toMikroORMEntity(ProductCategory),
"category-1"
)) as ProductCategory
))!
workingProduct = await assignCategoriesToProduct(
testManager,
@@ -477,9 +477,7 @@ moduleIntegrationTestRunner<Service>({
}
)
const product = products.find(
(p) => p.id === workingProduct.id
) as unknown as Product
const product = products.find((p) => p.id === workingProduct.id)!
expect(product).toEqual(
expect.objectContaining({
@@ -534,32 +532,34 @@ moduleIntegrationTestRunner<Service>({
})
describe("relation: collections", () => {
let workingProduct: Product
let workingProductTwo: Product
let workingCollection: ProductCollection
let workingCollectionTwo: ProductCollection
let workingProduct: InferEntityType<typeof Product>
let workingProductTwo: InferEntityType<typeof Product>
let workingCollection: InferEntityType<typeof ProductCollection>
let workingCollectionTwo: InferEntityType<typeof ProductCollection>
const collectionData = [
{
id: "test-1",
title: "col 1",
handle: "col-1",
},
{
id: "test-2",
title: "col 2",
handle: "col-2",
},
]
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
await createCollections(testManager, collectionData)
workingCollection = (await testManager.findOne(
ProductCollection,
workingCollection = await testManager.findOne(
toMikroORMEntity(ProductCollection),
"test-1"
)) as ProductCollection
)
workingCollectionTwo = (await testManager.findOne(
ProductCollection,
toMikroORMEntity(ProductCollection),
"test-2"
)) as ProductCollection
))!
products = await createProductAndTags(testManager, [
{
@@ -575,10 +575,8 @@ moduleIntegrationTestRunner<Service>({
},
])
workingProduct = products.find((p) => p.id === "test-1") as Product
workingProductTwo = products.find(
(p) => p.id === "test-2"
) as Product
workingProduct = products.find((p) => p.id === "test-1")!
workingProductTwo = products.find((p) => p.id === "test-2")!
})
it("should filter by collection relation and scope fields", async () => {
@@ -588,7 +586,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: workingCollection.id,
},
{
select: ["title", "collection.title"],
select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"],
}
)
@@ -618,7 +621,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: [workingCollection.id, workingCollectionTwo.id],
},
{
select: ["title", "collection.title"],
select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"],
}
)
@@ -9,7 +9,6 @@ import {
ProductType,
ProductVariant,
} from "@models"
import ProductImage from "./models/product-image"
import { default as schema } from "./schema"
export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
@@ -20,7 +19,6 @@ export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
ProductOption,
ProductOptionValue,
ProductType,
ProductImage,
ProductTag,
ProductCollection,
ProductCategory,
@@ -82,6 +82,15 @@
"default": "0",
"mappedType": "integer"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"parent_category_id": {
"name": "parent_category_id",
"type": "text",
@@ -122,28 +131,42 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "product_category",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_category_parent_category_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_category_deleted_at",
"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",
@@ -247,13 +270,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_collection_deleted_at",
"columnNames": [],
"composite": 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",
@@ -334,13 +364,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_tag_deleted_at",
"columnNames": [],
"composite": 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",
@@ -377,7 +414,7 @@
},
"metadata": {
"name": "metadata",
"type": "json",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
@@ -421,13 +458,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_type_deleted_at",
"columnNames": [],
"composite": 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",
@@ -506,6 +550,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'draft'",
"enumItems": [
"draft",
"proposed",
@@ -595,24 +640,6 @@
"nullable": true,
"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": {
"name": "discountable",
"type": "boolean",
@@ -632,6 +659,33 @@
"nullable": true,
"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": {
"name": "created_at",
"type": "timestamptz",
@@ -663,28 +717,42 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "product",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_type_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_deleted_at",
"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",
@@ -698,19 +766,6 @@
],
"checks": [],
"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": {
"constraintName": "product_type_id_foreign",
"columnNames": [
@@ -723,6 +778,19 @@
"referencedTableName": "public.product_type",
"deleteRule": "set null",
"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,
"mappedType": "text"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -764,6 +823,15 @@
"nullable": true,
"mappedType": "json"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -801,13 +869,28 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_option_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_option_deleted_at",
"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",
@@ -856,15 +939,6 @@
"nullable": false,
"mappedType": "text"
},
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -874,6 +948,15 @@
"nullable": true,
"mappedType": "json"
},
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -911,13 +994,28 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_option_value_option_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_option_value_deleted_at",
"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",
@@ -975,6 +1073,25 @@
"nullable": true,
"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": {
"name": "created_at",
"type": "timestamptz",
@@ -1006,38 +1123,34 @@
"nullable": true,
"length": 6,
"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",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_image_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_image_deleted_at",
"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",
@@ -1310,39 +1423,39 @@
},
"weight": {
"name": "weight",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"length": {
"name": "length",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"height": {
"name": "height",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"width": {
"name": "width",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"metadata": {
"name": "metadata",
@@ -1409,13 +1522,52 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_variant_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_variant_deleted_at",
"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",
@@ -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 {
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 { model } from "@medusajs/framework/utils"
import Product from "./product"
const categoryHandleIndexName = "IDX_category_handle_unique"
const categoryHandleIndexStatement = createPsqlIndexStatementHelper({
name: categoryHandleIndexName,
tableName: "product_category",
columns: ["handle"],
unique: true,
where: "deleted_at IS NULL",
const ProductCategory = model
.define("ProductCategory", {
id: model.id({ prefix: "pcat" }).primaryKey(),
name: model.text().searchable(),
description: model.text().searchable().default(""),
handle: model.text().searchable(),
mpath: model.text(),
is_active: model.boolean().default(false),
is_internal: model.boolean().default(false),
rank: model.number().default(0),
metadata: model.json().nullable(),
parent_category: model
.belongsTo(() => ProductCategory, {
mappedBy: "category_children",
})
const categoryMpathIndexName = "IDX_product_category_path"
const categoryMpathIndexStatement = createPsqlIndexStatementHelper({
name: categoryMpathIndexName,
tableName: "product_category",
columns: ["mpath"],
.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,
where: "deleted_at IS NULL",
})
categoryMpathIndexStatement.MikroORMIndex()
categoryHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_category" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
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}`
}
}
},
{
name: "IDX_category_handle_unique",
on: ["handle"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductCategory
@@ -1,81 +1,23 @@
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
kebabCase,
Searchable,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const collectionHandleIndexName = "IDX_collection_handle_unique"
const collectionHandleIndexStatement = createPsqlIndexStatementHelper({
name: collectionHandleIndexName,
tableName: "product_collection",
columns: ["handle"],
const ProductCollection = model
.define("ProductCollection", {
id: model.id({ prefix: "pcol" }).primaryKey(),
title: model.text().searchable(),
handle: model.text(),
metadata: model.json().nullable(),
products: model.hasMany(() => Product, {
mappedBy: "collection",
}),
})
.indexes([
{
name: "IDX_collection_handle_unique",
on: ["handle"],
unique: true,
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
@@ -1,88 +1,26 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
ManyToOne,
OnInit,
PrimaryKey,
Property,
Rel,
} from "@mikro-orm/core"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const imageUrlIndexName = "IDX_product_image_url"
const imageUrlIndexStatement = createPsqlIndexStatementHelper({
name: imageUrlIndexName,
tableName: "image",
columns: ["url"],
const ProductImage = model
.define(
{ tableName: "image", name: "ProductImage" },
{
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,
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
@@ -1,87 +1,27 @@
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { ProductOption, ProductVariant } from "./index"
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
name: optionValueOptionIdIndexName,
tableName: "product_option_value",
columns: ["option_id", "value"],
const ProductOptionValue = model
.define("ProductOptionValue", {
id: model.id({ prefix: "optval" }).primaryKey(),
value: model.text(),
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,
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
@@ -1,93 +1,29 @@
import {
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 { model } from "@medusajs/framework/utils"
import { Product } from "./index"
import ProductOptionValue from "./product-option-value"
const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique"
const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({
name: optionProductIdTitleIndexName,
tableName: "product_option",
columns: ["product_id", "title"],
const ProductOption = model
.define("ProductOption", {
id: model.id({ prefix: "opt" }).primaryKey(),
title: model.text().searchable(),
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,
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
@@ -1,73 +1,25 @@
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const tagValueIndexName = "IDX_tag_value_unique"
const tagValueIndexStatement = createPsqlIndexStatementHelper({
name: tagValueIndexName,
tableName: "product_tag",
columns: ["value"],
const ProductTag = model
.define(
{ tableName: "product_tag", name: "ProductTag" },
{
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,
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
@@ -1,67 +1,22 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { Product } from "@models"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
const typeValueIndexName = "IDX_type_value_unique"
const typeValueIndexStatement = createPsqlIndexStatementHelper({
name: typeValueIndexName,
tableName: "product_type",
columns: ["value"],
const ProductType = model
.define("ProductType", {
id: model.id({ prefix: "ptyp" }).primaryKey(),
value: model.text().searchable(),
metadata: model.json().nullable(),
product: model.hasMany(() => Product, {
mappedBy: "type",
}),
})
.indexes([
{
name: "IDX_type_value_unique",
on: ["value"],
unique: true,
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
@@ -1,187 +1,69 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { Product, ProductOptionValue } from "@models"
const variantSkuIndexName = "IDX_product_variant_sku_unique"
const variantSkuIndexStatement = createPsqlIndexStatementHelper({
name: variantSkuIndexName,
tableName: "product_variant",
columns: ["sku"],
unique: true,
where: "deleted_at IS NULL",
const ProductVariant = model
.define("ProductVariant", {
id: model.id({ prefix: "variant" }).primaryKey(),
title: model.text().searchable(),
sku: model.text().searchable().nullable(),
barcode: model.text().searchable().nullable(),
ean: model.text().searchable().nullable(),
upc: model.text().searchable().nullable(),
allow_backorder: model.boolean().default(false),
manage_inventory: model.boolean().default(true),
hs_code: model.text().nullable(),
origin_country: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
weight: model.number().nullable(),
length: model.number().nullable(),
height: model.number().nullable(),
width: model.number().nullable(),
metadata: model.json().nullable(),
variant_rank: model.number().default(0).nullable(),
product: model
.belongsTo(() => Product, {
mappedBy: "variants",
})
const variantBarcodeIndexName = "IDX_product_variant_barcode_unique"
const variantBarcodeIndexStatement = createPsqlIndexStatementHelper({
name: variantBarcodeIndexName,
tableName: "product_variant",
columns: ["barcode"],
unique: true,
where: "deleted_at IS NULL",
})
const variantEanIndexName = "IDX_product_variant_ean_unique"
const variantEanIndexStatement = createPsqlIndexStatementHelper({
name: variantEanIndexName,
tableName: "product_variant",
columns: ["ean"],
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
@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,
.nullable(),
options: model.manyToMany(() => ProductOptionValue, {
pivotTable: "product_variant_option",
mappedBy: "variants",
joinColumn: "variant_id",
inverseJoinColumn: "option_value_id",
}),
})
options = new Collection<ProductOptionValue>(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_variant_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "variant")
this.product_id ??= this.product?.id ?? null
}
}
.indexes([
{
name: "IDX_product_variant_product_id",
on: ["product_id"],
unique: false,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_sku_unique",
on: ["sku"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_barcode_unique",
on: ["barcode"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_ean_unique",
on: ["ean"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_upc_unique",
on: ["upc"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductVariant
+73 -209
View File
@@ -1,222 +1,86 @@
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Filter,
Index,
ManyToMany,
ManyToOne,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model, ProductUtils } from "@medusajs/framework/utils"
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 ProductType from "./product-type"
import ProductImage from "./product-image"
import ProductOption from "./product-option"
import ProductVariant from "./product-variant"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
const productHandleIndexName = "IDX_product_handle_unique"
const productHandleIndexStatement = createPsqlIndexStatementHelper({
name: productHandleIndexName,
tableName: "product",
columns: ["handle"],
const Product = model
.define("Product", {
id: model.id({ prefix: "prod" }).primaryKey(),
title: model.text().searchable(),
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,
where: "deleted_at IS NULL",
})
const productTypeIndexName = "IDX_product_type_id"
const productTypeIndexStatement = createPsqlIndexStatementHelper({
name: productTypeIndexName,
tableName: "product",
columns: ["type_id"],
},
{
name: "IDX_product_type_id",
on: ["type_id"],
unique: false,
where: "deleted_at IS NULL",
})
const productCollectionIndexName = "IDX_product_collection_id"
const productCollectionIndexStatement = createPsqlIndexStatementHelper({
name: productCollectionIndexName,
tableName: "product",
columns: ["collection_id"],
},
{
name: "IDX_product_collection_id",
on: ["collection_id"],
unique: false,
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
@@ -1,30 +1,30 @@
import {
Context,
DAL,
InferEntityType,
ProductCategoryTransformOptions,
ProductTypes,
} from "@medusajs/framework/types"
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
import {
EntityDTO,
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
RequiredEntityData,
} from "@mikro-orm/core"
import { LoadStrategy, FindOptions as MikroOptions } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models"
import { UpdateCategoryInput } from "@types"
// eslint-disable-next-line max-len
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> {
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<
typeof ProductCategory
> {
buildFindOptions(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
familyOptions: ProductCategoryTransformOptions = {}
) {
const findOptions_ = { ...findOptions }
findOptions_.options ??= {
orderBy: { rank: "ASC" },
findOptions_.options ??= {}
findOptions_.options.orderBy = {
id: "ASC",
rank: "ASC",
...findOptions_.options.orderBy,
}
const fields = (findOptions_.options.fields ??= [])
@@ -69,17 +69,19 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
async find(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const productCategories = await manager.find(
ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>,
{ ...findOptions_.options } as MikroOptions<ProductCategory>
const productCategories = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
findOptions_.where,
{ ...findOptions_.options } as any // TODO
)
if (
@@ -102,7 +104,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
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)
for (const category of sortedCategories) {
@@ -122,12 +126,12 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
descendants?: boolean
ancestors?: boolean
},
productCategories: ProductCategory[],
findOptions: DAL.FindOptions<ProductCategory> & {
productCategories: InferEntityType<typeof ProductCategory>[],
findOptions: DAL.FindOptions<typeof ProductCategory> & {
serialize?: boolean
} = { where: {} },
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const { serialize = true } = findOptions
delete findOptions.serialize
@@ -173,17 +177,17 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
...findOptions.options,
limit: undefined,
offset: 0,
} as MikroOptions<ProductCategory>
} as MikroOptions<any>
delete where.id
delete where.mpath
delete where.parent_category_id
const categoriesInTree = serialize
? await this.serialize<ProductCategory[]>(
await manager.find(ProductCategory, where, options)
? await this.serialize<InferEntityType<typeof ProductCategory>[]>(
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]))
@@ -235,18 +239,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
async findAndCount(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<[ProductCategory[], number]> {
): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const [productCategories, count] = await manager.findAndCount(
ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>,
findOptions_.options as MikroOptions<ProductCategory>
)
const [productCategories, count] = (await manager.findAndCount(
ProductCategory.name,
findOptions_.where,
findOptions_.options as any
)) as unknown as [InferEntityType<typeof ProductCategory>[], number]
if (
!transformOptions.includeDescendantsTree &&
@@ -271,19 +275,23 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory, { id: ids }, {})
await manager.nativeDelete(ProductCategory.name, { id: ids }, {})
}
async softDelete(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOne(ProductCategory, {
const productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id,
})
@@ -306,11 +314,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async restore(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(ProductCategory, {
const productCategory = await manager.findOneOrFail<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id,
})
manager.assign(productCategory, { deleted_at: null })
@@ -327,12 +339,14 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOne(
ProductCategory,
const productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{ id },
{
populate: ["category_children"],
}
} as any // TODO
)
if (!productCategory) {
@@ -357,13 +371,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async create(
data: ProductTypes.CreateProductCategoryDTO[],
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
const siblingsCount = await manager.count(ProductCategory, {
const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
...entry,
}
const siblingsCount = await manager.count(ProductCategory.name, {
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.
let parentCategory: InferEntityType<typeof ProductCategory> | null =
null
const parentCategoryId =
categoryData.parent_category_id ?? categoryData.parent_category?.id
if (parentCategoryId) {
const parentCategory = await manager.findOne(
ProductCategory,
parentCategoryId
)
parentCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, parentCategoryId)
if (!parentCategory) {
throw new MedusaError(
@@ -393,14 +410,28 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
`Parent category with id: '${parentCategoryId}' does not exist`
)
}
categoryData.mpath = parentCategory.mpath
}
return manager.create(
ProductCategory,
categoryData as RequiredEntityData<ProductCategory>
const result = await manager.create<
InferEntityType<typeof 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(
data: UpdateCategoryInput[],
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
let productCategory = (await manager.findOne(ProductCategory, {
const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
...entry,
}
let productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id: categoryData.id,
})) as ProductCategory
})
if (!productCategory) {
throw new MedusaError(
@@ -465,10 +500,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
)
)[0]
const newParentCategory = await manager.findOne(
ProductCategory,
categoryData.parent_category_id
)
const newParentCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, categoryData.parent_category_id)
if (!newParentCategory) {
throw new MedusaError(
@@ -487,13 +521,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
)
function updateMpathRecursively(
category: ProductCategory,
category: InferEntityType<typeof ProductCategory>,
newBaseMpath: string
) {
const newMpath = `${newBaseMpath}.${category.id}`
category.mpath = newMpath
for (let child of category.category_children) {
child = manager.getReference(ProductCategory, child.id)
child = manager.getReference(ProductCategory.name, child.id)
manager.assign(
child,
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}`
}
// 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,
})
@@ -534,7 +571,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return productCategory
// If only the rank changed, we need to rerank all siblings.
} 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,
})
@@ -546,7 +583,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await this.rerankAllSiblings(
manager,
productCategory,
categoryData as Partial<EntityDTO<ProductCategory>> & {
categoryData as Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
}
)
@@ -569,9 +606,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterDeletion(
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,
rank: { $gt: removedSibling.rank },
})
@@ -586,9 +625,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterCreation(
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,
rank: { $gte: addedSibling.rank },
})
@@ -603,16 +644,22 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankAllSiblings(
manager: SqlEntityManager,
originalSibling: Partial<ProductCategory> & { rank: number },
updatedSibling: Partial<EntityDTO<ProductCategory>> & { rank: number }
originalSibling: Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
},
updatedSibling: Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
}
) {
if (originalSibling.rank === updatedSibling.rank) {
return
}
if (originalSibling.rank < updatedSibling.rank) {
const siblings = await manager.find(
ProductCategory,
const siblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank },
@@ -627,8 +674,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
manager.persist(updatedSiblings)
} else {
const siblings = await manager.find(
ProductCategory,
const siblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank },
@@ -1,11 +1,11 @@
import { Product } from "@models"
import { Context, DAL } from "@medusajs/framework/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { DALUtils } from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
// eslint-disable-next-line max-len
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Product>(
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Product
) {
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
*/
protected async mutateNotInCategoriesConstraints(
findOptions: DAL.FindOptions<Product> = { where: {} },
findOptions: DAL.FindOptions<typeof Product> = {
where: {},
},
context: Context = {}
): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -29,7 +31,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
findOptions.where.categories?.id?.["$nin"]
) {
const productsInCategories = await manager.find(
Product,
this.entity,
{
categories: {
id: { $in: findOptions.where.categories.id["$nin"] },
@@ -2,6 +2,7 @@ import {
Context,
DAL,
FindConfig,
InferEntityType,
ProductTypes,
} from "@medusajs/framework/types"
import {
@@ -33,7 +34,7 @@ export default class ProductCategoryService {
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> {
): Promise<InferEntityType<typeof ProductCategory>> {
if (!isDefined(productCategoryId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
@@ -41,7 +42,7 @@ export default class ProductCategoryService {
)
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
const queryOptions = ModulesSdkUtils.buildQuery(
{
id: productCategoryId,
},
@@ -67,7 +68,7 @@ export default class ProductCategoryService {
)
}
return productCategories[0] as ProductCategory
return productCategories[0]
}
@InjectManager("productCategoryRepository_")
@@ -75,7 +76,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -94,17 +95,14 @@ export default class ProductCategoryService {
delete filters.q
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
queryOptions.where ??= {}
return (await this.productCategoryRepository_.find(
return await this.productCategoryRepository_.find(
queryOptions,
transformOptions,
sharedContext
)) as ProductCategory[]
)
}
@InjectManager("productCategoryRepository_")
@@ -112,7 +110,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductCategory[], number]> {
): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -131,37 +129,34 @@ export default class ProductCategoryService {
delete filters.q
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
queryOptions.where ??= {}
return (await this.productCategoryRepository_.findAndCount(
return await this.productCategoryRepository_.findAndCount(
queryOptions,
transformOptions,
sharedContext
)) as [ProductCategory[], number]
)
}
@InjectTransactionManager("productCategoryRepository_")
async create(
data: ProductTypes.CreateProductCategoryDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
return (await (
): Promise<InferEntityType<typeof ProductCategory>[]> {
return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).create(data, sharedContext)) as ProductCategory[]
).create(data, sharedContext)
}
@InjectTransactionManager("productCategoryRepository_")
async update(
data: UpdateCategoryInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
return (await (
): Promise<InferEntityType<typeof ProductCategory>[]> {
return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).update(data, sharedContext)) as ProductCategory[]
).update(data, sharedContext)
}
@InjectTransactionManager("productCategoryRepository_")
@@ -3,6 +3,7 @@ import {
DAL,
FindConfig,
IEventBusModuleService,
InferEntityType,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
@@ -105,15 +106,31 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<Product>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<ProductVariant>
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Product>
>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductVariant>
>
protected readonly productCategoryService_: ProductCategoryService
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<ProductTag>
protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<ProductCollection>
protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService<ProductImage>
protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService<ProductType>
protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService<ProductOption>
protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService<ProductOptionValue>
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductTag>
>
protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductCollection>
>
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
constructor(
@@ -211,12 +228,16 @@ export default class ProductModuleService
return {
...config,
order: {
id: "ASC",
...config?.order,
...(hasImagesRelation ? {
...(hasImagesRelation
? {
images: {
rank: "ASC",
...((config?.order?.images as object) ?? {}),
},
} : {}),
}
: {}),
},
}
}
@@ -256,7 +277,7 @@ export default class ProductModuleService
protected async createVariants_(
data: ProductTypes.CreateProductVariantDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> {
): Promise<InferEntityType<typeof ProductVariant>[]> {
if (data.some((v) => !v.product_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -332,8 +353,8 @@ export default class ProductModuleService
(variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id
)
let created: ProductVariant[] = []
let updated: ProductVariant[] = []
let created: InferEntityType<typeof ProductVariant>[] = []
let updated: InferEntityType<typeof ProductVariant>[] = []
if (forCreate.length) {
created = await this.createVariants_(forCreate, sharedContext)
@@ -400,7 +421,7 @@ export default class ProductModuleService
protected async updateVariants_(
data: UpdateProductVariantInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> {
): Promise<InferEntityType<typeof ProductVariant>[]> {
// Validation step
const variantIdsToUpdate = data.map(({ id }) => id)
const variants = await this.productVariantService_.list(
@@ -535,8 +556,8 @@ export default class ProductModuleService
(tag): tag is ProductTypes.CreateProductTagDTO => !tag.id
)
let created: ProductTag[] = []
let updated: ProductTag[] = []
let created: InferEntityType<typeof ProductTag>[] = []
let updated: InferEntityType<typeof ProductTag>[] = []
if (forCreate.length) {
created = await this.productTagService_.create(forCreate, sharedContext)
@@ -665,8 +686,8 @@ export default class ProductModuleService
(type): type is ProductTypes.CreateProductTypeDTO => !type.id
)
let created: ProductType[] = []
let updated: ProductType[] = []
let created: InferEntityType<typeof ProductType>[] = []
let updated: InferEntityType<typeof ProductType>[] = []
if (forCreate.length) {
created = await this.productTypeService_.create(forCreate, sharedContext)
@@ -763,7 +784,7 @@ export default class ProductModuleService
protected async createOptions_(
data: ProductTypes.CreateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> {
): Promise<InferEntityType<typeof ProductOption>[]> {
if (data.some((v) => !v.product_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -810,8 +831,8 @@ export default class ProductModuleService
(option): option is ProductTypes.CreateProductOptionDTO => !option.id
)
let created: ProductOption[] = []
let updated: ProductOption[] = []
let created: InferEntityType<typeof ProductOption>[] = []
let updated: InferEntityType<typeof ProductOption>[] = []
if (forCreate.length) {
created = await this.createOptions_(forCreate, sharedContext)
@@ -876,7 +897,7 @@ export default class ProductModuleService
protected async updateOptions_(
data: UpdateProductOptionInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> {
): Promise<InferEntityType<typeof ProductOption>[]> {
// Validation step
if (data.some((option) => !option.id)) {
throw new MedusaError(
@@ -985,7 +1006,7 @@ export default class ProductModuleService
async createCollections_(
data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> {
): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map(
ProductModuleService.normalizeCreateProductCollectionInput
)
@@ -1030,8 +1051,8 @@ export default class ProductModuleService
!collection.id
)
let created: ProductCollection[] = []
let updated: ProductCollection[] = []
let created: InferEntityType<typeof ProductCollection>[] = []
let updated: InferEntityType<typeof ProductCollection>[] = []
if (forCreate.length) {
created = await this.createCollections_(forCreate, sharedContext)
@@ -1125,7 +1146,7 @@ export default class ProductModuleService
protected async updateCollections_(
data: UpdateCollectionInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> {
): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map(
ProductModuleService.normalizeUpdateProductCollectionInput
) as UpdateCollectionInput[]
@@ -1139,7 +1160,7 @@ export default class ProductModuleService
sharedContext
)
const collections: ProductCollection[] = []
const collections: InferEntityType<typeof ProductCollection>[] = []
const updateSelectorAndData = updatedCollections.flatMap(
(collectionData) => {
@@ -1178,7 +1199,7 @@ export default class ProductModuleService
collections.push({
...collectionData,
products: productsToUpdate ?? [],
} as ProductCollection)
} as InferEntityType<typeof ProductCollection>)
return result
}
@@ -1208,7 +1229,12 @@ export default class ProductModuleService
): Promise<
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(
input,
@@ -1255,8 +1281,8 @@ export default class ProductModuleService
!category.id
)
let created: ProductCategory[] = []
let updated: ProductCategory[] = []
let created: InferEntityType<typeof ProductCategory>[] = []
let updated: InferEntityType<typeof ProductCategory>[] = []
if (forCreate.length) {
created = await this.productCategoryService_.create(
@@ -1406,8 +1432,8 @@ export default class ProductModuleService
(product): product is ProductTypes.CreateProductDTO => !product.id
)
let created: Product[] = []
let updated: Product[] = []
let created: InferEntityType<typeof Product>[] = []
let updated: InferEntityType<typeof Product>[] = []
if (forCreate.length) {
created = await this.createProducts_(forCreate, sharedContext)
@@ -1494,7 +1520,7 @@ export default class ProductModuleService
protected async createProducts_(
data: ProductTypes.CreateProductDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<Product[]> {
): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll(
data.map(async (d) => {
const normalized = await this.normalizeCreateProductInput(
@@ -1581,7 +1607,7 @@ export default class ProductModuleService
protected async updateProducts_(
data: UpdateProductInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<Product[]> {
): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll(
data.map(async (d) => {
const normalized = await this.normalizeUpdateProductInput(
@@ -1733,6 +1759,13 @@ export default class ProductModuleService
) {
this.validateProductPayload(productData)
if (!productData.title) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Product title is required`
)
}
const options = productData.options
const missingOptionsVariants: string[] = []
@@ -1872,7 +1905,7 @@ export default class ProductModuleService
variants:
| ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[],
options: ProductOption[]
options: InferEntityType<typeof ProductOption>[]
):
| ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[] {
@@ -1945,7 +1978,7 @@ export default class ProductModuleService
| ProductTypes.CreateProductVariantDTO
| UpdateProductVariantInput
) & { options: { id: string }[]; product_id: string })[],
variants: ProductVariant[]
variants: InferEntityType<typeof ProductVariant>[]
) {
for (const variantData of data) {
const existingVariant = variants.find((v) => {