feat: refactor module joiner config and links generation (#7859)

* feat: refactor module joiner config and links generation

* improve typings

* WIP

* WIP

* WIP

* rename type file

* create link config

* finish typings and add utils

* improve links

* WIP typings

* finalize ExportModule utils

* finalize ExportModule utils

* fix: dml tests

* improve and fixes

* simplify typings with id changes

* add toJSON

* multiple fixes and entity builder fixes

* fix currency searchable

* fix tests

* medusa service refactoring

* cleanup

* cleanup and fixes

* make module name optional

* renaming

---------

Co-authored-by: Harminder Virk <virk.officials@gmail.com>
This commit is contained in:
Adrien de Peretti
2024-07-03 13:12:49 +02:00
committed by GitHub
parent 5aa62e59e4
commit 617a5972bf
89 changed files with 1706 additions and 950 deletions
@@ -1,5 +1,5 @@
export function toCamelCase(str: string): string {
return /^([a-z]+)(([A-Z]([a-z]+))+)$/.test(str)
return /^([a-zA-Z]+)(([A-Z]([a-z]+))+)$/.test(str)
? str
: str
.toLowerCase()
@@ -34,7 +34,6 @@ describe("Base property", () => {
dataType: {
name: "text",
options: {
primaryKey: false,
searchable: true,
},
},
@@ -1,4 +1,4 @@
import { ArrayType, MetadataStorage } from "@mikro-orm/core"
import { ArrayType, EntityMetadata, MetadataStorage } from "@mikro-orm/core"
import { expectTypeOf } from "expect-type"
import { DmlEntity } from "../entity"
import { model } from "../entity-builder"
@@ -72,7 +72,7 @@ describe("Entity builder", () => {
phones: model.array(),
})
expect(user.name).toEqual("user")
expect(user.name).toEqual("User")
expect(user.parse().tableName).toEqual("user")
const User = toMikroORMEntity(user)
@@ -202,7 +202,7 @@ describe("Entity builder", () => {
}
)
expect(user.name).toEqual("user")
expect(user.name).toEqual("User")
expect(user.parse().tableName).toEqual("user_table")
const User = toMikroORMEntity(user)
@@ -324,7 +324,7 @@ describe("Entity builder", () => {
}
)
expect(user.name).toEqual("userRole")
expect(user.name).toEqual("UserRole")
expect(user.parse().tableName).toEqual("user_role")
const User = toMikroORMEntity(user)
@@ -1558,9 +1558,10 @@ describe("Entity builder", () => {
account_id: model.number(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const metaData = MetadataStorage.getMetadataFromDecorator(User)
const User = toMikroORMEntity(user)
const metaData = MetadataStorage.getMetadataFromDecorator(
User
) as unknown as EntityMetadata<InstanceType<typeof User>>
expect(metaData.properties.id).toEqual({
columnType: "text",
@@ -3848,7 +3849,7 @@ describe("Entity builder", () => {
})
expect(defineEmail).toThrow(
'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed'
'Cannot cascade delete "user" relationship(s) from "Email" entity. Child to parent cascades are not allowed'
)
})
@@ -10,9 +10,7 @@ describe("Id property", () => {
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: false,
},
options: {},
},
nullable: false,
indexes: [],
@@ -28,13 +26,12 @@ describe("Id property", () => {
fieldName: "id",
dataType: {
name: "id",
options: {
primaryKey: true,
},
options: {},
},
nullable: false,
indexes: [],
relationships: [],
primaryKey: true,
})
})
})
@@ -10,9 +10,7 @@ describe("Number property", () => {
fieldName: "age",
dataType: {
name: "number",
options: {
primaryKey: false,
},
options: {},
},
nullable: false,
indexes: [],
@@ -10,7 +10,7 @@ describe("Text property", () => {
fieldName: "username",
dataType: {
name: "text",
options: { primaryKey: false, searchable: false },
options: { searchable: false },
},
nullable: false,
indexes: [],
@@ -26,11 +26,12 @@ describe("Text property", () => {
fieldName: "username",
dataType: {
name: "text",
options: { primaryKey: true, searchable: false },
options: { searchable: false },
},
nullable: false,
indexes: [],
relationships: [],
primaryKey: true,
})
})
})
+25 -9
View File
@@ -1,7 +1,17 @@
import type { DMLSchema, RelationshipOptions } from "@medusajs/types"
import {
DMLSchema,
IDmlEntityConfig,
RelationshipOptions,
} from "@medusajs/types"
import { DmlEntity } from "./entity"
import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties"
import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties"
import {
createBigNumberProperties,
DMLSchemaWithBigNumber,
} from "./helpers/entity-builder/create-big-number-properties"
import {
createDefaultProperties,
DMLSchemaDefaults,
} from "./helpers/entity-builder/create-default-properties"
import { ArrayProperty } from "./properties/array"
import { BigNumberProperty } from "./properties/big-number"
import { BooleanProperty } from "./properties/boolean"
@@ -57,7 +67,7 @@ export type ManyToManyOptions = RelationshipOptions &
* representing the pivot table created in the
* database for this relationship.
*/
pivotEntity?: () => DmlEntity<any>
pivotEntity?: () => DmlEntity<any, any>
}
)
@@ -99,17 +109,23 @@ export class EntityBuilder {
*
* export default MyCustom
*/
define<Schema extends DMLSchema>(
nameOrConfig: DefineOptions,
define<Schema extends DMLSchema, TConfig extends IDmlEntityConfig>(
nameOrConfig: TConfig,
schema: Schema
) {
): DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
> {
this.#disallowImplicitProperties(schema)
return new DmlEntity(nameOrConfig, {
return new DmlEntity<Schema, TConfig>(nameOrConfig, {
...schema,
...createBigNumberProperties(schema),
...createDefaultProperties(),
})
}) as unknown as DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
>
}
/**
+26 -11
View File
@@ -4,25 +4,32 @@ import {
EntityIndex,
ExtractEntityRelations,
IDmlEntity,
IDmlEntityConfig,
InferDmlEntityNameFromConfig,
IsDmlEntity,
QueryCondition,
} from "@medusajs/types"
import { isObject, isString, toCamelCase } from "../common"
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
import { BelongsTo } from "./relations/belongs-to"
type Config = string | { name?: string; tableName: string }
function extractNameAndTableName(nameOrConfig: Config) {
function extractNameAndTableName<Config extends IDmlEntityConfig>(
nameOrConfig: Config
) {
const result = {
name: "",
tableName: "",
} as {
name: InferDmlEntityNameFromConfig<Config>
tableName: string
}
if (isString(nameOrConfig)) {
const [schema, ...rest] = nameOrConfig.split(".")
const name = rest.length ? rest.join(".") : schema
result.name = toCamelCase(name)
result.name = upperCaseFirst(
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig
}
@@ -37,7 +44,9 @@ function extractNameAndTableName(nameOrConfig: Config) {
const [schema, ...rest] = potentialName.split(".")
const name = rest.length ? rest.join(".") : schema
result.name = toCamelCase(name)
result.name = upperCaseFirst(
toCamelCase(name)
) as InferDmlEntityNameFromConfig<Config>
result.tableName = nameOrConfig.tableName
}
@@ -48,17 +57,23 @@ function extractNameAndTableName(nameOrConfig: Config) {
* Dml entity is a representation of a DML model with a unique
* name, its schema and relationships.
*/
export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
export class DmlEntity<
Schema extends DMLSchema,
TConfig extends IDmlEntityConfig
> implements IDmlEntity<Schema, TConfig>
{
[IsDmlEntity]: true = true
name: string
name: InferDmlEntityNameFromConfig<TConfig>
schema: Schema
readonly #tableName: string
#cascades: EntityCascades<string[]> = {}
#indexes: EntityIndex<Schema>[] = []
constructor(nameOrConfig: Config, public schema: Schema) {
constructor(nameOrConfig: TConfig, schema: Schema) {
const { name, tableName } = extractNameAndTableName(nameOrConfig)
this.schema = schema
this.name = name
this.#tableName = tableName
}
@@ -70,7 +85,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
*
* @param entity
*/
static isDmlEntity(entity: unknown): entity is DmlEntity<any> {
static isDmlEntity(entity: unknown): entity is DmlEntity<any, any> {
return !!entity?.[IsDmlEntity]
}
@@ -78,7 +93,7 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
* Parse entity to get its underlying information
*/
parse(): {
name: string
name: InferDmlEntityNameFromConfig<TConfig>
tableName: string
schema: DMLSchema
cascades: EntityCascades<string[]>
@@ -1,4 +1,10 @@
import type { EntityConstructor, Infer } from "@medusajs/types"
import type {
DMLSchema,
EntityConstructor,
IDmlEntity,
Infer,
PropertyType,
} from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity"
@@ -34,7 +40,9 @@ export function createMikrORMEntity() {
* A helper function to define a Mikro ORM entity from a
* DML entity.
*/
return function createEntity<T extends DmlEntity<any>>(entity: T): Infer<T> {
return function createEntity<T extends DmlEntity<any, any>>(
entity: T
): Infer<T> {
class MikroORMEntity {}
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
@@ -57,11 +65,11 @@ export function createMikrORMEntity() {
/**
* Processing schema fields
*/
Object.entries(schema).forEach(([name, property]) => {
Object.entries(schema as DMLSchema).forEach(([name, property]) => {
const field = property.parse(name)
if ("fieldName" in field) {
defineProperty(MikroORMEntity, field)
defineProperty(MikroORMEntity, name, property as PropertyType<any>)
applyIndexes(MikroORMEntity, tableName, field)
applySearchable(MikroORMEntity, field)
} else {
@@ -85,14 +93,16 @@ export function createMikrORMEntity() {
* return the input idempotently
* @param entity
*/
export const toMikroORMEntity = <T>(entity: T): Infer<T> => {
export const toMikroORMEntity = <T>(
entity: T
): T extends IDmlEntity<any, any> ? Infer<T> : T => {
let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) {
mikroOrmEntity = createMikrORMEntity()(entity)
}
return mikroOrmEntity as Infer<T>
return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T
}
/**
@@ -110,6 +120,6 @@ export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
return entity
}) as {
[K in keyof T]: T[K] extends DmlEntity<any> ? Infer<T[K]> : T[K]
[K in keyof T]: T[K] extends IDmlEntity<any, any> ? Infer<T[K]> : T[K]
}
}
@@ -13,7 +13,7 @@ import { NullableModifier } from "../../properties/nullable"
* test.amount // valid | type = number
* test.raw_amount // valid | type = Record<string, any>
*/
type DMLSchemaWithBigNumber<T extends DMLSchema> = {
export type DMLSchemaWithBigNumber<T extends DMLSchema> = {
[K in keyof T]: T[K]
} & {
[K in keyof T as T[K] extends
@@ -2,6 +2,7 @@ import {
EntityConstructor,
KnownDataTypes,
PropertyMetadata,
PropertyType,
} from "@medusajs/types"
import { MikroOrmBigNumberProperty } from "../../../dal"
import { generateEntityId, isDefined } from "../../../common"
@@ -14,6 +15,7 @@ import {
Property,
Utils,
} from "@mikro-orm/core"
import { PrimaryKeyModifier } from "../../properties/primary-key"
/**
* DML entity data types to PostgreSQL data types via
@@ -91,8 +93,10 @@ const SPECIAL_PROPERTIES: {
*/
export function defineProperty(
MikroORMEntity: EntityConstructor<any>,
field: PropertyMetadata
propertyName: string,
property: PropertyType<any>
) {
const field = property.parse(propertyName)
/**
* Here we initialize nullable properties with a null value
*/
@@ -169,7 +173,7 @@ export function defineProperty(
* Defining an id property
*/
if (field.dataType.name === "id") {
const IdDecorator = field.dataType.options?.primaryKey
const IdDecorator = PrimaryKeyModifier.isPrimaryKeyModifier(property)
? PrimaryKey({
columnType: "text",
type: "string",
@@ -211,7 +215,7 @@ export function defineProperty(
/**
* Defining a primary key property
*/
if (field.dataType.options?.primaryKey) {
if (PrimaryKeyModifier.isPrimaryKeyModifier(property)) {
PrimaryKey({
columnType,
type: propertyType,
@@ -9,9 +9,9 @@ import {
BeforeCreate,
ManyToMany,
ManyToOne,
OnInit,
OneToMany,
OneToOne,
OnInit,
Property,
} from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common"
@@ -81,7 +81,8 @@ export function defineBelongsToRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>
Record<string, PropertyType<any> | RelationshipType<any>>,
any
>,
{ relatedModelName }: { relatedModelName: string }
) {
@@ -213,7 +214,8 @@ export function defineManyToManyRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, PropertyType<any> | RelationshipType<any>>
Record<string, PropertyType<any> | RelationshipType<any>>,
any
>,
{
relatedModelName,
@@ -5,7 +5,7 @@ import { camelToSnakeCase, toCamelCase, upperCaseFirst } from "../../../common"
* Parses entity name and returns model and table name from
* it
*/
export function parseEntityName(entity: DmlEntity<any>) {
export function parseEntityName(entity: DmlEntity<any, any>) {
const parsedEntity = entity.parse()
/**
+11 -4
View File
@@ -1,19 +1,27 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
const IsIdProperty = Symbol("IsIdProperty")
/**
* The Id property defines a unique identifier for the schema.
* Most of the times it will be the primary as well.
*/
export class IdProperty extends BaseProperty<string> {
[IsIdProperty] = true
static isIdProperty(value: any): value is IdProperty {
return !!value?.[IsIdProperty]
}
protected dataType: {
name: "id"
options: {
primaryKey: boolean
prefix?: string
}
} = {
name: "id",
options: { primaryKey: false },
options: {},
}
constructor(options?: { prefix?: string }) {
@@ -37,7 +45,6 @@ export class IdProperty extends BaseProperty<string> {
* @customNamespace Property Configuration Methods
*/
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<string, IdProperty>(this)
}
}
@@ -1,4 +1,5 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
/**
* The NumberProperty is used to define a numeric/integer
@@ -7,15 +8,11 @@ import { BaseProperty } from "./base"
export class NumberProperty extends BaseProperty<number> {
protected dataType: {
name: "number"
options: {
primaryKey: boolean
}
options: {}
}
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<number, NumberProperty>(this)
}
constructor(options?: { primaryKey?: boolean }) {
@@ -23,7 +20,7 @@ export class NumberProperty extends BaseProperty<number> {
this.dataType = {
name: "number",
options: { primaryKey: false, ...options },
options: { ...options },
}
}
}
@@ -0,0 +1,39 @@
import { PropertyType } from "@medusajs/types"
const IsPrimaryKeyModifier = Symbol.for("isPrimaryKeyModifier")
/**
* PrimaryKey modifier marks a schema node as primaryKey
*/
export class PrimaryKeyModifier<T, Schema extends PropertyType<T>>
implements PropertyType<T>
{
[IsPrimaryKeyModifier]: true = true
static isPrimaryKeyModifier(obj: any): obj is PrimaryKeyModifier<any, any> {
return !!obj?.[IsPrimaryKeyModifier]
}
/**
* A type-only property to infer the JavScript data-type
* of the schema property
*/
declare $dataType: T
/**
* The parent schema on which the primaryKey modifier is
* applied
*/
#schema: Schema
constructor(schema: Schema) {
this.#schema = schema
}
/**
* Returns the serialized metadata
*/
parse(fieldName: string) {
const schema = this.#schema.parse(fieldName)
schema.primaryKey = true
return schema
}
}
@@ -1,4 +1,5 @@
import { BaseProperty } from "./base"
import { PrimaryKeyModifier } from "./primary-key"
/**
* The TextProperty is used to define a textual property
@@ -7,14 +8,12 @@ export class TextProperty extends BaseProperty<string> {
protected dataType: {
name: "text"
options: {
primaryKey: boolean
prefix?: string
searchable: boolean
}
} = {
name: "text",
options: {
primaryKey: false,
searchable: false,
},
}
@@ -35,9 +34,7 @@ export class TextProperty extends BaseProperty<string> {
* @customNamespace Property Configuration Methods
*/
primaryKey() {
this.dataType.options.primaryKey = true
return this
return new PrimaryKeyModifier<string, TextProperty>(this)
}
/**
@@ -5,11 +5,15 @@ import {
RelationshipTypes,
} from "@medusajs/types"
export const IsRelationship = Symbol.for("isRelationship")
/**
* The BaseRelationship encapsulates the repetitive parts of defining
* a relationship
*/
export abstract class BaseRelationship<T> implements RelationshipType<T> {
[IsRelationship]: true = true
#referencedEntity: T
/**
@@ -28,6 +32,12 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
*/
declare $dataType: T
static isRelationship<T>(
relationship: any
): relationship is BaseRelationship<T> {
return !!relationship?.[IsRelationship]
}
constructor(referencedEntity: T, options: RelationshipOptions) {
this.#referencedEntity = referencedEntity
this.options = options
@@ -1,4 +1,5 @@
import { RelationshipType } from "@medusajs/types"
import { IsRelationship } from "./base"
const IsNullableModifier = Symbol.for("isNullableModifier")
@@ -8,7 +9,8 @@ const IsNullableModifier = Symbol.for("isNullableModifier")
export class NullableModifier<T, Relation extends RelationshipType<T>>
implements RelationshipType<T | null>
{
[IsNullableModifier]: true = true
[IsNullableModifier]: true = true;
[IsRelationship]: true = true
static isNullableModifier<T>(
modifier: any
@@ -0,0 +1,53 @@
import { model } from "../../../dml"
export const FulfillmentSet = {
name: "FulfillmentSet",
}
export const ShippingOption = {
name: "ShippingOption",
}
export const ShippingProfile = {
name: "ShippingProfile",
}
export const Fulfillment = {
name: "Fulfillment",
}
export const FulfillmentProvider = {
name: "FulfillmentProvider",
}
export const ServiceZone = {
name: "ServiceZone",
}
export const GeoZone = {
name: "GeoZone",
}
export const ShippingOptionRule = {
name: "ShippingOptionRule",
}
export const dmlFulfillmentSet = model.define(FulfillmentSet.name, {
id: model.id().primaryKey(),
})
export const dmlShippingOption = model.define(ShippingOption.name, {
id: model.id().primaryKey(),
})
export const dmlShippingProfile = model.define(ShippingProfile.name, {
id: model.id().primaryKey(),
})
export const dmlFulfillment = model.define(Fulfillment.name, {
id: model.id().primaryKey(),
})
export const dmlFulfillmentProvider = model.define(FulfillmentProvider.name, {
id: model.id().primaryKey(),
})
export const dmlServiceZone = model.define(ServiceZone.name, {
id: model.id().primaryKey(),
})
export const dmlGeoZone = model.define(GeoZone.name, {
id: model.id().primaryKey(),
})
export const dmlShippingOptionRule = model.define(ShippingOptionRule.name, {
id: model.id().primaryKey(),
})
@@ -1,315 +1,505 @@
import { defineJoinerConfig } from "../joiner-config-builder"
import {
buildLinkableKeysFromDmlObjects,
buildLinkableKeysFromMikroOrmObjects,
buildLinkConfigFromDmlObjects,
defineJoinerConfig,
} from "../joiner-config-builder"
import { Modules } from "../definition"
import { model } from "../../dml"
import { expectTypeOf } from "expect-type"
import {
dmlFulfillment,
dmlFulfillmentProvider,
dmlFulfillmentSet,
dmlGeoZone,
dmlServiceZone,
dmlShippingOption,
dmlShippingOptionRule,
dmlShippingProfile,
Fulfillment,
FulfillmentProvider,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingOptionRule,
ShippingProfile,
} from "../__fixtures__/joiner-config/entities"
const FulfillmentSet = {
name: "FulfillmentSet",
}
const ShippingOption = {
name: "ShippingOption",
}
const ShippingProfile = {
name: "ShippingProfile",
}
const Fulfillment = {
name: "Fulfillment",
}
const FulfillmentProvider = {
name: "FulfillmentProvider",
}
const ServiceZone = {
name: "ServiceZone",
}
const GeoZone = {
name: "GeoZone",
}
const ShippingOptionRule = {
name: "ShippingOptionRule",
}
describe("joiner-config-builder", () => {
describe("defineJoiner | Mikro orm objects", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
})
describe("defineJoiner", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
})
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
it("should return a full joiner configuration with custom aliases", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
],
})
})
it("should return a full joiner configuration with custom aliases and models", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases without method suffix", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
})
it("should return a full joiner configuration with custom aliases overriding defaults", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [FulfillmentSet],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
},
],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
})
})
it("should return a full joiner configuration with custom aliases", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
describe("defineJoiner | DML objects", () => {
it("should return a full joiner configuration", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
models: [
dmlFulfillmentSet,
dmlShippingOption,
dmlShippingProfile,
dmlFulfillment,
dmlFulfillmentProvider,
dmlServiceZone,
dmlGeoZone,
dmlShippingOptionRule,
],
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
})
})
})
it("should return a full joiner configuration with custom aliases and models", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [
FulfillmentSet,
ShippingOption,
ShippingProfile,
Fulfillment,
FulfillmentProvider,
ServiceZone,
GeoZone,
ShippingOptionRule,
],
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
})
describe("buildLinkableKeysFromDmlObjects", () => {
it("should return a linkableKeys object based on the DML's primary keys", () => {
const user = model.define("user", {
id: model.id().primaryKey(),
name: model.text(),
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
shipping_profile_id: ShippingProfile.name,
fulfillment_id: Fulfillment.name,
fulfillment_provider_id: FulfillmentProvider.name,
service_zone_id: ServiceZone.name,
geo_zone_id: GeoZone.name,
shipping_option_rule_id: ShippingOptionRule.name,
},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
methodSuffix: "FulfillmentSets",
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["fulfillment_provider", "fulfillment_providers"],
args: {
entity: FulfillmentProvider.name,
methodSuffix: "FulfillmentProviders",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
{
name: ["shipping_option_rule", "shipping_option_rules"],
args: {
entity: ShippingOptionRule.name,
methodSuffix: "ShippingOptionRules",
},
},
],
const car = model.define("car", {
id: model.id(),
number_plate: model.text().primaryKey(),
test: model.text(),
})
const linkableKeys = buildLinkableKeysFromDmlObjects([user, car])
expectTypeOf(linkableKeys).toMatchTypeOf<{
user_id: "User"
car_number_plate: "Car"
}>()
expect(linkableKeys).toEqual({
user_id: user.name,
car_number_plate: car.name,
})
})
})
it("should return a full joiner configuration with custom aliases without method suffix", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
},
},
],
})
describe("buildLinkableKeysFromMikroOrmObjects", () => {
it("should return a linkableKeys object based on the mikro orm models name", () => {
class User {}
class Car {}
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {},
alias: [
{
name: ["custom", "customs"],
args: {
entity: "Custom",
methodSuffix: "Customs",
},
},
],
const linkableKeys = buildLinkableKeysFromMikroOrmObjects([Car, User])
expect(linkableKeys).toEqual({
user_id: User.name,
car_id: Car.name,
})
})
})
it("should return a full joiner configuration with custom aliases overriding defaults", () => {
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
entityQueryingConfig: [FulfillmentSet],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
})
describe("buildLinkConfigFromDmlObjects", () => {
it("should return a link config object based on the DML's primary keys", () => {
const user = model.define("user", {
id: model.id().primaryKey(),
name: model.text(),
})
expect(joinerConfig).toEqual({
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
schema: undefined,
linkableKeys: {
fulfillment_set_id: FulfillmentSet.name,
},
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: "FulfillmentSet",
methodSuffix: "fulfillmentSetCustom",
},
},
],
const car = model.define("car", {
id: model.id(),
number_plate: model.text().primaryKey(),
})
const linkConfig = buildLinkConfigFromDmlObjects([user, car])
expectTypeOf(linkConfig).toMatchTypeOf<{
user: {
id: {
linkable: "user_id"
primaryKey: "id"
}
toJSON: () => {
linkable: string
primaryKey: string
}
}
car: {
number_plate: {
linkable: "car_number_plate"
primaryKey: "number_plate"
}
toJSON: () => {
linkable: string
primaryKey: string
}
}
}>()
expect(linkConfig.user.id).toEqual({
linkable: "user_id",
primaryKey: "id",
})
expect(linkConfig.car.number_plate).toEqual({
linkable: "car_number_plate",
primaryKey: "number_plate",
})
expect(linkConfig.car.toJSON()).toEqual({
linkable: "car_number_plate",
primaryKey: "number_plate",
})
expect(linkConfig.user.toJSON()).toEqual({
linkable: "user_id",
primaryKey: "id",
})
})
})
})
@@ -1,6 +1,9 @@
import { Context, EventBusTypes } from "@medusajs/types"
// TODO should that move closer to the event bus? and maybe be rename to moduleEventBuilderFactory
/**
*
* Factory function to create event builders for different entities
*
* @example
@@ -48,7 +51,8 @@ export function eventBuilderFactory({
// The event enums contains event formatted like so [object]_[action] e.g. PRODUCT_CREATED
// We expect the keys of events to be fully uppercased
const eventName = eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
const eventName =
eventsEnum[`${object.toUpperCase()}_${action.toUpperCase()}`]
data.forEach((dataItem) => {
messages.push({
+1 -1
View File
@@ -13,4 +13,4 @@ export * from "./medusa-internal-service"
export * from "./medusa-service"
export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder"
export * from "./module"
@@ -1,14 +1,24 @@
import { JoinerServiceConfigAlias, ModuleJoinerConfig } from "@medusajs/types"
import {
JoinerServiceConfigAlias,
ModuleJoinerConfig,
PropertyType,
} from "@medusajs/types"
import { dirname, join } from "path"
import {
MapToConfig,
camelToSnakeCase,
deduplicate,
getCallerFilePath,
isObject,
lowerCaseFirst,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
import { loadModels } from "./loaders/load-models"
import { DmlEntity } from "../dml"
import { BaseRelationship } from "../dml/relations/base"
import { PrimaryKeyModifier } from "../dml/properties/primary-key"
import { InferLinkableKeys, InfersLinksConfig } from "./types/links-config"
/**
* Define joiner config for a module based on the models (object representation or entities) present in the models directory. This action will be sync until
@@ -20,7 +30,7 @@ import { loadModels } from "./loaders/load-models"
* @param moduleName
* @param alias
* @param schema
* @param entityQueryingConfig
* @param models
* @param linkableKeys
* @param primaryKeys
*/
@@ -29,13 +39,13 @@ export function defineJoinerConfig(
{
alias,
schema,
entityQueryingConfig,
models,
linkableKeys,
primaryKeys,
}: {
alias?: JoinerServiceConfigAlias[]
schema?: string
entityQueryingConfig?: { name: string }[]
models?: DmlEntity<any, any>[] | { name: string }[]
linkableKeys?: Record<string, string>
primaryKeys?: string[]
} = {}
@@ -62,20 +72,64 @@ export function defineJoinerConfig(
basePath = join(basePath, "models")
const models = deduplicate(
[...(entityQueryingConfig ?? loadModels(basePath))].flatMap((v) => v!.name)
).map((name) => ({ name }))
let loadedModels = models ?? loadModels(basePath)
const modelDefinitions = new Map(
loadedModels
.filter((model) => !!DmlEntity.isDmlEntity(model))
.map((model) => [model.name, model])
)
const mikroOrmObjects = new Map(
loadedModels
.filter((model) => !DmlEntity.isDmlEntity(model))
.map((model) => [model.name, model])
)
// We prioritize DML if there is any equivalent Mikro orm entities found
loadedModels = [...modelDefinitions.values()]
mikroOrmObjects.forEach((model) => {
if (modelDefinitions.has(model.name)) {
return
}
loadedModels.push(model)
})
if (!linkableKeys) {
const linkableKeysFromDml = buildLinkableKeysFromDmlObjects([
...modelDefinitions.values(),
])
const linkableKeysFromMikroOrm = buildLinkableKeysFromMikroOrmObjects([
...mikroOrmObjects.values(),
])
linkableKeys = {
...linkableKeysFromDml,
...linkableKeysFromMikroOrm,
}
}
if (!primaryKeys && modelDefinitions.size) {
const linkConfig = buildLinkConfigFromDmlObjects([
...modelDefinitions.values(),
])
primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig) as any[])
.filter((linkableConfig) => isObject(linkableConfig))
.map((linkableConfig) => {
return linkableConfig.primaryKey
})
})
)
}
// 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: moduleName,
primaryKeys: primaryKeys ?? ["id"],
schema,
linkableKeys:
linkableKeys ??
models.reduce((acc, entity) => {
acc[`${camelToSnakeCase(entity.name).toLowerCase()}_id`] = entity.name
return acc
}, {} as Record<string, string>),
linkableKeys: linkableKeys,
alias: [
...[...(alias ?? ([] as any))].map((alias) => ({
name: alias.name,
@@ -86,7 +140,7 @@ export function defineJoinerConfig(
pluralize(upperCaseFirst(alias.args.entity)),
},
})),
...models
...loadedModels
.filter((model) => {
return (
!alias || !alias.some((alias) => alias.args?.entity === model.name)
@@ -106,8 +160,153 @@ export function defineJoinerConfig(
}
}
/**
* From a set of DML objects, build the linkable keys
*
* @example
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* // output:
* // {
* // user_id: 'User',
* // car_number_plate: 'Car',
* // }
*
* @param models
*/
export function buildLinkableKeysFromDmlObjects<
const T extends DmlEntity<any, any>[],
LinkableKeys = InferLinkableKeys<T>
>(models: T): LinkableKeys {
const linkableKeys = {} as LinkableKeys
for (const model of models) {
if (!DmlEntity.isDmlEntity(model)) {
continue
}
const schema = model.schema
const primaryKeys: string[] = []
for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) {
continue
}
const parsedProperty = (value as PropertyType<any>).parse(property)
if (PrimaryKeyModifier.isPrimaryKeyModifier(value)) {
const linkableKeyName =
parsedProperty.dataType.options?.linkable ??
`${camelToSnakeCase(model.name).toLowerCase()}_${property}`
primaryKeys.push(linkableKeyName)
}
}
if (primaryKeys.length) {
primaryKeys.forEach((primaryKey) => {
linkableKeys[primaryKey] = model.name
})
}
}
return linkableKeys
}
/**
* Build linkable keys from MikroORM objects
* @deprecated
* @param models
*/
export function buildLinkableKeysFromMikroOrmObjects(
models: Function[]
): Record<string, string> {
return models.reduce((acc, entity) => {
acc[`${camelToSnakeCase(entity.name).toLowerCase()}_id`] = entity.name
return acc
}, {}) as Record<string, string>
}
/**
* Build entities name to linkable keys map
*
* @example
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* // output:
* // {
* // toJSON: function () { },
* // user: {
* // id: "user_id",
* // },
* // car: {
* // number_plate: "car_number_plate",
* // },
* // }
*
* @param models
*/
export function buildLinkConfigFromDmlObjects<
const T extends DmlEntity<any, any>[]
>(models: T = [] as unknown as T): InfersLinksConfig<T> {
const linkConfig = {} as InfersLinksConfig<T>
for (const model of models) {
if (!DmlEntity.isDmlEntity(model)) {
continue
}
const schema = model.schema
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]
},
})
for (const [property, value] of Object.entries(schema)) {
if (BaseRelationship.isRelationship(value)) {
continue
}
const parsedProperty = (value as PropertyType<any>).parse(property)
if (PrimaryKeyModifier.isPrimaryKeyModifier(value)) {
const linkableKeyName =
parsedProperty.dataType.options?.linkable ??
`${camelToSnakeCase(model.name).toLowerCase()}_${property}`
modelLinkConfig[property] = {
linkable: linkableKeyName,
primaryKey: property,
}
}
}
}
return linkConfig as InfersLinksConfig<T> & Record<any, any>
}
/**
* Reversed map from linkableKeys to entity name to linkable keys
* @param linkableKeys
*/
export function buildEntitiesNameToLinkableKeysMap(
@@ -27,15 +27,15 @@ export function loadModels(basePath: string) {
if (stats.isFile()) {
try {
const required = require(filePath) as {
[key: string]: { name?: string }
}
const required = require(filePath)
return Object.values(required).filter((resource) => !!resource.name)
return Object.values(required).filter(
(resource: any) => !!resource.name
)
} catch (e) {}
}
return
})
.filter(Boolean) as { name: string }[]
.filter(Boolean) as any[]
}
@@ -35,12 +35,16 @@ type SelectorAndData = {
data: any
}
export function MedusaInternalService<TContainer extends object = object>(
export function MedusaInternalService<
TContainer extends object = object,
TEntity extends object = any
>(
rawModel: any
): {
new <TEntity extends object = any>(
container: TContainer
): ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
new (container: TContainer): ModulesSdkTypes.IMedusaInternalService<
TEntity,
TContainer
>
} {
const model = DmlEntity.isDmlEntity(rawModel)
? toMikroORMEntity(rawModel)
@@ -49,7 +53,7 @@ export function MedusaInternalService<TContainer extends object = object>(
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
const propertyRepositoryName = `__${injectedRepositoryName}__`
class AbstractService_<TEntity extends object>
class AbstractService_
implements ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
{
readonly __container__: TContainer;
@@ -556,7 +560,5 @@ export function MedusaInternalService<TContainer extends object = object>(
}
}
return AbstractService_ as unknown as new <TEntity extends {}>(
container: TContainer
) => ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
return AbstractService_ as any
}
@@ -2,11 +2,10 @@
* Utility factory and interfaces for module service public facing API
*/
import {
Constructor,
Context,
FindConfig,
IEventBusModuleService,
Pluralize,
ModuleJoinerConfig,
RepositoryService,
RestoreReturn,
SoftDeleteReturn,
@@ -22,17 +21,15 @@ import {
} from "../common"
import { InjectManager, MedusaContext } from "./decorators"
import { ModuleRegistrationName } from "./definition"
import { DmlEntity } from "../dml"
type BaseMethods =
| "retrieve"
| "list"
| "listAndCount"
| "delete"
| "softDelete"
| "restore"
| "create"
| "update"
import {
BaseMethods,
EntitiesConfigTemplate,
ExtractKeysFromConfig,
MedusaServiceReturnType,
ModelConfigurationsToConfigTemplate,
TEntityEntries,
} from "./types/medusa-service"
import { buildEntitiesNameToLinkableKeysMap } from "./joiner-config-builder"
const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[]
const writeMethods = [
@@ -45,200 +42,6 @@ const writeMethods = [
const methods: BaseMethods[] = [...readMethods, ...writeMethods]
type ModelDTOConfig = {
dto: object
create?: any
update?: any
/**
* @internal
* @deprecated
*/
singular?: string
/**
* @internal
* @deprecated
*/
plural?: string
}
type EntitiesConfigTemplate = { [key: string]: ModelDTOConfig }
type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
[Key in keyof T as `${Capitalize<Key & string>}`]: {
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
create: any
update: any
singular: T[Key] extends { singular: string } ? T[Key]["singular"] : Key
plural: T[Key] extends { plural: string }
? T[Key]["plural"]
: Pluralize<Key & string>
}
}
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
*/
type ExtractSingularName<T extends Record<any, any>, K = keyof T> = Capitalize<
T[K] extends { singular?: string } ? T[K]["singular"] & string : K & string
>
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
* The pluralize will move to where it should be used instead
*/
type ExtractPluralName<T extends Record<any, any>, K = keyof T> = Capitalize<
T[K] extends {
plural?: string
}
? T[K]["plural"] & string
: Pluralize<K & string>
>
// TODO: The future expected entry will be a DML object but in the meantime we have to maintain backward compatibility for ouw own modules and therefore we need to support Constructor<any> as well as this temporary object
type TEntityEntries<Keys = string> = Record<
Keys & string,
| Constructor<any>
| DmlEntity<any>
| { name?: string; singular?: string; plural?: string }
>
type ExtractKeysFromConfig<EntitiesConfig> = EntitiesConfig extends {
__empty: any
}
? string
: keyof EntitiesConfig
export type AbstractModuleService<
TEntitiesDtoConfig extends Record<string, any>
> = {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
id: string,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `list${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `listAndCount${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
[TEntitiesDtoConfig[TEntityName]["dto"][], number]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `delete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `softDelete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `restore${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
}
// TODO: Because of a bug, those methods were not made visible which now cause issues with the fix as our interface are not consistent with the expectations
// are not consistent accross modules
/* & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any[], sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any, sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TEntitiesDtoConfig[TEntityName]["update"][],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
idOrdSelector: any,
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
}*/
/**
* @internal
*/
@@ -263,6 +66,36 @@ function buildMethodNamesFromModel(
}, {})
}
/**
* Accessible from the MedusaService, holds the model objects when provided
*/
export const MedusaServiceModelObjectsSymbol = Symbol.for(
"MedusaServiceModelObjectsSymbol"
)
/**
* Symbol to mark a class as a Medusa service
*/
export const MedusaServiceSymbol = Symbol.for("MedusaServiceSymbol")
/**
* Accessible from the MedusaService, holds the entity name to linkable keys map
* to be used for softDelete and restore methods
*/
export const MedusaServiceEntityNameToLinkableKeysMapSymbol = Symbol.for(
"MedusaServiceEntityNameToLinkableKeysMapSymbol"
)
/**
* Check if a value is a Medusa service
* @param value
*/
export function isMedusaService(
value: any
): value is MedusaServiceReturnType<any> {
return value && value?.prototype[MedusaServiceSymbol]
}
/**
* Factory function for creating an abstract module service
*
@@ -281,26 +114,22 @@ function buildMethodNamesFromModel(
* RuleType,
* }
*
* class MyService extends ModulesSdkUtils.MedusaService(entities, entityNameToLinkableKeysMap) {}
* class MyService extends ModulesSdkUtils.MedusaService(entities) {}
*
* @param entities
* @param entityNameToLinkableKeysMap
*/
export function MedusaService<
EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
TEntities extends TEntityEntries<
const EntitiesConfig extends EntitiesConfigTemplate = { __empty: any },
const TEntities extends TEntityEntries<
ExtractKeysFromConfig<EntitiesConfig>
> = TEntityEntries<ExtractKeysFromConfig<EntitiesConfig>>
>(
entities: TEntities,
entityNameToLinkableKeysMap: MapToConfig = {}
): {
new (...args: any[]): AbstractModuleService<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>
} {
entities: TEntities
): MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
> {
const buildAndAssignMethodImpl = function (
klassPrototype: any,
method: string,
@@ -474,7 +303,7 @@ export function MedusaService<
// eg: product.id = product_id, variant.id = variant_id
const mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
this[MedusaServiceEntityNameToLinkableKeysMapSymbol],
{
pick: config.returnLinkableKeys,
}
@@ -506,7 +335,7 @@ export function MedusaService<
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
this[MedusaServiceEntityNameToLinkableKeysMapSymbol],
{
pick: config.returnLinkableKeys,
}
@@ -522,11 +351,23 @@ export function MedusaService<
}
class AbstractModuleService_ {
[MedusaServiceSymbol] = true
static [MedusaServiceModelObjectsSymbol] = Object.values(
entities
) as unknown as MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>["$modelObjects"];
[MedusaServiceEntityNameToLinkableKeysMapSymbol]: MapToConfig
readonly __container__: Record<any, any>
readonly baseRepository_: RepositoryService
readonly eventBusModuleService_: IEventBusModuleService;
readonly eventBusModuleService_: IEventBusModuleService
[key: string]: any
__joinerConfig?(): ModuleJoinerConfig
constructor(container: Record<any, any>) {
this.__container__ = container
@@ -539,6 +380,11 @@ export function MedusaService<
this.eventBusModuleService_ = hasEventBusModuleService
? this.__container__.eventBusModuleService
: undefined
this[MedusaServiceEntityNameToLinkableKeysMapSymbol] =
buildEntitiesNameToLinkableKeysMap(
this.__joinerConfig?.()?.linkableKeys ?? {}
)
}
protected async emitEvents_(groupedEvents) {
@@ -563,7 +409,7 @@ export function MedusaService<
string,
TEntities[keyof TEntities],
Record<string, string>
][] = Object.entries(entities).map(([name, config]) => [
][] = Object.entries(entities as {}).map(([name, config]) => [
name,
config as TEntities[keyof TEntities],
buildMethodNamesFromModel(name, config as TEntities[keyof TEntities]),
@@ -14,7 +14,7 @@ type Options = Partial<MikroORMOptions> & {
| EntityClass<AnyEntity>
| EntityClassGroup<AnyEntity>
| EntitySchema
| DmlEntity<any>
| DmlEntity<any, any>
)[]
databaseName: string
}
@@ -0,0 +1,45 @@
import { Constructor, ModuleExports } from "@medusajs/types"
import { MedusaServiceModelObjectsSymbol } from "./medusa-service"
import {
buildLinkConfigFromDmlObjects,
defineJoinerConfig,
} from "./joiner-config-builder"
import { InfersLinksConfig } from "./types/links-config"
import { DmlEntity } from "../dml"
/**
* Wrapper to build the module export and auto generate the joiner config if needed as well as
* return a links object based on the DML objects
* @param moduleName
* @param service
* @param loaders
* @constructor
*/
export function Module<
const Service extends Constructor<any>,
const ModelObjects extends DmlEntity<any, any>[] = Service extends {
$modelObjects: infer $DmlObjects
}
? $DmlObjects
: [],
Links = keyof ModelObjects extends never
? Record<string, any>
: InfersLinksConfig<ModelObjects>
>({
name = "",
service,
loaders,
}: ModuleExports<Service> & { name?: string }): ModuleExports<Service> & {
links: Links
} {
service.prototype.__joinerConfig ??= defineJoinerConfig(name)
const dmlObjects = service[MedusaServiceModelObjectsSymbol]
return {
service,
loaders,
links: (dmlObjects?.length
? buildLinkConfigFromDmlObjects(dmlObjects)
: {}) as Links,
}
}
@@ -0,0 +1,131 @@
import {
DMLSchema,
IDmlEntityConfig,
InferDmlEntityNameFromConfig,
SnakeCase,
} from "@medusajs/types"
import { DmlEntity } from "../../dml"
import { PrimaryKeyModifier } from "../../dml/properties/primary-key"
type FlattenUnion<T> = T extends { [K in keyof T]: infer U }
? { [K in keyof T]: U }
: never
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
type InferLinkableKeyName<
Key,
Property,
DmlConfig extends IDmlEntityConfig
> = Property extends PrimaryKeyModifier<any, any>
? `${Lowercase<SnakeCase<InferDmlEntityNameFromConfig<DmlConfig>>>}_${Key &
string}`
: never
type InferSchemaLinkableKeys<T> = T extends DmlEntity<
infer Schema,
infer Config
>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferLinkableKeyName<K, Schema[K], Config>
: never]: InferDmlEntityNameFromConfig<Config>
}
: {}
type InferSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
[K in keyof T]: InferSchemaLinkableKeys<T[K]>
}
type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
[K in keyof InferSchemasLinkableKeys<T>]: InferSchemasLinkableKeys<T>[K]
}
/**
* From an array of DmlEntity, returns a formatted object with the linkable keys
*
* @example:
*
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' }
*
*/
export type InferLinkableKeys<T extends DmlEntity<any, any>[]> =
UnionToIntersection<FlattenUnion<AggregateSchemasLinkableKeys<T>>[0]>
type InferPrimaryKeyNameOrNever<
Schema extends DMLSchema,
Key extends keyof Schema
> = Schema[Key] extends PrimaryKeyModifier<any, any> ? Key : never
type InferSchemaLinksConfig<T> = T extends DmlEntity<infer Schema, infer Config>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferPrimaryKeyNameOrNever<Schema, K>
: never]: {
linkable: InferLinkableKeyName<K, Schema[K], Config>
primaryKey: K
}
}
: {}
/**
* From an array of DmlEntity, returns a formatted object with the linkable keys
*
* @example:
*
* const user = model.define("user", {
* id: model.id(),
* name: model.text(),
* })
*
* const car = model.define("car", {
* id: model.id(),
* number_plate: model.text().primaryKey(),
* test: model.text(),
* })
*
* const linkConfig = buildLinkConfigFromDmlObjects([user, car])
* // {
* // user: {
* // id: {
* // linkable: 'user_id',
* // primaryKey: 'id'
* // },
* // toJSON() { ... }
* // },
* // car: {
* // number_plate: {
* // linkable: 'car_number_plate',
* // primaryKey: 'number_plate'
* // },
* // toJSON() { ... }
* // }
* // }
*
*/
export type InfersLinksConfig<T extends DmlEntity<any, any>[]> =
UnionToIntersection<{
[K in keyof T as T[K] extends DmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>>
: never]: InferSchemaLinksConfig<T[K]> & {
toJSON: () => {
linkable: string
primaryKey: string
}
}
}>
@@ -0,0 +1,261 @@
import {
Constructor,
Context,
FindConfig,
Pluralize,
RestoreReturn,
SoftDeleteReturn,
} from "@medusajs/types"
import { DmlEntity } from "../../dml"
export type BaseMethods =
| "retrieve"
| "list"
| "listAndCount"
| "delete"
| "softDelete"
| "restore"
| "create"
| "update"
export type ModelDTOConfig = {
dto: object
model?: DmlEntity<any, any>
create?: any
update?: any
/**
* @internal
* @deprecated
*/
singular?: string
/**
* @internal
* @deprecated
*/
plural?: string
}
export type EntitiesConfigTemplate = { [key: string]: ModelDTOConfig }
export type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
[Key in keyof T]: {
dto: T[Key] extends Constructor<any> ? InstanceType<T[Key]> : any
model: T[Key] extends { model: infer MODEL }
? MODEL
: T[Key] extends DmlEntity<any, any>
? T[Key]
: never
/**
* @deprecated
*/
create: any
update: any
/**
* @deprecated
*/
singular: T[Key] extends { singular: string } ? T[Key]["singular"] : Key
/**
* @deprecated
*/
plural: T[Key] extends { plural: string }
? T[Key]["plural"]
: Pluralize<Key & string>
}
}
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
*/
export type ExtractSingularName<
T extends Record<any, any>,
K = keyof T
> = Capitalize<
T[K] extends { singular?: string } ? T[K]["singular"] & string : K & string
>
/**
* @deprecated should all notion of singular and plural be removed once all modules are aligned with the convention
* The pluralize will move to where it should be used instead
*/
export type ExtractPluralName<
T extends Record<any, any>,
K = keyof T
> = Capitalize<
T[K] extends {
plural?: string
}
? T[K]["plural"] & string
: Pluralize<K & string>
>
// TODO: The future expected entry will be a MODEL object but in the meantime we have to maintain backward compatibility for ouw own modules and therefore we need to support Constructor<any> as well as this temporary object
export type TEntityEntries<Keys = string> = Record<
Keys & string,
| DmlEntity<any, any>
/**
* @deprecated
*/
| Constructor<any>
/**
* @deprecated
*/
| { name?: string; singular?: string; plural?: string }
>
export type ExtractKeysFromConfig<EntitiesConfig> = EntitiesConfig extends {
__empty: any
}
? string
: keyof EntitiesConfig
export type AbstractModuleService<
TEntitiesDtoConfig extends Record<string, any>
> = {
[TEntityName in keyof TEntitiesDtoConfig as `retrieve${ExtractSingularName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
id: string,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `list${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: (
filters?: any,
config?: FindConfig<any>,
sharedContext?: Context
) => Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
} & {
[TEntityName in keyof TEntitiesDtoConfig as `listAndCount${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
[TEntitiesDtoConfig[TEntityName]["dto"][], number]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `delete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
primaryKeyValues: string | object | string[] | object[],
sharedContext?: Context
): Promise<void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `softDelete${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `restore${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
<TReturnableLinkableKeys extends string>(
primaryKeyValues: string | object | string[] | object[],
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<string, string[]> | void>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(...args: any[]): Promise<any>
}
}
// TODO: Because of a bug, those methods were not made visible which now cause issues with the fix as our interface are not consistent with the expectations
// are not consistent accross modules
/* & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any[], sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `create${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(data: any, sharedContext?: Context): Promise<
TEntitiesDtoConfig[TEntityName]["dto"][]
>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TEntitiesDtoConfig[TEntityName]["update"][],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"]>
}
} & {
[TEntityName in keyof TEntitiesDtoConfig as `update${ExtractPluralName<
TEntitiesDtoConfig,
TEntityName
>}`]: {
(
idOrdSelector: any,
data: TEntitiesDtoConfig[TEntityName]["update"],
sharedContext?: Context
): Promise<TEntitiesDtoConfig[TEntityName]["dto"][]>
}
}*/
type InferModelFromConfig<T> = {
[K in keyof T as T[K] extends { model: any }
? K
: K extends DmlEntity<any, any>
? K
: never]: T[K] extends {
model: infer MODEL
}
? MODEL extends DmlEntity<any, any>
? MODEL
: never
: T[K] extends DmlEntity<any, any>
? T[K]
: never
}
export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = {
new (...args: any[]): AbstractModuleService<ModelsConfig>
$modelObjects: InferModelFromConfig<ModelsConfig>[keyof InferModelFromConfig<ModelsConfig>][]
}