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:
@@ -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
|
||||
})
|
||||
})
|
||||
)
|
||||
Object.values(linkConfig).flatMap((entityLinkConfig) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user