diff --git a/integration-tests/modules/__tests__/link-modules/define-link.spec.ts b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts index cde78ff706..64130a6284 100644 --- a/integration-tests/modules/__tests__/link-modules/define-link.spec.ts +++ b/integration-tests/modules/__tests__/link-modules/define-link.spec.ts @@ -76,6 +76,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "currency_code", foreignKey: "code", alias: "region_link", @@ -93,6 +94,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "region_id", foreignKey: "id", alias: "currency_link", @@ -171,6 +173,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "ProductProductVariantRegionRegionLink", + entity: "LinkProductProductVariantRegionRegion", primaryKey: "product_variant_id", foreignKey: "id", alias: "region_link", @@ -190,6 +193,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "ProductProductVariantRegionRegionLink", + entity: "LinkProductProductVariantRegionRegion", primaryKey: "region_id", foreignKey: "id", alias: "product_variant_link", @@ -271,6 +275,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "currency_code", foreignKey: "code", alias: "region_link", @@ -288,6 +293,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "region_id", foreignKey: "id", alias: "currency_link", @@ -365,6 +371,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "currency_code", foreignKey: "code", alias: "region_link", @@ -382,6 +389,7 @@ medusaIntegrationTestRunner({ }, relationship: { serviceName: "CurrencyCurrencyRegionRegionLink", + entity: "LinkCurrencyCurrencyRegionRegion", primaryKey: "region_id", foreignKey: "id", alias: "currency_link", diff --git a/packages/core/utils/src/modules-sdk/define-link.ts b/packages/core/utils/src/modules-sdk/define-link.ts index 68bb8ad36e..af598e5e64 100644 --- a/packages/core/utils/src/modules-sdk/define-link.ts +++ b/packages/core/utils/src/modules-sdk/define-link.ts @@ -7,6 +7,7 @@ export const DefineLinkSymbol = Symbol.for("DefineLink") export interface DefineLinkExport { [DefineLinkSymbol]: boolean serviceName: string + entity?: string entryPoint: string } @@ -19,6 +20,17 @@ type InputSource = { primaryKey: string } +type ReadOnlyInputSource = { + linkable: + | CombinedSource + | InputSource + | { + serviceName: string + entity?: string + } + field?: string +} + type InputToJson = { toJSON: () => InputSource } @@ -27,10 +39,18 @@ type CombinedSource = Record & InputToJson type InputOptions = { linkable: CombinedSource | InputSource + field?: string isList?: boolean deleteCascade?: boolean } +type Shortcut = { + property: string + path: string + isList?: boolean + forwardArguments?: string | string[] +} + type ExtraOptions = { pk?: { [key: string]: string @@ -40,10 +60,22 @@ type ExtraOptions = { idPrefix?: string extraColumns?: LinkModulesExtraFields } + readOnly?: boolean +} + +type ReadOnlyExtraOptions = { + readOnly: true + isList?: boolean + shortcut?: Shortcut | Shortcut[] } type DefineLinkInputSource = InputSource | InputOptions | CombinedSource +type DefineReadOnlyLinkInputSource = + | ReadOnlyInputSource + | InputOptions + | CombinedSource + type ModuleLinkableKeyConfig = { module: string entity?: string @@ -53,9 +85,7 @@ type ModuleLinkableKeyConfig = { deleteCascade?: boolean primaryKey: string alias: string - shortcuts?: { - [key: string]: string | { path: string; isList?: boolean } - } + shortcut?: Shortcut | Shortcut[] } function isInputOptions(input: any): input is InputOptions { @@ -70,7 +100,33 @@ function isToJSON(input: any): input is InputToJson { return isObject(input) && input?.["toJSON"] } -function prepareServiceConfig(input: DefineLinkInputSource) { +function buildFieldAlias(fieldAliases?: Shortcut | Shortcut[]) { + if (!fieldAliases) { + return + } + + const fieldAlias = {} + + const shortcuts = Array.isArray(fieldAliases) ? fieldAliases : [fieldAliases] + for (const sc of shortcuts) { + const fwArgs = sc.forwardArguments + ? Array.isArray(sc.forwardArguments) + ? sc.forwardArguments + : [sc.forwardArguments] + : [] + fieldAlias[sc.property] = { + path: sc.path, + isList: !!sc.isList, + forwardArgumentsOnPath: fwArgs, + } + } + + return fieldAlias +} + +function prepareServiceConfig( + input: DefineLinkInputSource | DefineReadOnlyLinkInputSource +) { let serviceConfig = {} as ModuleLinkableKeyConfig if (isInputSource(input)) { @@ -78,8 +134,8 @@ function prepareServiceConfig(input: DefineLinkInputSource) { serviceConfig = { key: source.linkable, - alias: source.alias ?? camelToSnakeCase(source.field), - field: source.field, + alias: source.alias ?? camelToSnakeCase(source.field ?? ""), + field: input.field ?? source.field, primaryKey: source.primaryKey, isList: false, deleteCascade: false, @@ -93,8 +149,8 @@ function prepareServiceConfig(input: DefineLinkInputSource) { serviceConfig = { key: source.linkable, - alias: source.alias ?? camelToSnakeCase(source.field), - field: source.field, + alias: source.alias ?? camelToSnakeCase(source.field ?? ""), + field: input.field ?? source.field, primaryKey: source.primaryKey, isList: input.isList ?? false, deleteCascade: input.deleteCascade ?? false, @@ -123,14 +179,27 @@ function prepareServiceConfig(input: DefineLinkInputSource) { * @param linkServiceOptions */ export function defineLink( - leftService: DefineLinkInputSource, - rightService: DefineLinkInputSource, - linkServiceOptions?: ExtraOptions + leftService: DefineLinkInputSource | DefineReadOnlyLinkInputSource, + rightService: DefineLinkInputSource | DefineReadOnlyLinkInputSource, + linkServiceOptions?: ExtraOptions | ReadOnlyExtraOptions ): DefineLinkExport { const serviceAObj = prepareServiceConfig(leftService) const serviceBObj = prepareServiceConfig(rightService) - const output = { [DefineLinkSymbol]: true, serviceName: "", entryPoint: "" } + if (linkServiceOptions?.readOnly) { + return defineReadOnlyLink( + serviceAObj, + serviceBObj, + linkServiceOptions as ReadOnlyExtraOptions + ) as unknown as DefineLinkExport + } + + const output = { + [DefineLinkSymbol]: true, + serviceName: "", + entity: "", + entryPoint: "", + } const register = function ( modules: ModuleJoinerConfig[] @@ -276,6 +345,9 @@ ${serviceBObj.module}: { ) output.entryPoint = aliasA + "_" + aliasB + output.entity = toPascalCase( + ["Link", serviceAObj.module, aliasA, serviceBObj.module, aliasB].join("_") + ) const linkDefinition: ModuleJoinerConfig = { serviceName: output.serviceName, @@ -284,15 +356,7 @@ ${serviceBObj.module}: { { name: [output.entryPoint], args: { - entity: toPascalCase( - [ - "Link", - serviceAObj.module, - aliasA, - serviceBObj.module, - aliasB, - ].join("_") - ), + entity: output.entity, }, }, ], @@ -324,15 +388,15 @@ ${serviceBObj.module}: { extends: [ { serviceName: serviceAObj.module, - fieldAlias: { - [serviceBObj.isList ? pluralize(aliasB) : aliasB]: { - path: aliasB + "_link." + aliasB, - isList: serviceBObj.isList, - forwardArgumentsOnPath: [aliasB + "_link." + aliasB], - }, - }, + fieldAlias: buildFieldAlias({ + property: serviceBObj.isList ? pluralize(aliasB) : aliasB, + path: aliasB + "_link." + aliasB, + isList: serviceBObj.isList, + forwardArguments: [aliasB + "_link." + aliasB], + }), relationship: { serviceName: output.serviceName, + entity: output.entity, primaryKey: serviceAObj.key, foreignKey: serviceAPrimaryKey, alias: aliasB + "_link", @@ -341,15 +405,15 @@ ${serviceBObj.module}: { }, { serviceName: serviceBObj.module, - fieldAlias: { - [serviceAObj.isList ? pluralize(aliasA) : aliasA]: { - path: aliasA + "_link." + aliasA, - isList: serviceAObj.isList, - forwardArgumentsOnPath: [aliasA + "_link." + aliasA], - }, - }, + fieldAlias: buildFieldAlias({ + property: serviceAObj.isList ? pluralize(aliasA) : aliasA, + path: aliasA + "_link." + aliasA, + isList: serviceAObj.isList, + forwardArguments: [aliasA + "_link." + aliasA], + }), relationship: { serviceName: output.serviceName, + entity: output.entity, primaryKey: serviceBObj.key, foreignKey: serviceBPrimaryKey, alias: aliasA + "_link", @@ -375,3 +439,62 @@ ${serviceBObj.module}: { return output } + +function defineReadOnlyLink( + serviceAObj: ModuleLinkableKeyConfig, + serviceBObj: ModuleLinkableKeyConfig, + readOnlyLinkOptions?: ReadOnlyExtraOptions +): void { + const register = function ( + modules: ModuleJoinerConfig[] + ): ModuleJoinerConfig { + const serviceAInfo = modules.find( + (mod) => mod.serviceName === serviceAObj.module + )! + const serviceBInfo = modules.find( + (mod) => mod.serviceName === serviceBObj.module + )! + + if (!serviceAInfo) { + throw new Error(`Service ${serviceAObj.module} was not found. If this is your module, make sure you set isQueryable to true in medusa-config.js: + +${serviceAObj.module}: { + // ... + definition: { + isQueryable: true + } +}`) + } + if (!serviceBInfo) { + throw new Error(`Service ${serviceBObj.module} was not found. If this is your module, make sure you set isQueryable to true in medusa-config.js: + +${serviceBObj.module}: { + // ... + definition: { + isQueryable: true + } +}`) + } + + return { + isLink: true, + isReadOnlyLink: true, + extends: [ + { + serviceName: serviceAObj.module, + fieldAlias: buildFieldAlias(readOnlyLinkOptions?.shortcut), + relationship: { + serviceName: serviceBObj.module, + entity: serviceBObj.entity, + primaryKey: serviceBObj.primaryKey, + foreignKey: serviceAObj.field, + alias: serviceBObj.alias, + isList: readOnlyLinkOptions?.isList ?? serviceAObj.isList, + }, + }, + ], + } + } + + global.MedusaModule.setCustomLink(register) +}