* feat(index): add filterable fields to link definition * rm comment * break recursion * validate read only links * validate filterable * gql schema array * link parents * isInverse * push id when not present * Fix ciruclar relationships and add tests to ensure proper behaviour (part 1) * log and fallback to entity.alias * cleanup and fixes * cleanup and fixes * cleanup and fixes * fix get attributes * gql type * unit test * array inference * rm only * package.json * pacvkage.json * fix link retrieval on duplicated entity type and aliases + tests * link parents as array * Match only parent entity * rm comment * remove hard coded schema * extend types * unit test * test * types * pagination type * type * fix integration tests * Improve performance of in selection * use @@ to filter property * escape jsonPath * add Event Bus by default * changeset * rm postgres analyze * estimate count * new query * parent aliases * inner query w/ filter and sort relations * address comments --------- Co-authored-by: adrien2p <adrien.deperetti@gmail.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
import { MedusaModule } from "@medusajs/framework/modules-sdk"
|
|
import {
|
|
ModuleJoinerConfig,
|
|
ModuleJoinerRelationship,
|
|
} from "@medusajs/framework/types"
|
|
import {
|
|
camelToSnakeCase,
|
|
composeTableName,
|
|
isString,
|
|
lowerCaseFirst,
|
|
toPascalCase,
|
|
} from "@medusajs/framework/utils"
|
|
|
|
export function generateGraphQLSchema(
|
|
joinerConfig: ModuleJoinerConfig,
|
|
primary: ModuleJoinerRelationship,
|
|
foreign: ModuleJoinerRelationship,
|
|
{ logger }: { logger } = { logger: console }
|
|
) {
|
|
let fieldNames!: string[]
|
|
let entityName!: string
|
|
|
|
const isReadOnlyLink = joinerConfig.isReadOnlyLink
|
|
if (!isReadOnlyLink) {
|
|
fieldNames = primary.foreignKey.split(",").concat(foreign.foreignKey)
|
|
|
|
entityName = toPascalCase(
|
|
"Link_" +
|
|
(joinerConfig.databaseConfig?.tableName ??
|
|
composeTableName(
|
|
primary.serviceName,
|
|
primary.foreignKey,
|
|
foreign.serviceName,
|
|
foreign.foreignKey
|
|
))
|
|
)
|
|
}
|
|
|
|
let typeDef = ""
|
|
|
|
for (const extend of joinerConfig.extends ?? []) {
|
|
const extendedModule = MedusaModule.getModuleInstance(extend.serviceName)
|
|
if (!extendedModule) {
|
|
throw new Error(
|
|
`Module ${extend.serviceName} not found. Please verify that the module is configured and installed, also the module must be loaded before the link modules.`
|
|
)
|
|
}
|
|
|
|
let extendedEntityName =
|
|
extendedModule[extend.serviceName].__joinerConfig.linkableKeys[
|
|
extend.relationship.primaryKey
|
|
]
|
|
|
|
if (!isReadOnlyLink && (!primary || !foreign || !extendedEntityName)) {
|
|
logger.warn(
|
|
`Link modules schema: No linkable key found for ${extend.relationship.primaryKey} on module ${extend.serviceName}.`
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
const fieldName = camelToSnakeCase(
|
|
lowerCaseFirst(extend.relationship.alias)
|
|
)
|
|
|
|
let type = extend.relationship.isList ? `[${entityName}]` : entityName
|
|
if (joinerConfig?.isReadOnlyLink) {
|
|
// TODO: In readonly links, the relationship of the extend where entity is undefined has to be applied on all entities in the module that have the relationshiop foreing key attribute (unkown in this context)
|
|
if (!extend.entity) {
|
|
continue
|
|
}
|
|
|
|
const rel = extend.relationship
|
|
const extendedService = MedusaModule.getModuleInstance(rel.serviceName)
|
|
|
|
const hasGraphqlSchema =
|
|
!!extendedService[rel.serviceName].__joinerConfig.schema
|
|
const relEntity = rel.entity
|
|
? rel.entity
|
|
: extendedService[rel.serviceName].__joinerConfig.linkableKeys[
|
|
rel.primaryKey
|
|
]
|
|
|
|
if (!relEntity || !hasGraphqlSchema) {
|
|
continue
|
|
}
|
|
|
|
type = rel.isList ? `[${relEntity}]` : relEntity!
|
|
extendedEntityName = extend.entity
|
|
}
|
|
|
|
/**
|
|
* Find the field aliases shortcut to extend the entity with it
|
|
*/
|
|
const fieldsAliasesField = Object.entries(extend.fieldAlias || {})
|
|
.map(([field, config]) => {
|
|
const path = isString(config) ? config : config.path
|
|
const isList = isString(config)
|
|
? extend.relationship.isList
|
|
: config.isList ?? extend.relationship.isList
|
|
|
|
const targetEntityAlias = path.split(".").pop()
|
|
|
|
const targetEntityRelation = joinerConfig.relationships?.find(
|
|
(relation) => relation.alias === targetEntityAlias
|
|
)
|
|
|
|
if (!targetEntityRelation) {
|
|
return
|
|
}
|
|
|
|
const targetEntityName = MedusaModule.getJoinerConfig(
|
|
targetEntityRelation.serviceName
|
|
).linkableKeys?.[targetEntityRelation.foreignKey]
|
|
|
|
if (!targetEntityName) {
|
|
logger.warn(
|
|
`Link modules schema: No linkable key found for ${targetEntityRelation.foreignKey} on module ${targetEntityRelation.serviceName}.`
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// TODO: Re visit field aliases that access properties from a type
|
|
/*const targetEntityType = `${targetEntityName}${
|
|
relationshipPropertyPath.length
|
|
? relationshipPropertyPath.reduce((acc, value) => {
|
|
return `${acc}[${value}]`
|
|
}, targetEntityName)
|
|
: ""
|
|
}`*/
|
|
|
|
return `${field}: ${
|
|
isList ? `[${targetEntityName}]` : targetEntityName
|
|
}`
|
|
})
|
|
.filter(Boolean)
|
|
|
|
typeDef += `
|
|
extend type ${extendedEntityName} {
|
|
${fieldName}: ${type}
|
|
|
|
${fieldsAliasesField.join("\n")}
|
|
}
|
|
`
|
|
}
|
|
|
|
if (isReadOnlyLink) {
|
|
return typeDef
|
|
}
|
|
|
|
// Pivot table fields
|
|
const fields = fieldNames.reduce((acc, curr) => {
|
|
acc[curr] = {
|
|
type: "String",
|
|
nullable: false,
|
|
}
|
|
return acc
|
|
}, {})
|
|
|
|
const extraFields = joinerConfig.databaseConfig?.extraFields ?? {}
|
|
|
|
for (const column in extraFields) {
|
|
fields[column] = {
|
|
type: getGraphQLType(extraFields[column].type),
|
|
nullable: !!extraFields[column].nullable,
|
|
}
|
|
}
|
|
|
|
// TODO: temporary, every module might always expose their schema
|
|
const doesPrimaryExportSchema = !!MedusaModule.getJoinerConfig(
|
|
primary.serviceName
|
|
)?.schema
|
|
const doesForeignExportSchema = !!MedusaModule.getJoinerConfig(
|
|
foreign.serviceName
|
|
)?.schema
|
|
|
|
// Link table relationships
|
|
const primaryField = doesPrimaryExportSchema
|
|
? `${camelToSnakeCase(primary.alias)}: ${toPascalCase(
|
|
primary.entity ?? composeTableName(primary.serviceName)
|
|
)}`
|
|
: ""
|
|
|
|
const foreignField = doesForeignExportSchema
|
|
? `${camelToSnakeCase(foreign.alias)}: ${toPascalCase(
|
|
foreign.entity ?? composeTableName(foreign.serviceName)
|
|
)}`
|
|
: ""
|
|
|
|
typeDef += `
|
|
type ${entityName} {
|
|
${(Object.entries(fields) as any)
|
|
.map(
|
|
([field, { type, nullable }]) =>
|
|
`${field}: ${nullable ? type : `${type}!`}`
|
|
)
|
|
.join("\n ")}
|
|
|
|
${primaryField}
|
|
${foreignField}
|
|
|
|
createdAt: String!
|
|
updatedAt: String!
|
|
deletedAt: String
|
|
}
|
|
`
|
|
|
|
return typeDef
|
|
}
|
|
|
|
function getGraphQLType(type) {
|
|
const typeDef = {
|
|
numeric: "Float",
|
|
integer: "Int",
|
|
smallint: "Int",
|
|
tinyint: "Int",
|
|
mediumint: "Int",
|
|
float: "Float",
|
|
double: "Float",
|
|
boolean: "Boolean",
|
|
decimal: "Float",
|
|
string: "String",
|
|
uuid: "ID",
|
|
text: "String",
|
|
date: "Date",
|
|
time: "Time",
|
|
datetime: "DateTime",
|
|
bigint: "BigInt",
|
|
blob: "Blob",
|
|
uint8array: "[Int]",
|
|
array: "[String]",
|
|
enumArray: "[String]",
|
|
enum: "String",
|
|
json: "JSON",
|
|
jsonb: "JSON",
|
|
}
|
|
|
|
return typeDef[type] ?? "String"
|
|
}
|