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:
Harminder Virk
2024-11-26 20:01:02 +05:30
committed by GitHub
parent d6fa912b22
commit 9f204817b0
61 changed files with 2315 additions and 1939 deletions

View File

@@ -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: [

View File

@@ -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(

View File

@@ -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}`

View File

@@ -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>

View File

@@ -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)) {

View File

@@ -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
}
}