feat: define link util (#7931)

* feat: define link util

* handle pluralized fieldAlias for isList

* serviceName ar reference

* finalize

* todo

* WIP

* finalize

* fix tests

* update typings

* fix Module

* linkable

* update errors
This commit is contained in:
Adrien de Peretti
2024-07-04 10:37:30 +02:00
committed by GitHub
parent 9b1688944e
commit 41c4307fc7
26 changed files with 523 additions and 341 deletions
@@ -25,6 +25,7 @@ import {
ShippingOptionRule,
ShippingProfile,
} from "../__fixtures__/joiner-config/entities"
import { upperCaseFirst } from "../../common"
describe("joiner-config-builder", () => {
describe("defineJoiner | Mikro orm objects", () => {
@@ -420,14 +421,15 @@ describe("joiner-config-builder", () => {
})
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,
user_id: upperCaseFirst(user.name),
car_number_plate: upperCaseFirst(car.name),
})
})
})
@@ -453,12 +455,18 @@ describe("joiner-config-builder", () => {
name: model.text(),
})
const car = model.define("car", {
id: model.id(),
number_plate: model.text().primaryKey(),
})
const car = model.define(
{ name: "car", tableName: "car" },
{
id: model.id(),
number_plate: model.text().primaryKey(),
}
)
const linkConfig = buildLinkConfigFromDmlObjects("myService", [user, car])
const linkConfig = buildLinkConfigFromDmlObjects("myService", {
user,
car,
})
expectTypeOf(linkConfig).toMatchTypeOf<{
user: {
@@ -0,0 +1,316 @@
import { LinkModulesExtraFields, ModuleJoinerConfig } from "@medusajs/types"
import { isObject, pluralize, toPascalCase } from "../common"
import { composeLinkName } from "../link"
type InputSource = {
serviceName: string
field: string
linkable: string
primaryKey: string
}
type InputToJson = {
toJSON: () => InputSource
}
type CombinedSource = Record<any, any> & InputToJson
type InputOptions = {
source: CombinedSource | InputSource
isList?: boolean
}
type ExtraOptions = {
pk?: {
[key: string]: string
}
database?: {
table: string
idPrefix?: string
extraColumns?: LinkModulesExtraFields
}
}
type DefineLinkInputSource = InputSource | InputOptions | CombinedSource
type ModuleLinkableKeyConfig = {
module: string
key: string
isList?: boolean
primaryKey: string
alias: string
shortcuts?: {
[key: string]: string | { path: string; isList?: boolean }
}
}
function isInputOptions(input: any): input is InputOptions {
return isObject(input) && "source" in input
}
function isInputSource(input: any): input is InputSource {
return (isObject(input) && "serviceName" in input) || "toJSON" in input
}
function isToJSON(input: any): input is InputToJson {
return isObject(input) && "toJSON" in input
}
export function defineLink(
leftService: DefineLinkInputSource,
rightService: DefineLinkInputSource,
linkServiceOptions?: ExtraOptions
) {
let serviceAObj = {} as ModuleLinkableKeyConfig
let serviceBObj = {} as ModuleLinkableKeyConfig
if (isInputSource(leftService)) {
const source = isToJSON(leftService) ? leftService.toJSON() : leftService
serviceAObj = {
key: source.linkable,
alias: source.field,
primaryKey: source.primaryKey,
isList: false,
module: source.serviceName,
}
} else if (isInputOptions(leftService)) {
const source = isToJSON(leftService.source)
? leftService.source.toJSON()
: leftService.source
serviceAObj = {
key: source.linkable,
alias: source.field,
primaryKey: source.primaryKey,
isList: leftService.isList ?? false,
module: source.serviceName,
}
} else {
throw new Error("Invalid linkable passed for the first argument")
}
if (isInputSource(rightService)) {
const source = isToJSON(rightService) ? rightService.toJSON() : rightService
serviceBObj = {
key: source.linkable,
alias: source.field,
primaryKey: source.primaryKey,
isList: false,
module: source.serviceName,
}
} else if (isInputOptions(rightService)) {
const source = isToJSON(rightService.source)
? rightService.source.toJSON()
: rightService.source
serviceBObj = {
key: source.linkable,
alias: source.field,
primaryKey: source.primaryKey,
isList: rightService.isList ?? false,
module: source.serviceName,
}
} else {
throw new Error(`Invalid linkable passed for the second argument`)
}
const output = { serviceName: "" }
const register = function (
modules: ModuleJoinerConfig[]
): ModuleJoinerConfig {
const serviceAInfo = modules
.map((mod) => mod[serviceAObj.module])
.filter(Boolean)[0]
const serviceBInfo = modules
.map((mod) => mod[serviceBObj.module])
.filter(Boolean)[0]
if (!serviceAInfo) {
throw new Error(`Service ${serviceAObj.module} was not found`)
}
if (!serviceBInfo) {
throw new Error(`Service ${serviceBObj.module} was not found`)
}
const serviceAKeyInfo =
serviceAInfo.__joinerConfig.linkableKeys?.[serviceAObj.key]
const serviceBKeyInfo =
serviceBInfo.__joinerConfig.linkableKeys?.[serviceBObj.key]
if (!serviceAKeyInfo) {
throw new Error(
`Key ${serviceAObj.key} is not linkable on service ${serviceAObj.module}`
)
}
if (!serviceBKeyInfo) {
throw new Error(
`Key ${serviceBObj.key} is not linkable on service ${serviceBObj.module}`
)
}
let serviceAAliases = serviceAInfo.__joinerConfig.alias ?? []
if (!Array.isArray(serviceAAliases)) {
serviceAAliases = [serviceAAliases]
}
let aliasAOptions =
serviceAObj.alias ??
serviceAAliases.find((a) => {
return a.args?.entity == serviceAKeyInfo
})?.name
let aliasA = aliasAOptions
if (Array.isArray(aliasAOptions)) {
aliasA = aliasAOptions[0]
}
if (!aliasA) {
throw new Error(
`You need to provide an alias for ${serviceAObj.module}.${serviceAObj.key}`
)
}
let serviceBAliases = serviceBInfo.__joinerConfig.alias ?? []
if (!Array.isArray(serviceBAliases)) {
serviceBAliases = [serviceBAliases]
}
let aliasBOptions =
serviceBObj.alias ??
serviceBAliases.find((a) => {
return a.args?.entity == serviceBKeyInfo
})?.name
let aliasB = aliasBOptions
if (Array.isArray(aliasBOptions)) {
aliasB = aliasBOptions[0]
}
if (!aliasB) {
throw new Error(
`You need to provide an alias for ${serviceBObj.module}.${serviceBObj.key}`
)
}
const moduleAPrimaryKeys = serviceAInfo.__joinerConfig.primaryKeys
let serviceAPrimaryKey =
serviceAObj.primaryKey ??
linkServiceOptions?.pk?.[serviceAObj.module] ??
moduleAPrimaryKeys
if (Array.isArray(serviceAPrimaryKey)) {
serviceAPrimaryKey = serviceAPrimaryKey[0]
}
const isModuleAPrimaryKeyValid =
moduleAPrimaryKeys.includes(serviceAPrimaryKey)
if (!isModuleAPrimaryKeyValid) {
throw new Error(
`Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}`
)
}
const moduleBPrimaryKeys = serviceBInfo.__joinerConfig.primaryKeys
let serviceBPrimaryKey =
serviceBObj.primaryKey ??
linkServiceOptions?.pk?.[serviceBObj.module] ??
moduleBPrimaryKeys
if (Array.isArray(serviceBPrimaryKey)) {
serviceBPrimaryKey = serviceBPrimaryKey[0]
}
const isModuleBPrimaryKeyValid =
moduleBPrimaryKeys.includes(serviceBPrimaryKey)
if (!isModuleBPrimaryKeyValid) {
throw new Error(
`Primary key ${serviceBPrimaryKey} is not defined on service ${serviceBObj.module}`
)
}
output.serviceName = composeLinkName(
serviceAObj.module,
aliasA,
serviceBObj.module,
aliasB
)
const linkDefinition: ModuleJoinerConfig = {
serviceName: output.serviceName,
isLink: true,
alias: [
{
name: [aliasA + "_" + aliasB],
args: {
entity: toPascalCase(
[
"Link",
serviceAObj.module,
aliasA,
serviceBObj.module,
aliasB,
].join("_")
),
},
},
],
primaryKeys: ["id", serviceAObj.key, serviceBObj.key],
relationships: [
{
serviceName: serviceAObj.module,
primaryKey: serviceAPrimaryKey,
foreignKey: serviceAObj.key,
alias: aliasA,
},
{
serviceName: serviceBObj.module,
primaryKey: serviceBPrimaryKey!,
foreignKey: serviceBObj.key,
alias: aliasB,
},
],
extends: [
{
serviceName: serviceAObj.module,
fieldAlias: {
[serviceBObj.isList ? pluralize(aliasB) : aliasB]:
aliasB + "_link." + aliasB, //plural aliasA
},
relationship: {
serviceName: output.serviceName,
primaryKey: serviceBObj.key,
foreignKey: serviceBPrimaryKey,
alias: aliasB + "_link", // plural alias
isList: serviceBObj.isList,
},
},
{
serviceName: serviceBObj.module,
fieldAlias: {
[serviceAObj.isList ? pluralize(aliasA) : aliasA]:
aliasA + "_link." + aliasA,
},
relationship: {
serviceName: output.serviceName,
primaryKey: serviceAObj.key,
foreignKey: serviceAPrimaryKey,
alias: aliasA + "_link", // plural alias
isList: serviceAObj.isList,
},
},
],
}
if (linkServiceOptions?.database) {
const { table, idPrefix, extraColumns } = linkServiceOptions.database
linkDefinition.databaseConfig = {
tableName: table,
idPrefix,
extraFields: extraColumns,
}
}
return linkDefinition
}
global.MedusaModule.setCustomLink(register)
return output
}
@@ -14,3 +14,4 @@ export * from "./medusa-service"
export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder"
export * from "./module"
export * from "./define-link"
@@ -1,4 +1,5 @@
import {
IDmlEntity,
JoinerServiceConfigAlias,
ModuleJoinerConfig,
PropertyType,
@@ -108,13 +109,14 @@ export function defineJoinerConfig(
}
if (!primaryKeys && modelDefinitions.size) {
const linkConfig = buildLinkConfigFromDmlObjects(serviceName, [
...modelDefinitions.values(),
])
const linkConfig = buildLinkConfigFromDmlObjects(
serviceName,
Object.fromEntries(modelDefinitions)
)
primaryKeys = deduplicate(
Object.values(linkConfig).flatMap((entityLinkConfig) => {
return (Object.values(entityLinkConfig) as any[])
return (Object.values(entityLinkConfig as any) as any[])
.filter((linkableConfig) => isObject(linkableConfig))
.map((linkableConfig) => {
return linkableConfig.primaryKey
@@ -152,7 +154,7 @@ export function defineJoinerConfig(
`${pluralize(camelToSnakeCase(entity.name).toLowerCase())}`,
],
args: {
entity: entity.name,
entity: upperCaseFirst(entity.name),
methodSuffix: pluralize(upperCaseFirst(entity.name)),
},
})),
@@ -175,6 +177,8 @@ export function defineJoinerConfig(
* test: model.text(),
* })
*
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car])
*
* // output:
* // {
* // user_id: 'User',
@@ -213,7 +217,7 @@ export function buildLinkableKeysFromDmlObjects<
if (primaryKeys.length) {
primaryKeys.forEach((primaryKey) => {
linkableKeys[primaryKey] = model.name
linkableKeys[primaryKey] = upperCaseFirst(model.name)
})
}
}
@@ -250,7 +254,7 @@ export function buildLinkableKeysFromMikroOrmObjects(
* test: model.text(),
* })
*
* const links = buildLinkConfigFromDmlObjects('userService', [user, car])
* const links = buildLinkConfigFromDmlObjects('userService', { user, car })
*
* // output:
* // {
@@ -279,19 +283,17 @@ export function buildLinkableKeysFromMikroOrmObjects(
*/
export function buildLinkConfigFromDmlObjects<
const ServiceName extends string,
const T extends DmlEntity<any, any>[]
>(
serviceName: ServiceName,
models: T = [] as unknown as T
): InfersLinksConfig<ServiceName, T> {
const T extends Record<string, IDmlEntity<any, any>>
>(serviceName: ServiceName, models: T): InfersLinksConfig<ServiceName, T> {
const linkConfig = {} as InfersLinksConfig<ServiceName, T>
for (const model of models) {
for (const model of Object.values(models) ?? []) {
if (!DmlEntity.isDmlEntity(model)) {
continue
}
const schema = model.schema
// @ts-ignore
const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= {
toJSON: function () {
const linkables = Object.entries(this)
@@ -322,7 +324,7 @@ export function buildLinkConfigFromDmlObjects<
}
}
return linkConfig as InfersLinksConfig<ServiceName, T> & Record<any, any>
return linkConfig as InfersLinksConfig<ServiceName, T>
}
/**
@@ -353,13 +353,12 @@ export function MedusaService<
class AbstractModuleService_ {
[MedusaServiceSymbol] = true
static [MedusaServiceModelObjectsSymbol] = Object.values(
entities
) as unknown as MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>["$modelObjects"];
static [MedusaServiceModelObjectsSymbol] =
entities as unknown as MedusaServiceReturnType<
EntitiesConfig extends { __empty: any }
? ModelConfigurationsToConfigTemplate<TEntities>
: EntitiesConfig
>["$modelObjects"];
[MedusaServiceEntityNameToLinkableKeysMapSymbol]: MapToConfig
+15 -12
View File
@@ -1,11 +1,10 @@
import { Constructor, ModuleExports } from "@medusajs/types"
import { Constructor, IDmlEntity, 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
@@ -19,28 +18,32 @@ import { DmlEntity } from "../dml"
export function Module<
const ServiceName extends string,
const Service extends Constructor<any>,
const ModelObjects extends DmlEntity<any, any>[] = Service extends {
$modelObjects: infer $DmlObjects
ModelObjects extends Record<string, IDmlEntity<any, any>> = Service extends {
$modelObjects: any
}
? $DmlObjects
: [],
Links = keyof ModelObjects extends never
? Service["$modelObjects"]
: {},
Linkable = keyof ModelObjects extends never
? Record<string, any>
: InfersLinksConfig<ServiceName, ModelObjects>
>(
serviceName: ServiceName,
{ service, loaders }: ModuleExports<Service>
): ModuleExports<Service> & {
links: Links
linkable: Linkable
} {
service.prototype.__joinerConfig ??= defineJoinerConfig(serviceName)
const dmlObjects = service[MedusaServiceModelObjectsSymbol]
const dmlObjects = service[MedusaServiceModelObjectsSymbol] ?? {}
return {
service,
loaders,
links: (dmlObjects?.length
? buildLinkConfigFromDmlObjects<ServiceName, ModelObjects>(dmlObjects)
: {}) as Links,
linkable: (Object.keys(dmlObjects)?.length
? buildLinkConfigFromDmlObjects<ServiceName, ModelObjects>(
serviceName,
dmlObjects
)
: {}) as Linkable,
}
}
@@ -1,10 +1,11 @@
import {
DMLSchema,
IDmlEntity,
IDmlEntityConfig,
InferDmlEntityNameFromConfig,
Prettify,
SnakeCase,
} from "@medusajs/types"
import { DmlEntity } from "../../dml"
import { PrimaryKeyModifier } from "../../dml/properties/primary-key"
/**
@@ -54,22 +55,22 @@ type InferLinkableKeyName<
string}`
: never
type InferSchemaLinkableKeys<T> = T extends DmlEntity<
type InferSchemaLinkableKeys<T> = T extends IDmlEntity<
infer Schema,
infer Config
>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferLinkableKeyName<K, Schema[K], Config>
: never]: InferDmlEntityNameFromConfig<Config>
: never]: Capitalize<InferDmlEntityNameFromConfig<Config>>
}
: {}
type InferSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
type InferSchemasLinkableKeys<T extends IDmlEntity<any, any>[]> = {
[K in keyof T]: InferSchemaLinkableKeys<T[K]>
}
type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
type AggregateSchemasLinkableKeys<T extends IDmlEntity<any, any>[]> = {
[K in keyof InferSchemasLinkableKeys<T>]: InferSchemasLinkableKeys<T>[K]
}
@@ -92,7 +93,7 @@ type AggregateSchemasLinkableKeys<T extends DmlEntity<any, any>[]> = {
* const linkableKeys = buildLinkableKeysFromDmlObjects([user, car]) // { user_id: 'user', car_number_plate: 'car' }
*
*/
export type InferLinkableKeys<T extends DmlEntity<any, any>[]> =
export type InferLinkableKeys<T extends IDmlEntity<any, any>[]> =
UnionToIntersection<FlattenUnion<AggregateSchemasLinkableKeys<T>>[0]>
/**
@@ -141,14 +142,12 @@ type InferPrimaryKeyNameOrNever<
type InferSchemaLinksConfig<
ServiceName extends string,
T
> = T extends DmlEntity<infer Schema, infer Config>
> = T extends IDmlEntity<infer Schema, infer Config>
? {
[K in keyof Schema as Schema[K] extends PrimaryKeyModifier<any, any>
? InferPrimaryKeyNameOrNever<Schema, K>
: never]: {
[K in keyof Schema as InferPrimaryKeyNameOrNever<Schema, K>]: {
serviceName: ServiceName
field: T extends DmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>>
field: T extends IDmlEntity<any, infer Config>
? InferDmlEntityNameFromConfig<Config>
: string
linkable: InferLinkableKeyName<K, Schema[K], Config>
primaryKey: K
@@ -200,20 +199,22 @@ type InferSchemaLinksConfig<
*/
export type InfersLinksConfig<
ServiceName extends string,
T extends DmlEntity<any, any>[]
> = UnionToIntersection<{
[K in keyof T as T[K] extends DmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>>
: never]: InferSchemaLinksConfig<ServiceName, T[K]> & {
toJSON: () => {
serviceName: ServiceName
field: T[K] extends DmlEntity<any, infer Config>
? Uncapitalize<InferDmlEntityNameFromConfig<Config>>
: string
linkable: InferLastLinkable<ServiceName, T[K]>
primaryKey: InferLastPrimaryKey<ServiceName, T[K]>
T extends Record<string, IDmlEntity<any, any>>
> = Prettify<{
[K in keyof T as T[K] extends IDmlEntity<any, infer Config>
? InferDmlEntityNameFromConfig<Config>
: never]: Prettify<
InferSchemaLinksConfig<ServiceName, T[K]> & {
toJSON: () => {
serviceName: ServiceName
field: T[K] extends IDmlEntity<any, infer Config>
? InferDmlEntityNameFromConfig<Config>
: string
linkable: InferLastLinkable<ServiceName, T[K]>
primaryKey: InferLastPrimaryKey<ServiceName, T[K]>
}
}
}
>
}>
/**
@@ -2,6 +2,7 @@ import {
Constructor,
Context,
FindConfig,
IDmlEntity,
Pluralize,
RestoreReturn,
SoftDeleteReturn,
@@ -42,7 +43,7 @@ export type ModelConfigurationsToConfigTemplate<T extends TEntityEntries> = {
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] extends IDmlEntity<any, any>
? T[Key]
: never
/**
@@ -242,20 +243,20 @@ export type AbstractModuleService<
type InferModelFromConfig<T> = {
[K in keyof T as T[K] extends { model: any }
? K
: K extends DmlEntity<any, any>
: K extends IDmlEntity<any, any>
? K
: never]: T[K] extends {
model: infer MODEL
}
? MODEL extends DmlEntity<any, any>
? MODEL extends IDmlEntity<any, any>
? MODEL
: never
: T[K] extends DmlEntity<any, any>
: T[K] extends IDmlEntity<any, any>
? T[K]
: never
}
export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = {
new (...args: any[]): AbstractModuleService<ModelsConfig>
$modelObjects: InferModelFromConfig<ModelsConfig>[keyof InferModelFromConfig<ModelsConfig>][]
$modelObjects: InferModelFromConfig<ModelsConfig>
}