feat(index): Index module foundation (#9095)
**What** Index module foundation Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos R. L. Rodrigues
parent
3cfcd075ae
commit
58167b5dfa
@@ -0,0 +1,734 @@
|
||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||
import {
|
||||
cleanGraphQLSchema,
|
||||
gqlGetFieldsAndRelations,
|
||||
MedusaModule,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
JoinerServiceConfigAlias,
|
||||
ModuleJoinerConfig,
|
||||
ModuleJoinerRelationship,
|
||||
} from "@medusajs/types"
|
||||
import { CommonEvents } from "@medusajs/utils"
|
||||
import {
|
||||
SchemaObjectEntityRepresentation,
|
||||
SchemaObjectRepresentation,
|
||||
schemaObjectRepresentationPropertiesToOmit,
|
||||
} from "@types"
|
||||
import { Kind, ObjectTypeDefinitionNode } from "graphql/index"
|
||||
|
||||
export const CustomDirectives = {
|
||||
Listeners: {
|
||||
configurationPropertyName: "listeners",
|
||||
isRequired: true,
|
||||
name: "Listeners",
|
||||
directive: "@Listeners",
|
||||
definition: "directive @Listeners (values: [String!]) on OBJECT",
|
||||
},
|
||||
}
|
||||
|
||||
function makeSchemaExecutable(inputSchema: string) {
|
||||
const { schema: cleanedSchema } = cleanGraphQLSchema(inputSchema)
|
||||
|
||||
return makeExecutableSchema({ typeDefs: cleanedSchema })
|
||||
}
|
||||
|
||||
function extractNameFromAlias(
|
||||
alias: JoinerServiceConfigAlias | JoinerServiceConfigAlias[]
|
||||
) {
|
||||
const alias_ = Array.isArray(alias) ? alias[0] : alias
|
||||
const names = Array.isArray(alias_?.name) ? alias_?.name : [alias_?.name]
|
||||
return names[0]
|
||||
}
|
||||
|
||||
function retrieveAliasForEntity(entityName: string, aliases) {
|
||||
aliases = aliases ? (Array.isArray(aliases) ? aliases : [aliases]) : []
|
||||
|
||||
for (const alias of aliases) {
|
||||
const names = Array.isArray(alias.name) ? alias.name : [alias.name]
|
||||
|
||||
if (alias.entity === entityName) {
|
||||
return names[0]
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
if (name.toLowerCase() === entityName.toLowerCase()) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function retrieveModuleAndAlias(entityName, moduleJoinerConfigs) {
|
||||
let relatedModule
|
||||
let alias
|
||||
|
||||
for (const moduleJoinerConfig of moduleJoinerConfigs) {
|
||||
const moduleSchema = moduleJoinerConfig.schema
|
||||
const moduleAliases = moduleJoinerConfig.alias
|
||||
|
||||
/**
|
||||
* If the entity exist in the module schema, then the current module is the
|
||||
* one we are looking for.
|
||||
*
|
||||
* If the module does not have any schema, then we need to base the search
|
||||
* on the provided aliases. in any case, we try to get both
|
||||
*/
|
||||
|
||||
if (moduleSchema) {
|
||||
const executableSchema = makeSchemaExecutable(moduleSchema)
|
||||
const entitiesMap = executableSchema.getTypeMap()
|
||||
|
||||
if (entitiesMap[entityName]) {
|
||||
relatedModule = moduleJoinerConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (relatedModule && moduleAliases) {
|
||||
alias = retrieveAliasForEntity(entityName, moduleJoinerConfig.alias)
|
||||
}
|
||||
|
||||
if (relatedModule) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!relatedModule) {
|
||||
return { relatedModule: null, alias: null }
|
||||
}
|
||||
|
||||
if (!alias) {
|
||||
throw new Error(
|
||||
`Index Module error, the module ${relatedModule?.serviceName} has a schema but does not have any alias for the entity ${entityName}. Please add an alias to the module configuration and the entity it correspond to in the args under the entity property.`
|
||||
)
|
||||
}
|
||||
|
||||
return { relatedModule, alias }
|
||||
}
|
||||
|
||||
// TODO: rename util
|
||||
function retrieveLinkModuleAndAlias({
|
||||
primaryEntity,
|
||||
primaryModuleConfig,
|
||||
foreignEntity,
|
||||
foreignModuleConfig,
|
||||
moduleJoinerConfigs,
|
||||
}: {
|
||||
primaryEntity: string
|
||||
primaryModuleConfig: ModuleJoinerConfig
|
||||
foreignEntity: string
|
||||
foreignModuleConfig: ModuleJoinerConfig
|
||||
moduleJoinerConfigs: ModuleJoinerConfig[]
|
||||
}): {
|
||||
entityName: string
|
||||
alias: string
|
||||
linkModuleConfig: ModuleJoinerConfig
|
||||
intermediateEntityNames: string[]
|
||||
}[] {
|
||||
const linkModulesMetadata: {
|
||||
entityName: string
|
||||
alias: string
|
||||
linkModuleConfig: ModuleJoinerConfig
|
||||
intermediateEntityNames: string[]
|
||||
}[] = []
|
||||
|
||||
for (const linkModuleJoinerConfig of moduleJoinerConfigs.filter(
|
||||
(config) => config.isLink && !config.isReadOnlyLink
|
||||
)) {
|
||||
const linkPrimary =
|
||||
linkModuleJoinerConfig.relationships![0] as ModuleJoinerRelationship
|
||||
const linkForeign =
|
||||
linkModuleJoinerConfig.relationships![1] as ModuleJoinerRelationship
|
||||
|
||||
if (
|
||||
linkPrimary.serviceName === primaryModuleConfig.serviceName &&
|
||||
linkForeign.serviceName === foreignModuleConfig.serviceName
|
||||
) {
|
||||
const primaryEntityLinkableKey = linkPrimary.foreignKey
|
||||
const isTheForeignKeyEntityEqualPrimaryEntity =
|
||||
primaryModuleConfig.linkableKeys?.[primaryEntityLinkableKey] ===
|
||||
primaryEntity
|
||||
|
||||
const foreignEntityLinkableKey = linkForeign.foreignKey
|
||||
const isTheForeignKeyEntityEqualForeignEntity =
|
||||
foreignModuleConfig.linkableKeys?.[foreignEntityLinkableKey] ===
|
||||
foreignEntity
|
||||
|
||||
const linkName = linkModuleJoinerConfig.extends?.find((extend) => {
|
||||
return (
|
||||
extend.serviceName === primaryModuleConfig.serviceName &&
|
||||
extend.relationship.primaryKey === primaryEntityLinkableKey
|
||||
)
|
||||
})?.relationship.serviceName
|
||||
|
||||
if (!linkName) {
|
||||
throw new Error(
|
||||
`Index Module error, unable to retrieve the link module name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the extend relationship service name is set correctly`
|
||||
)
|
||||
}
|
||||
|
||||
if (!linkModuleJoinerConfig.alias?.[0]?.entity) {
|
||||
throw new Error(
|
||||
`Index Module error, unable to retrieve the link module entity name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the link module alias has an entity property in the args.`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
isTheForeignKeyEntityEqualPrimaryEntity &&
|
||||
isTheForeignKeyEntityEqualForeignEntity
|
||||
) {
|
||||
/**
|
||||
* The link will become the parent of the foreign entity, that is why the alias must be the one that correspond to the extended foreign module
|
||||
*/
|
||||
|
||||
linkModulesMetadata.push({
|
||||
entityName: linkModuleJoinerConfig.alias[0].entity,
|
||||
alias: extractNameFromAlias(linkModuleJoinerConfig.alias),
|
||||
linkModuleConfig: linkModuleJoinerConfig,
|
||||
intermediateEntityNames: [],
|
||||
})
|
||||
} else {
|
||||
const intermediateEntityName =
|
||||
foreignModuleConfig.linkableKeys![foreignEntityLinkableKey]
|
||||
|
||||
if (!foreignModuleConfig.schema) {
|
||||
throw new Error(
|
||||
`Index Module error, unable to retrieve the intermediate entity name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the foreign module ${foreignModuleConfig.serviceName} has a schema.`
|
||||
)
|
||||
}
|
||||
|
||||
const executableSchema = makeSchemaExecutable(
|
||||
foreignModuleConfig.schema
|
||||
)
|
||||
const entitiesMap = executableSchema.getTypeMap()
|
||||
|
||||
let intermediateEntities: string[] = []
|
||||
let foundCount = 0
|
||||
|
||||
const isForeignEntityChildOfIntermediateEntity = (
|
||||
entityName
|
||||
): boolean => {
|
||||
for (const entityType of Object.values(entitiesMap)) {
|
||||
if (
|
||||
entityType.astNode?.kind === "ObjectTypeDefinition" &&
|
||||
entityType.astNode?.fields?.some((field) => {
|
||||
return (field.type as any)?.type?.name?.value === entityName
|
||||
})
|
||||
) {
|
||||
if (entityType.name === intermediateEntityName) {
|
||||
++foundCount
|
||||
return true
|
||||
} else {
|
||||
const test = isForeignEntityChildOfIntermediateEntity(
|
||||
entityType.name
|
||||
)
|
||||
if (test) {
|
||||
intermediateEntities.push(entityType.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
isForeignEntityChildOfIntermediateEntity(foreignEntity)
|
||||
|
||||
if (foundCount !== 1) {
|
||||
throw new Error(
|
||||
`Index Module error, unable to retrieve the intermediate entities for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName} between ${foreignEntity} and ${intermediateEntityName}. Multiple paths or no path found. Please check your schema in ${foreignModuleConfig.serviceName}`
|
||||
)
|
||||
}
|
||||
|
||||
intermediateEntities.push(intermediateEntityName!)
|
||||
|
||||
/**
|
||||
* The link will become the parent of the foreign entity, that is why the alias must be the one that correspond to the extended foreign module
|
||||
*/
|
||||
|
||||
linkModulesMetadata.push({
|
||||
entityName: linkModuleJoinerConfig.alias[0].entity,
|
||||
alias: extractNameFromAlias(linkModuleJoinerConfig.alias),
|
||||
linkModuleConfig: linkModuleJoinerConfig,
|
||||
intermediateEntityNames: intermediateEntities,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkModulesMetadata.length) {
|
||||
// TODO: change to use the logger
|
||||
console.warn(
|
||||
`Index Module warning, unable to retrieve the link module that correspond to the entities ${primaryEntity} - ${foreignEntity}.`
|
||||
)
|
||||
}
|
||||
|
||||
return linkModulesMetadata
|
||||
}
|
||||
|
||||
function getObjectRepresentationRef(
|
||||
entityName,
|
||||
{ objectRepresentationRef }
|
||||
): SchemaObjectEntityRepresentation {
|
||||
return (objectRepresentationRef[entityName] ??= {
|
||||
entity: entityName,
|
||||
parents: [],
|
||||
alias: "",
|
||||
listeners: [],
|
||||
moduleConfig: null,
|
||||
fields: [],
|
||||
})
|
||||
}
|
||||
|
||||
function setCustomDirectives(currentObjectRepresentationRef, directives) {
|
||||
for (const customDirectiveConfiguration of Object.values(CustomDirectives)) {
|
||||
const directive = directives.find(
|
||||
(typeDirective) =>
|
||||
typeDirective.name.value === customDirectiveConfiguration.name
|
||||
)
|
||||
|
||||
if (!directive) {
|
||||
return
|
||||
}
|
||||
|
||||
// Only support array directive value for now
|
||||
currentObjectRepresentationRef[
|
||||
customDirectiveConfiguration.configurationPropertyName
|
||||
] = ((directive.arguments[0].value as any)?.values ?? []).map(
|
||||
(v) => v.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function processEntity(
|
||||
entityName: string,
|
||||
{
|
||||
entitiesMap,
|
||||
moduleJoinerConfigs,
|
||||
objectRepresentationRef,
|
||||
}: {
|
||||
entitiesMap: any
|
||||
moduleJoinerConfigs: ModuleJoinerConfig[]
|
||||
objectRepresentationRef: SchemaObjectRepresentation
|
||||
}
|
||||
) {
|
||||
/**
|
||||
* Get the reference to the object representation for the current entity.
|
||||
*/
|
||||
|
||||
const currentObjectRepresentationRef = getObjectRepresentationRef(
|
||||
entityName,
|
||||
{
|
||||
objectRepresentationRef,
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Retrieve and set the custom directives for the current entity.
|
||||
*/
|
||||
|
||||
setCustomDirectives(
|
||||
currentObjectRepresentationRef,
|
||||
entitiesMap[entityName].astNode?.directives ?? []
|
||||
)
|
||||
|
||||
currentObjectRepresentationRef.fields =
|
||||
gqlGetFieldsAndRelations(entitiesMap, entityName) ?? []
|
||||
|
||||
/**
|
||||
* Retrieve the module and alias for the current entity.
|
||||
*/
|
||||
|
||||
const { relatedModule: currentEntityModule, alias } = retrieveModuleAndAlias(
|
||||
entityName,
|
||||
moduleJoinerConfigs
|
||||
)
|
||||
|
||||
if (
|
||||
!currentEntityModule &&
|
||||
currentObjectRepresentationRef.listeners.length > 0
|
||||
) {
|
||||
const example = JSON.stringify({
|
||||
alias: [
|
||||
{
|
||||
name: "entity-alias",
|
||||
entity: entityName,
|
||||
},
|
||||
],
|
||||
})
|
||||
throw new Error(
|
||||
`Index Module error, unable to retrieve the module that corresponds to the entity ${entityName}.\nPlease add the entity to the module schema or add an alias to the joiner config like the example below:\n${example}`
|
||||
)
|
||||
}
|
||||
|
||||
if (currentEntityModule) {
|
||||
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||
currentEntityModule.serviceName
|
||||
] = currentEntityModule
|
||||
currentObjectRepresentationRef.moduleConfig = currentEntityModule
|
||||
currentObjectRepresentationRef.alias = alias
|
||||
}
|
||||
/**
|
||||
* Retrieve the parent entities for the current entity.
|
||||
*/
|
||||
|
||||
const schemaParentEntity = Object.values(entitiesMap).filter((value: any) => {
|
||||
return (
|
||||
value.astNode &&
|
||||
(value.astNode as ObjectTypeDefinitionNode).fields?.some((field: any) => {
|
||||
let currentType = field.type
|
||||
while (currentType.type) {
|
||||
currentType = currentType.type
|
||||
}
|
||||
|
||||
return currentType.name?.value === entityName
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
if (!schemaParentEntity.length) {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current entity has parent entities, then we need to process them.
|
||||
*/
|
||||
const parentEntityNames = schemaParentEntity.map((parent: any) => {
|
||||
return parent.name
|
||||
})
|
||||
|
||||
for (const parent of parentEntityNames) {
|
||||
/**
|
||||
* Retrieve the parent entity field in the schema
|
||||
*/
|
||||
|
||||
const entityFieldInParent = (
|
||||
entitiesMap[parent].astNode as any
|
||||
)?.fields?.find((field) => {
|
||||
let currentType = field.type
|
||||
while (currentType.type) {
|
||||
currentType = currentType.type
|
||||
}
|
||||
return currentType.name?.value === entityName
|
||||
})
|
||||
|
||||
const isEntityListInParent =
|
||||
entityFieldInParent.type.kind === Kind.LIST_TYPE
|
||||
const entityTargetPropertyNameInParent = entityFieldInParent.name.value
|
||||
|
||||
/**
|
||||
* Retrieve the parent entity object representation reference.
|
||||
*/
|
||||
|
||||
const parentObjectRepresentationRef = getObjectRepresentationRef(parent, {
|
||||
objectRepresentationRef,
|
||||
})
|
||||
const parentModuleConfig = parentObjectRepresentationRef.moduleConfig
|
||||
|
||||
// If the entity is not part of any module, just set the parent and continue
|
||||
if (!currentObjectRepresentationRef.moduleConfig) {
|
||||
currentObjectRepresentationRef.parents.push({
|
||||
ref: parentObjectRepresentationRef,
|
||||
targetProp: entityTargetPropertyNameInParent,
|
||||
isList: isEntityListInParent,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
/**
|
||||
* If the parent entity and the current entity are part of the same servive then configure the parent and
|
||||
* add the parent id as a field to the current entity.
|
||||
*/
|
||||
|
||||
if (
|
||||
currentObjectRepresentationRef.moduleConfig.serviceName ===
|
||||
parentModuleConfig.serviceName ||
|
||||
parentModuleConfig.isLink
|
||||
) {
|
||||
currentObjectRepresentationRef.parents.push({
|
||||
ref: parentObjectRepresentationRef,
|
||||
targetProp: entityTargetPropertyNameInParent,
|
||||
isList: isEntityListInParent,
|
||||
})
|
||||
|
||||
currentObjectRepresentationRef.fields.push(
|
||||
parentObjectRepresentationRef.alias + ".id"
|
||||
)
|
||||
} else {
|
||||
/**
|
||||
* If the parent entity and the current entity are not part of the same service then we need to
|
||||
* find the link module that join them.
|
||||
*/
|
||||
|
||||
const linkModuleMetadatas = retrieveLinkModuleAndAlias({
|
||||
primaryEntity: parentObjectRepresentationRef.entity,
|
||||
primaryModuleConfig: parentModuleConfig,
|
||||
foreignEntity: currentObjectRepresentationRef.entity,
|
||||
foreignModuleConfig: currentEntityModule,
|
||||
moduleJoinerConfigs,
|
||||
})
|
||||
|
||||
for (const linkModuleMetadata of linkModuleMetadatas) {
|
||||
const linkObjectRepresentationRef = getObjectRepresentationRef(
|
||||
linkModuleMetadata.entityName,
|
||||
{ objectRepresentationRef }
|
||||
)
|
||||
|
||||
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||
linkModuleMetadata.linkModuleConfig.serviceName ||
|
||||
linkModuleMetadata.entityName
|
||||
] = currentEntityModule
|
||||
|
||||
/**
|
||||
* Add the schema parent entity as a parent to the link module and configure it.
|
||||
*/
|
||||
|
||||
linkObjectRepresentationRef.parents = [
|
||||
{
|
||||
ref: parentObjectRepresentationRef,
|
||||
targetProp: linkModuleMetadata.alias,
|
||||
},
|
||||
]
|
||||
linkObjectRepresentationRef.alias = linkModuleMetadata.alias
|
||||
linkObjectRepresentationRef.listeners = [
|
||||
`${linkModuleMetadata.entityName}.${CommonEvents.ATTACHED}`,
|
||||
`${linkModuleMetadata.entityName}.${CommonEvents.DETACHED}`,
|
||||
]
|
||||
linkObjectRepresentationRef.moduleConfig =
|
||||
linkModuleMetadata.linkModuleConfig
|
||||
|
||||
linkObjectRepresentationRef.fields = [
|
||||
"id",
|
||||
...linkModuleMetadata.linkModuleConfig
|
||||
.relationships!.map(
|
||||
(relationship) =>
|
||||
[
|
||||
parentModuleConfig.serviceName,
|
||||
currentEntityModule.serviceName,
|
||||
].includes(relationship.serviceName) && relationship.foreignKey
|
||||
)
|
||||
.filter((v): v is string => Boolean(v)),
|
||||
]
|
||||
|
||||
/**
|
||||
* If the current entity is not the entity that is used to join the link module and the parent entity
|
||||
* then we need to add the new entity that join them and then add the link as its parent
|
||||
* before setting the new entity as the true parent of the current entity.
|
||||
*/
|
||||
|
||||
for (
|
||||
let i = linkModuleMetadata.intermediateEntityNames.length - 1;
|
||||
i >= 0;
|
||||
--i
|
||||
) {
|
||||
const intermediateEntityName =
|
||||
linkModuleMetadata.intermediateEntityNames[i]
|
||||
|
||||
const isLastIntermediateEntity =
|
||||
i === linkModuleMetadata.intermediateEntityNames.length - 1
|
||||
|
||||
const parentIntermediateEntityRef = isLastIntermediateEntity
|
||||
? linkObjectRepresentationRef
|
||||
: objectRepresentationRef[
|
||||
linkModuleMetadata.intermediateEntityNames[i + 1]
|
||||
]
|
||||
|
||||
const {
|
||||
relatedModule: intermediateEntityModule,
|
||||
alias: intermediateEntityAlias,
|
||||
} = retrieveModuleAndAlias(
|
||||
intermediateEntityName,
|
||||
moduleJoinerConfigs
|
||||
)
|
||||
|
||||
const intermediateEntityObjectRepresentationRef =
|
||||
getObjectRepresentationRef(intermediateEntityName, {
|
||||
objectRepresentationRef,
|
||||
})
|
||||
|
||||
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||
intermediateEntityModule.serviceName
|
||||
] = intermediateEntityModule
|
||||
|
||||
intermediateEntityObjectRepresentationRef.parents.push({
|
||||
ref: parentIntermediateEntityRef,
|
||||
targetProp: intermediateEntityAlias,
|
||||
isList: true, // TODO: check if it is a list in retrieveLinkModuleAndAlias and return the intermediate entity names + isList for each
|
||||
})
|
||||
|
||||
intermediateEntityObjectRepresentationRef.alias =
|
||||
intermediateEntityAlias
|
||||
intermediateEntityObjectRepresentationRef.listeners = [
|
||||
intermediateEntityName + "." + CommonEvents.CREATED,
|
||||
intermediateEntityName + "." + CommonEvents.UPDATED,
|
||||
]
|
||||
intermediateEntityObjectRepresentationRef.moduleConfig =
|
||||
intermediateEntityModule
|
||||
intermediateEntityObjectRepresentationRef.fields = ["id"]
|
||||
|
||||
/**
|
||||
* We push the parent id only between intermediate entities but not between intermediate and link
|
||||
*/
|
||||
|
||||
if (!isLastIntermediateEntity) {
|
||||
intermediateEntityObjectRepresentationRef.fields.push(
|
||||
parentIntermediateEntityRef.alias + ".id"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is any intermediate entity then we need to set the last one as the parent field for the current entity.
|
||||
* otherwise there is not need to set the link id field into the current entity.
|
||||
*/
|
||||
|
||||
let currentParentIntermediateRef = linkObjectRepresentationRef
|
||||
if (linkModuleMetadata.intermediateEntityNames.length) {
|
||||
currentParentIntermediateRef =
|
||||
objectRepresentationRef[
|
||||
linkModuleMetadata.intermediateEntityNames[0]
|
||||
]
|
||||
currentObjectRepresentationRef.fields.push(
|
||||
currentParentIntermediateRef.alias + ".id"
|
||||
)
|
||||
}
|
||||
|
||||
currentObjectRepresentationRef.parents.push({
|
||||
ref: currentParentIntermediateRef,
|
||||
inSchemaRef: parentObjectRepresentationRef,
|
||||
targetProp: entityTargetPropertyNameInParent,
|
||||
isList: isEntityListInParent,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a special object which will be used to retrieve the correct
|
||||
* object representation using path tree
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* _schemaPropertiesMap: {
|
||||
* "product": <ProductRef>
|
||||
* "product.variants": <ProductVariantRef>
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
function buildAliasMap(objectRepresentation: SchemaObjectRepresentation) {
|
||||
const aliasMap: SchemaObjectRepresentation["_schemaPropertiesMap"] = {}
|
||||
|
||||
function recursivelyBuildAliasPath(
|
||||
current,
|
||||
alias = "",
|
||||
aliases: { alias: string; shortCutOf?: string }[] = []
|
||||
): { alias: string; shortCutOf?: string }[] {
|
||||
if (current.parents?.length) {
|
||||
for (const parentEntity of current.parents) {
|
||||
/**
|
||||
* Here we build the alias from child to parent to get it as parent to child
|
||||
*/
|
||||
|
||||
const _aliases = recursivelyBuildAliasPath(
|
||||
parentEntity.ref,
|
||||
`${parentEntity.targetProp}${alias ? "." + alias : ""}`
|
||||
).map((alias) => ({ alias: alias.alias }))
|
||||
|
||||
aliases.push(..._aliases)
|
||||
|
||||
/**
|
||||
* Now if there is a inSchemaRef it means that we had inferred a link module
|
||||
* and we want to get the alias path as it would be in the schema provided
|
||||
* and it become the short cut path of the full path above
|
||||
*/
|
||||
|
||||
if (parentEntity.inSchemaRef) {
|
||||
const shortCutOf = _aliases.map((a) => a.alias)[0]
|
||||
const _aliasesShortCut = recursivelyBuildAliasPath(
|
||||
parentEntity.inSchemaRef,
|
||||
`${parentEntity.targetProp}${alias ? "." + alias : ""}`
|
||||
).map((alias_) => {
|
||||
return {
|
||||
alias: alias_.alias,
|
||||
// It has to be the same entry point
|
||||
shortCutOf:
|
||||
shortCutOf.split(".")[0] === alias_.alias.split(".")[0]
|
||||
? shortCutOf
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
aliases.push(..._aliasesShortCut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aliases.push({ alias: current.alias + (alias ? "." + alias : "") })
|
||||
|
||||
return aliases
|
||||
}
|
||||
|
||||
for (const objectRepresentationKey of Object.keys(
|
||||
objectRepresentation
|
||||
).filter(
|
||||
(key) => !schemaObjectRepresentationPropertiesToOmit.includes(key)
|
||||
)) {
|
||||
const entityRepresentationRef =
|
||||
objectRepresentation[objectRepresentationKey]
|
||||
|
||||
const aliases = recursivelyBuildAliasPath(entityRepresentationRef)
|
||||
|
||||
for (const alias of aliases) {
|
||||
aliasMap[alias.alias] = {
|
||||
ref: entityRepresentationRef,
|
||||
}
|
||||
|
||||
if (alias.shortCutOf) {
|
||||
aliasMap[alias.alias]["shortCutOf"] = alias.shortCutOf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aliasMap
|
||||
}
|
||||
|
||||
/**
|
||||
* This util build an internal representation object from the provided schema.
|
||||
* It will resolve all modules, fields, link module representation to build
|
||||
* the appropriate representation for the index module.
|
||||
*
|
||||
* This representation will be used to re construct the expected output object from a search
|
||||
* but can also be used for anything since the relation tree is available through ref.
|
||||
*
|
||||
* @param schema
|
||||
*/
|
||||
export function buildSchemaObjectRepresentation(
|
||||
schema
|
||||
): [SchemaObjectRepresentation, Record<string, any>] {
|
||||
const moduleJoinerConfigs = MedusaModule.getAllJoinerConfigs()
|
||||
const augmentedSchema = CustomDirectives.Listeners.definition + schema
|
||||
const executableSchema = makeSchemaExecutable(augmentedSchema)
|
||||
const entitiesMap = executableSchema.getTypeMap()
|
||||
|
||||
const objectRepresentation = {
|
||||
_serviceNameModuleConfigMap: {},
|
||||
} as SchemaObjectRepresentation
|
||||
|
||||
Object.entries(entitiesMap).forEach(([entityName, entityMapValue]) => {
|
||||
if (!entityMapValue.astNode) {
|
||||
return
|
||||
}
|
||||
|
||||
processEntity(entityName, {
|
||||
entitiesMap,
|
||||
moduleJoinerConfigs,
|
||||
objectRepresentationRef: objectRepresentation,
|
||||
})
|
||||
})
|
||||
|
||||
objectRepresentation._schemaPropertiesMap =
|
||||
buildAliasMap(objectRepresentation)
|
||||
|
||||
return [objectRepresentation, entitiesMap]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
SchemaObjectRepresentation,
|
||||
schemaObjectRepresentationPropertiesToOmit,
|
||||
} from "../types"
|
||||
|
||||
export async function createPartitions(
|
||||
schemaObjectRepresentation: SchemaObjectRepresentation,
|
||||
manager: SqlEntityManager
|
||||
): Promise<void> {
|
||||
const activeSchema = manager.config.get("schema")
|
||||
? `"${manager.config.get("schema")}".`
|
||||
: ""
|
||||
const partitions = Object.keys(schemaObjectRepresentation)
|
||||
.filter(
|
||||
(key) =>
|
||||
!schemaObjectRepresentationPropertiesToOmit.includes(key) &&
|
||||
schemaObjectRepresentation[key].listeners.length > 0
|
||||
)
|
||||
.map((key) => {
|
||||
const cName = key.toLowerCase()
|
||||
const part: string[] = []
|
||||
part.push(
|
||||
`CREATE TABLE IF NOT EXISTS ${activeSchema}cat_${cName} PARTITION OF ${activeSchema}index_data FOR VALUES IN ('${key}')`
|
||||
)
|
||||
|
||||
for (const parent of schemaObjectRepresentation[key].parents) {
|
||||
const pKey = `${parent.ref.entity}-${key}`
|
||||
const pName = `${parent.ref.entity}${key}`.toLowerCase()
|
||||
part.push(
|
||||
`CREATE TABLE IF NOT EXISTS ${activeSchema}cat_pivot_${pName} PARTITION OF ${activeSchema}index_relation FOR VALUES IN ('${pKey}')`
|
||||
)
|
||||
}
|
||||
return part
|
||||
})
|
||||
.flat()
|
||||
|
||||
if (!partitions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
partitions.push(`analyse ${activeSchema}index_data`)
|
||||
partitions.push(`analyse ${activeSchema}index_relation`)
|
||||
|
||||
await manager.execute(partitions.join("; "))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const defaultSchema = `
|
||||
type Product @Listeners(values: ["Product.product.created", "Product.product.updated", "Product.product.deleted"]) {
|
||||
id: String
|
||||
title: String
|
||||
variants: [ProductVariant]
|
||||
}
|
||||
|
||||
type ProductVariant @Listeners(values: ["Product.product-variant.created", "Product.product-variant.updated", "Product.product-variant.deleted"]) {
|
||||
id: String
|
||||
product_id: String
|
||||
sku: String
|
||||
prices: [Price]
|
||||
}
|
||||
|
||||
type Price @Listeners(values: ["Pricing.price.created", "Pricing.price.updated", "Pricing.price.deleted"]) {
|
||||
amount: Int
|
||||
currency_code: String
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./query-builder"
|
||||
export * from "./create-partitions"
|
||||
export * from "./build-config"
|
||||
@@ -0,0 +1,628 @@
|
||||
import { isObject, isString } from "@medusajs/utils"
|
||||
import { GraphQLList } from "graphql"
|
||||
import { Knex } from "knex"
|
||||
import {
|
||||
OrderBy,
|
||||
QueryFormat,
|
||||
QueryOptions,
|
||||
SchemaObjectRepresentation,
|
||||
SchemaPropertiesMap,
|
||||
Select,
|
||||
} from "../types"
|
||||
|
||||
export class QueryBuilder {
|
||||
private readonly structure: Select
|
||||
private readonly entityMap: Record<string, any>
|
||||
private readonly knex: Knex
|
||||
private readonly selector: QueryFormat
|
||||
private readonly options?: QueryOptions
|
||||
private readonly schema: SchemaObjectRepresentation
|
||||
|
||||
constructor(args: {
|
||||
schema: SchemaObjectRepresentation
|
||||
entityMap: Record<string, any>
|
||||
knex: Knex
|
||||
selector: QueryFormat
|
||||
options?: QueryOptions
|
||||
}) {
|
||||
this.schema = args.schema
|
||||
this.entityMap = args.entityMap
|
||||
this.selector = args.selector
|
||||
this.options = args.options
|
||||
this.knex = args.knex
|
||||
this.structure = this.selector.select
|
||||
}
|
||||
|
||||
private getStructureKeys(structure) {
|
||||
return Object.keys(structure ?? {}).filter((key) => key !== "entity")
|
||||
}
|
||||
|
||||
private getEntity(path): SchemaPropertiesMap[0] {
|
||||
if (!this.schema._schemaPropertiesMap[path]) {
|
||||
throw new Error(`Could not find entity for path: ${path}`)
|
||||
}
|
||||
|
||||
return this.schema._schemaPropertiesMap[path]
|
||||
}
|
||||
|
||||
private getGraphQLType(path, field) {
|
||||
const entity = this.getEntity(path)?.ref?.entity
|
||||
const fieldRef = this.entityMap[entity]._fields[field]
|
||||
if (!fieldRef) {
|
||||
throw new Error(`Field ${field} not found in the entityMap.`)
|
||||
}
|
||||
|
||||
let currentType = fieldRef.type
|
||||
let isArray = false
|
||||
while (currentType.ofType) {
|
||||
if (currentType instanceof GraphQLList) {
|
||||
isArray = true
|
||||
}
|
||||
|
||||
currentType = currentType.ofType
|
||||
}
|
||||
|
||||
return currentType.name + (isArray ? "[]" : "")
|
||||
}
|
||||
|
||||
private transformValueToType(path, field, value) {
|
||||
if (value === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const typeToFn = {
|
||||
Int: (val) => parseInt(val, 10),
|
||||
Float: (val) => parseFloat(val),
|
||||
String: (val) => String(val),
|
||||
Boolean: (val) => Boolean(val),
|
||||
ID: (val) => String(val),
|
||||
Date: (val) => new Date(val).toISOString(),
|
||||
Time: (val) => new Date(`1970-01-01T${val}Z`).toISOString(),
|
||||
}
|
||||
|
||||
const fullPath = [path, ...field]
|
||||
const prop = fullPath.pop()
|
||||
const fieldPath = fullPath.join(".")
|
||||
const graphqlType = this.getGraphQLType(fieldPath, prop).replace("[]", "")
|
||||
|
||||
const fn = typeToFn[graphqlType]
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => (!fn ? v : fn(v)))
|
||||
}
|
||||
return !fn ? value : fn(value)
|
||||
}
|
||||
|
||||
private getPostgresCastType(path, field) {
|
||||
const graphqlToPostgresTypeMap = {
|
||||
Int: "::int",
|
||||
Float: "::double precision",
|
||||
Boolean: "::boolean",
|
||||
Date: "::timestamp",
|
||||
Time: "::time",
|
||||
"": "",
|
||||
}
|
||||
|
||||
const fullPath = [path, ...field]
|
||||
const prop = fullPath.pop()
|
||||
const fieldPath = fullPath.join(".")
|
||||
let graphqlType = this.getGraphQLType(fieldPath, prop)
|
||||
const isList = graphqlType.endsWith("[]")
|
||||
graphqlType = graphqlType.replace("[]", "")
|
||||
|
||||
return (
|
||||
(graphqlToPostgresTypeMap[graphqlType] ?? "") + (isList ? "[]" : "") ?? ""
|
||||
)
|
||||
}
|
||||
|
||||
private parseWhere(
|
||||
aliasMapping: { [path: string]: string },
|
||||
obj: object,
|
||||
builder: Knex.QueryBuilder
|
||||
) {
|
||||
const OPERATOR_MAP = {
|
||||
$eq: "=",
|
||||
$lt: "<",
|
||||
$gt: ">",
|
||||
$lte: "<=",
|
||||
$gte: ">=",
|
||||
$ne: "!=",
|
||||
$in: "IN",
|
||||
$is: "IS",
|
||||
$like: "LIKE",
|
||||
$ilike: "ILIKE",
|
||||
}
|
||||
const keys = Object.keys(obj)
|
||||
|
||||
const getPathAndField = (key: string) => {
|
||||
const path = key.split(".")
|
||||
const field = [path.pop()]
|
||||
|
||||
while (!aliasMapping[path.join(".")] && path.length > 0) {
|
||||
field.unshift(path.pop())
|
||||
}
|
||||
|
||||
const attr = path.join(".")
|
||||
|
||||
return { field, attr }
|
||||
}
|
||||
|
||||
const getPathOperation = (
|
||||
attr: string,
|
||||
path: string[],
|
||||
value: unknown
|
||||
): string => {
|
||||
const partialPath = path.length > 1 ? path.slice(0, -1) : path
|
||||
const val = this.transformValueToType(attr, partialPath, value)
|
||||
const result = path.reduceRight((acc, key) => {
|
||||
return { [key]: acc }
|
||||
}, val)
|
||||
|
||||
return JSON.stringify(result)
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
let value = obj[key]
|
||||
|
||||
if ((key === "$and" || key === "$or") && !Array.isArray(value)) {
|
||||
value = [value]
|
||||
}
|
||||
|
||||
if (key === "$and" && Array.isArray(value)) {
|
||||
builder.where((qb) => {
|
||||
value.forEach((cond) => {
|
||||
qb.andWhere((subBuilder) =>
|
||||
this.parseWhere(aliasMapping, cond, subBuilder)
|
||||
)
|
||||
})
|
||||
})
|
||||
} else if (key === "$or" && Array.isArray(value)) {
|
||||
builder.where((qb) => {
|
||||
value.forEach((cond) => {
|
||||
qb.orWhere((subBuilder) =>
|
||||
this.parseWhere(aliasMapping, cond, subBuilder)
|
||||
)
|
||||
})
|
||||
})
|
||||
} else if (isObject(value) && !Array.isArray(value)) {
|
||||
const subKeys = Object.keys(value)
|
||||
subKeys.forEach((subKey) => {
|
||||
let operator = OPERATOR_MAP[subKey]
|
||||
if (operator) {
|
||||
const { field, attr } = getPathAndField(key)
|
||||
const nested = new Array(field.length).join("->?")
|
||||
|
||||
const subValue = this.transformValueToType(
|
||||
attr,
|
||||
field,
|
||||
value[subKey]
|
||||
)
|
||||
const castType = this.getPostgresCastType(attr, field)
|
||||
|
||||
const val = operator === "IN" ? subValue : [subValue]
|
||||
if (operator === "=" && subValue === null) {
|
||||
operator = "IS"
|
||||
}
|
||||
|
||||
if (operator === "=") {
|
||||
builder.whereRaw(
|
||||
`${aliasMapping[attr]}.data @> '${getPathOperation(
|
||||
attr,
|
||||
field as string[],
|
||||
subValue
|
||||
)}'::jsonb`
|
||||
)
|
||||
} else {
|
||||
builder.whereRaw(
|
||||
`(${aliasMapping[attr]}.data${nested}->>?)${castType} ${operator} ?`,
|
||||
[...field, ...val]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported operator: ${subKey}`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const { field, attr } = getPathAndField(key)
|
||||
const nested = new Array(field.length).join("->?")
|
||||
|
||||
value = this.transformValueToType(attr, field, value)
|
||||
if (Array.isArray(value)) {
|
||||
const castType = this.getPostgresCastType(attr, field)
|
||||
const inPlaceholders = value.map(() => "?").join(",")
|
||||
builder.whereRaw(
|
||||
`(${aliasMapping[attr]}.data${nested}->>?)${castType} IN (${inPlaceholders})`,
|
||||
[...field, ...value]
|
||||
)
|
||||
} else {
|
||||
const operator = value === null ? "IS" : "="
|
||||
|
||||
if (operator === "=") {
|
||||
builder.whereRaw(
|
||||
`${aliasMapping[attr]}.data @> '${getPathOperation(
|
||||
attr,
|
||||
field as string[],
|
||||
value
|
||||
)}'::jsonb`
|
||||
)
|
||||
} else {
|
||||
const castType = this.getPostgresCastType(attr, field)
|
||||
builder.whereRaw(
|
||||
`(${aliasMapping[attr]}.data${nested}->>?)${castType} ${operator} ?`,
|
||||
[...field, value]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
private buildQueryParts(
|
||||
structure: Select,
|
||||
parentAlias: string,
|
||||
parentEntity: string,
|
||||
parentProperty: string,
|
||||
aliasPath: string[] = [],
|
||||
level = 0,
|
||||
aliasMapping: { [path: string]: string } = {}
|
||||
): string[] {
|
||||
const currentAliasPath = [...aliasPath, parentProperty].join(".")
|
||||
|
||||
const entities = this.getEntity(currentAliasPath)
|
||||
|
||||
const mainEntity = entities.ref.entity
|
||||
const mainAlias = mainEntity.toLowerCase() + level
|
||||
|
||||
const allEntities: any[] = []
|
||||
if (!entities.shortCutOf) {
|
||||
allEntities.push({
|
||||
entity: mainEntity,
|
||||
parEntity: parentEntity,
|
||||
parAlias: parentAlias,
|
||||
alias: mainAlias,
|
||||
})
|
||||
} else {
|
||||
const intermediateAlias = entities.shortCutOf.split(".")
|
||||
|
||||
for (let i = intermediateAlias.length - 1, x = 0; i >= 0; i--, x++) {
|
||||
const intermediateEntity = this.getEntity(intermediateAlias.join("."))
|
||||
|
||||
intermediateAlias.pop()
|
||||
|
||||
if (intermediateEntity.ref.entity === parentEntity) {
|
||||
break
|
||||
}
|
||||
|
||||
const parentIntermediateEntity = this.getEntity(
|
||||
intermediateAlias.join(".")
|
||||
)
|
||||
|
||||
const alias =
|
||||
intermediateEntity.ref.entity.toLowerCase() + level + "_" + x
|
||||
const parAlias =
|
||||
parentIntermediateEntity.ref.entity === parentEntity
|
||||
? parentAlias
|
||||
: parentIntermediateEntity.ref.entity.toLowerCase() +
|
||||
level +
|
||||
"_" +
|
||||
(x + 1)
|
||||
|
||||
if (x === 0) {
|
||||
aliasMapping[currentAliasPath] = alias
|
||||
}
|
||||
|
||||
allEntities.unshift({
|
||||
entity: intermediateEntity.ref.entity,
|
||||
parEntity: parentIntermediateEntity.ref.entity,
|
||||
parAlias,
|
||||
alias,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let queryParts: string[] = []
|
||||
for (const join of allEntities) {
|
||||
const { alias, entity, parEntity, parAlias } = join
|
||||
|
||||
aliasMapping[currentAliasPath] = alias
|
||||
|
||||
if (level > 0) {
|
||||
const subQuery = this.knex.queryBuilder()
|
||||
const knex = this.knex
|
||||
subQuery
|
||||
.select(`${alias}.id`, `${alias}.data`)
|
||||
.from("index_data AS " + alias)
|
||||
.join(`index_relation AS ${alias}_ref`, function () {
|
||||
this.on(
|
||||
`${alias}_ref.pivot`,
|
||||
"=",
|
||||
knex.raw("?", [`${parEntity}-${entity}`])
|
||||
)
|
||||
.andOn(`${alias}_ref.parent_id`, "=", `${parAlias}.id`)
|
||||
.andOn(`${alias}.id`, "=", `${alias}_ref.child_id`)
|
||||
})
|
||||
.where(`${alias}.name`, "=", knex.raw("?", [entity]))
|
||||
|
||||
const joinWhere = this.selector.joinWhere ?? {}
|
||||
const joinKey = Object.keys(joinWhere).find((key) => {
|
||||
const k = key.split(".")
|
||||
k.pop()
|
||||
return k.join(".") === currentAliasPath
|
||||
})
|
||||
|
||||
if (joinKey) {
|
||||
this.parseWhere(
|
||||
aliasMapping,
|
||||
{ [joinKey]: joinWhere[joinKey] },
|
||||
subQuery
|
||||
)
|
||||
}
|
||||
|
||||
queryParts.push(`LEFT JOIN LATERAL (
|
||||
${subQuery.toQuery()}
|
||||
) ${alias} ON TRUE`)
|
||||
}
|
||||
}
|
||||
|
||||
const children = this.getStructureKeys(structure)
|
||||
for (const child of children) {
|
||||
const childStructure = structure[child] as Select
|
||||
queryParts = queryParts.concat(
|
||||
this.buildQueryParts(
|
||||
childStructure,
|
||||
mainAlias,
|
||||
mainEntity,
|
||||
child,
|
||||
aliasPath.concat(parentProperty),
|
||||
level + 1,
|
||||
aliasMapping
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return queryParts
|
||||
}
|
||||
|
||||
private buildSelectParts(
|
||||
structure: Select,
|
||||
parentProperty: string,
|
||||
aliasMapping: { [path: string]: string },
|
||||
aliasPath: string[] = [],
|
||||
selectParts: object = {}
|
||||
): object {
|
||||
const currentAliasPath = [...aliasPath, parentProperty].join(".")
|
||||
const alias = aliasMapping[currentAliasPath]
|
||||
|
||||
selectParts[currentAliasPath] = `${alias}.data`
|
||||
selectParts[currentAliasPath + ".id"] = `${alias}.id`
|
||||
|
||||
const children = this.getStructureKeys(structure)
|
||||
|
||||
for (const child of children) {
|
||||
const childStructure = structure[child] as Select
|
||||
|
||||
this.buildSelectParts(
|
||||
childStructure,
|
||||
child,
|
||||
aliasMapping,
|
||||
aliasPath.concat(parentProperty),
|
||||
selectParts
|
||||
)
|
||||
}
|
||||
|
||||
return selectParts
|
||||
}
|
||||
|
||||
private transformOrderBy(arr: (object | string)[]): OrderBy {
|
||||
const result = {}
|
||||
const map = new Map()
|
||||
map.set(true, "ASC")
|
||||
map.set(1, "ASC")
|
||||
map.set("ASC", "ASC")
|
||||
map.set(false, "DESC")
|
||||
map.set(-1, "DESC")
|
||||
map.set("DESC", "DESC")
|
||||
|
||||
function nested(obj, prefix = "") {
|
||||
const keys = Object.keys(obj)
|
||||
if (!keys.length) {
|
||||
return
|
||||
} else if (keys.length > 1) {
|
||||
throw new Error("Order by only supports one key per object.")
|
||||
}
|
||||
const key = keys[0]
|
||||
let value = obj[key]
|
||||
if (isObject(value)) {
|
||||
nested(value, prefix + key + ".")
|
||||
} else {
|
||||
if (isString(value)) {
|
||||
value = value.toUpperCase()
|
||||
}
|
||||
result[prefix + key] = map.get(value) ?? "ASC"
|
||||
}
|
||||
}
|
||||
arr.forEach((obj) => nested(obj))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public buildQuery(countAllResults = true, returnIdOnly = false): string {
|
||||
const queryBuilder = this.knex.queryBuilder()
|
||||
|
||||
const structure = this.structure
|
||||
const filter = this.selector.where ?? {}
|
||||
|
||||
const { orderBy: order, skip, take } = this.options ?? {}
|
||||
|
||||
const orderBy = this.transformOrderBy(
|
||||
(order && !Array.isArray(order) ? [order] : order) ?? []
|
||||
)
|
||||
|
||||
const rootKey = this.getStructureKeys(structure)[0]
|
||||
const rootStructure = structure[rootKey] as Select
|
||||
const entity = this.getEntity(rootKey).ref.entity
|
||||
const rootEntity = entity.toLowerCase()
|
||||
const aliasMapping: { [path: string]: string } = {}
|
||||
|
||||
const joinParts = this.buildQueryParts(
|
||||
rootStructure,
|
||||
"",
|
||||
entity,
|
||||
rootKey,
|
||||
[],
|
||||
0,
|
||||
aliasMapping
|
||||
)
|
||||
|
||||
const rootAlias = aliasMapping[rootKey]
|
||||
const selectParts = !returnIdOnly
|
||||
? this.buildSelectParts(rootStructure, rootKey, aliasMapping)
|
||||
: { [rootKey + ".id"]: `${rootAlias}.id` }
|
||||
|
||||
if (countAllResults) {
|
||||
selectParts["offset_"] = this.knex.raw(
|
||||
`DENSE_RANK() OVER (ORDER BY ${rootEntity}0.id)`
|
||||
)
|
||||
}
|
||||
|
||||
queryBuilder.select(selectParts)
|
||||
|
||||
queryBuilder.from(`index_data AS ${rootEntity}0`)
|
||||
|
||||
joinParts.forEach((joinPart) => {
|
||||
queryBuilder.joinRaw(joinPart)
|
||||
})
|
||||
|
||||
queryBuilder.where(`${aliasMapping[rootEntity]}.name`, "=", entity)
|
||||
|
||||
// WHERE clause
|
||||
this.parseWhere(aliasMapping, filter, queryBuilder)
|
||||
|
||||
// ORDER BY clause
|
||||
for (const aliasPath in orderBy) {
|
||||
const path = aliasPath.split(".")
|
||||
const field = path.pop()
|
||||
const attr = path.join(".")
|
||||
const alias = aliasMapping[attr]
|
||||
const direction = orderBy[aliasPath]
|
||||
|
||||
queryBuilder.orderByRaw(`${alias}.data->>'${field}' ${direction}`)
|
||||
}
|
||||
|
||||
let sql = `WITH data AS (${queryBuilder.toQuery()})
|
||||
SELECT * ${
|
||||
countAllResults ? ", (SELECT max(offset_) FROM data) AS count" : ""
|
||||
}
|
||||
FROM data`
|
||||
|
||||
let take_ = !isNaN(+take!) ? +take! : 15
|
||||
let skip_ = !isNaN(+skip!) ? +skip! : 0
|
||||
if (typeof take === "number" || typeof skip === "number") {
|
||||
sql += `
|
||||
WHERE offset_ > ${skip_}
|
||||
AND offset_ <= ${skip_ + take_}
|
||||
`
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
public buildObjectFromResultset(
|
||||
resultSet: Record<string, any>[]
|
||||
): Record<string, any>[] {
|
||||
const structure = this.structure
|
||||
const rootKey = this.getStructureKeys(structure)[0]
|
||||
|
||||
const maps: { [key: string]: { [id: string]: Record<string, any> } } = {}
|
||||
const isListMap: { [path: string]: boolean } = {}
|
||||
const referenceMap: { [key: string]: any } = {}
|
||||
const pathDetails: {
|
||||
[key: string]: { property: string; parents: string[]; parentPath: string }
|
||||
} = {}
|
||||
|
||||
const initializeMaps = (structure: Select, path: string[]) => {
|
||||
const currentPath = path.join(".")
|
||||
maps[currentPath] = {}
|
||||
|
||||
if (path.length > 1) {
|
||||
const property = path[path.length - 1]
|
||||
const parents = path.slice(0, -1)
|
||||
const parentPath = parents.join(".")
|
||||
|
||||
isListMap[currentPath] = !!this.getEntity(currentPath).ref.parents.find(
|
||||
(p) => p.targetProp === property
|
||||
)?.isList
|
||||
|
||||
pathDetails[currentPath] = { property, parents, parentPath }
|
||||
}
|
||||
|
||||
const children = this.getStructureKeys(structure)
|
||||
for (const key of children) {
|
||||
initializeMaps(structure[key] as Select, [...path, key])
|
||||
}
|
||||
}
|
||||
initializeMaps(structure[rootKey] as Select, [rootKey])
|
||||
|
||||
function buildReferenceKey(
|
||||
path: string[],
|
||||
id: string,
|
||||
row: Record<string, any>
|
||||
) {
|
||||
let current = ""
|
||||
let key = ""
|
||||
for (const p of path) {
|
||||
current += `${p}`
|
||||
key += row[`${current}.id`] + "."
|
||||
current += "."
|
||||
}
|
||||
return key + id
|
||||
}
|
||||
|
||||
resultSet.forEach((row) => {
|
||||
for (const path in maps) {
|
||||
const id = row[`${path}.id`]
|
||||
|
||||
// root level
|
||||
if (!pathDetails[path]) {
|
||||
if (!maps[path][id]) {
|
||||
maps[path][id] = row[path] || undefined
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const { property, parents, parentPath } = pathDetails[path]
|
||||
const referenceKey = buildReferenceKey(parents, id, row)
|
||||
|
||||
if (referenceMap[referenceKey]) {
|
||||
continue
|
||||
}
|
||||
|
||||
maps[path][id] = row[path] || undefined
|
||||
|
||||
const parentObj = maps[parentPath][row[`${parentPath}.id`]]
|
||||
|
||||
if (!parentObj) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isList = isListMap[parentPath + "." + property]
|
||||
if (isList) {
|
||||
parentObj[property] ??= []
|
||||
}
|
||||
|
||||
if (maps[path][id] !== undefined) {
|
||||
if (isList) {
|
||||
parentObj[property].push(maps[path][id])
|
||||
} else {
|
||||
parentObj[property] = maps[path][id]
|
||||
}
|
||||
}
|
||||
|
||||
referenceMap[referenceKey] = true
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(maps[rootKey] ?? {})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user