diff --git a/packages/graphql/package.json b/packages/graphql/package.json index f0c77dc6e8..ba319919ee 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -52,6 +52,7 @@ "@apollo/gateway": "2.7.0", "@apollo/server": "4.10.0", "@faker-js/faker": "8.3.1", + "@neo4j/cypher-builder": "../../../cypher-builder", "@types/deep-equal": "1.0.4", "@types/is-uuid": "1.0.2", "@types/jest": "29.5.11", @@ -91,7 +92,6 @@ "@graphql-tools/resolvers-composition": "^7.0.0", "@graphql-tools/schema": "10.0.2", "@graphql-tools/utils": "^10.0.0", - "@neo4j/cypher-builder": "^1.7.1", "camelcase": "^6.3.0", "debug": "^4.3.4", "deep-equal": "^2.0.5", @@ -105,6 +105,7 @@ "typescript-memoize": "^1.1.1" }, "peerDependencies": { + "@neo4j/cypher-builder": "^1.7.0", "graphql": "^16.0.0", "neo4j-driver": "^5.8.0" } diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index 45f037bd59..8f0ef62828 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -38,6 +38,8 @@ import { createAuthorizationBeforePredicateField } from "./authorization/create- import { checkAuthentication } from "./authorization/check-authentication"; import { compileCypher } from "../utils/compile-cypher"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; +import { uniqSubQueries } from "./queryAST/ast/operations/composite/optimization"; +import { UNION_UNIFICATION_ENABLED } from "./queryAST/ast/operations/optimizationSettings"; interface Res { projection: Cypher.Expr[]; @@ -141,7 +143,8 @@ export default function createProjectionAndParams({ const subqueryReturnAlias = new Cypher.Variable(); if (relationField.interface || relationField.union) { - let referenceNodes; + let isSelectingAllChildren = false; + let referenceNodes: Node[]; if (relationField.interface) { const interfaceImplementations = context.nodes.filter((x) => relationField.interface?.implementations?.includes(x.name) @@ -172,6 +175,9 @@ export default function createProjectionAndParams({ // where exists and has a filter on this implementation Object.prototype.hasOwnProperty.call(field.args.where, x.name) ); + + isSelectingAllChildren = + relationField.interface && interfaceImplementations.length === referenceNodes.length; } else { referenceNodes = context.nodes.filter( (x) => @@ -182,54 +188,81 @@ export default function createProjectionAndParams({ const parentNode = varName; - const unionSubqueries: Cypher.Clause[] = []; - for (const refNode of referenceNodes) { - const targetNode = new Cypher.Node({ labels: refNode.getLabels(context) }); - const recurse = createProjectionAndParams({ - resolveTree: field, - node: refNode, - context, - varName: targetNode, - cypherFieldAliasMap, - }); - res.params = { ...res.params, ...recurse.params }; - - const direction = getCypherRelationshipDirection(relationField, field.args); - - const nestedProjection = new Cypher.Raw((env) => { - // The nested projection will be surrounded by brackets, so we want to remove - // any linebreaks, and then the first opening and the last closing bracket of the line, - // as well as any surrounding whitespace. - const nestedProj = compileCypher(recurse.projection, env).replaceAll( - /(^\s*{\s*)|(\s*}\s*$)/g, - "" - ); + const matchByInterfaceOrUnion = + UNION_UNIFICATION_ENABLED && isSelectingAllChildren + ? relationField.interface?.typeMeta.name + : undefined; - return `{ __resolveType: "${refNode.name}", __id: id(${compileCypher(varName, env)})${ - nestedProj && `, ${nestedProj}` - } }`; - }); - - const subquery = createProjectionSubquery({ - parentNode, - whereInput: field.args.where ? field.args.where[refNode.name] : {}, - node: refNode, - context, - subqueryReturnAlias, - nestedProjection, - nestedSubqueries: [...recurse.subqueriesBeforeSort, ...recurse.subqueries], - targetNode, - relationField, - relationshipDirection: direction, - optionsInput, - addSkipAndLimit: false, - collect: false, - nestedPredicates: recurse.predicates, - }); - - const unionWith = new Cypher.With("*"); - unionSubqueries.push(Cypher.concat(unionWith, subquery)); - } + const unionSubqueries = uniqSubQueries( + context, + matchByInterfaceOrUnion, + referenceNodes, + (node) => node, + (subs) => { + const unionSubqueries: Cypher.Clause[][] = []; + for (const { child: refNode, unifyViaDataModelType, exclusionPredicates } of subs) { + const labels = + unifyViaDataModelType && context.labelManager + ? context.labelManager.getLabelSelectorExpressionObject(unifyViaDataModelType) + : refNode.getLabels(context); + + const targetNode = new Cypher.Node({ labels }); + const recurse = createProjectionAndParams({ + resolveTree: field, + node: refNode, + context, + varName: targetNode, + cypherFieldAliasMap, + }); + + res.params = { ...res.params, ...recurse.params }; + + const combinedPredicates = [ + ...(recurse.predicates ?? []), + ...(exclusionPredicates?.(targetNode) ?? []), + ]; + + const direction = getCypherRelationshipDirection(relationField, field.args); + + const nestedProjection = new Cypher.Raw((env) => { + // The nested projection will be surrounded by brackets, so we want to remove + // any linebreaks, and then the first opening and the last closing bracket of the line, + // as well as any surrounding whitespace. + const nestedProj = compileCypher(recurse.projection, env).replaceAll( + /(^\s*{\s*)|(\s*}\s*$)/g, + "" + ); + + return `{ __resolveType: ${ + UNION_UNIFICATION_ENABLED && context.labelManager?.hasMainType(refNode.name) + ? new Cypher.Property(targetNode, "mainType").getCypher(env) + : `"${refNode.name}"` + }, __id: id(${compileCypher(varName, env)})${nestedProj && `, ${nestedProj}`} }`; + }); + + const subquery = createProjectionSubquery({ + parentNode, + whereInput: field.args.where ? field.args.where[refNode.name] : {}, + node: refNode, + context, + subqueryReturnAlias, + nestedProjection, + nestedSubqueries: [...recurse.subqueriesBeforeSort, ...recurse.subqueries], + targetNode, + relationField, + relationshipDirection: direction, + optionsInput, + addSkipAndLimit: false, + collect: false, + nestedPredicates: combinedPredicates, + }); + + const unionWith = new Cypher.With("*"); + unionSubqueries.push([Cypher.concat(unionWith, subquery)]); + } + return unionSubqueries; + } + ).flat(); const unionClause = new Cypher.Union(...unionSubqueries); diff --git a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts index 07b4aca95b..190a84f0f2 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts @@ -143,6 +143,10 @@ export class ConnectionFilter extends Filter { protected getLabelPredicate(context: QueryASTContext): Cypher.Predicate | undefined { if (!hasTarget(context)) throw new Error("No parent node found!"); if (isConcreteEntity(this.target)) return undefined; + if (context.neo4jGraphQLContext.labelManager) { + const filterExpr = context.neo4jGraphQLContext.labelManager.getLabelSelectorExpression(this.target.name); + return new Cypher.Raw((env) => `${context.target.getCypher(env)}:${filterExpr}`); + } const labelPredicate = this.target.concreteEntities.map((e) => { return context.target.hasLabels(...e.labels); }); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts index 5ec50b3925..9442b8e4f1 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts @@ -35,6 +35,7 @@ import { CypherPropertySort } from "../sort/CypherPropertySort"; import type { Sort } from "../sort/Sort"; import type { OperationTranspileResult } from "./operations"; import { Operation } from "./operations"; +import { READ_LOWER_TARGET_INTERFACE_ENABLED } from "./optimizationSettings"; export class ReadOperation extends Operation { public readonly target: ConcreteEntityAdapter; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts index 7326bd3e15..c71fbc9494 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts @@ -28,6 +28,7 @@ import type { Sort, SortField } from "../../sort/Sort"; import type { OperationTranspileResult } from "../operations"; import { Operation } from "../operations"; import type { CompositeReadPartial } from "./CompositeReadPartial"; +import { uniqSubQueries } from "./optimization"; export class CompositeReadOperation extends Operation { private children: CompositeReadPartial[]; @@ -57,15 +58,25 @@ export class CompositeReadOperation extends Operation { public transpile(context: QueryASTContext): OperationTranspileResult { const parentNode = context.target; - const nestedSubqueries = this.children.flatMap((c) => { - const result = c.transpile(context); - let clauses = result.clauses; - if (parentNode) { - clauses = clauses.map((sq) => Cypher.concat(new Cypher.With("*"), sq)); - } - return clauses; - }); + const isSelectingAllChildren = this.entity.concreteEntities.length === this.children.length; + const matchByInterfaceOrUnion = isSelectingAllChildren ? this.entity.name : undefined; + const nestedSubqueries = uniqSubQueries( + context.neo4jGraphQLContext, + matchByInterfaceOrUnion, + this.children, + (child) => child.target, + (subs) => + subs.map(({ child, unifyViaDataModelType, exclusionPredicates }) => { + const result = child.transpile(context, unifyViaDataModelType, exclusionPredicates); + + let clauses = result.clauses; + if (parentNode) { + clauses = clauses.map((sq) => Cypher.concat(new Cypher.With("*"), sq)); + } + return clauses; + }) + ).flat(); let aggrExpr: Cypher.Expr = Cypher.collect(context.returnVariable); if (!this.relationship) { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts index 2b43ae83ae..c7d1779656 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts @@ -24,24 +24,30 @@ import type { QueryASTContext } from "../../QueryASTContext"; import type { SelectionClause } from "../../selection/EntitySelection"; import { ReadOperation } from "../ReadOperation"; import type { OperationTranspileResult } from "../operations"; +import { UNION_UNIFICATION_ENABLED } from "../optimizationSettings"; export class CompositeReadPartial extends ReadOperation { - public transpile(context: QueryASTContext) { + public transpile( + context: QueryASTContext, + matchByInterfaceOrUnion?: string, + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] + ) { if (this.relationship) { - return this.transpileNestedCompositeRelationship(this.relationship, context); + return this.transpileNestedCompositeRelationship(context, matchByInterfaceOrUnion, exclusionPredicates); } else { return this.transpileTopLevelCompositeEntity(context); } } private transpileNestedCompositeRelationship( - entity: RelationshipAdapter, - context: QueryASTContext + context: QueryASTContext, + matchByInterfaceOrUnion?: string, + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] ): OperationTranspileResult { if (!hasTarget(context)) throw new Error("No parent node found!"); // eslint-disable-next-line prefer-const - let { selection: matchClause, nestedContext } = this.selection.apply(context); + let { selection: matchClause, nestedContext } = this.selection.apply(context, matchByInterfaceOrUnion); let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { return f.getSelection(nestedContext); @@ -56,7 +62,11 @@ export class CompositeReadPartial extends ReadOperation { const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); - const wherePredicate = Cypher.and(filterPredicates, ...authFiltersPredicate); + const wherePredicate = Cypher.and( + filterPredicates, + ...authFiltersPredicate, + ...(exclusionPredicates?.(nestedContext.target) ?? []) + ); if (wherePredicate) { // NOTE: This is slightly different to ReadOperation for cypher compatibility, this could use `WITH *` matchClause.where(wherePredicate); @@ -124,7 +134,10 @@ export class CompositeReadPartial extends ReadOperation { const targetNodeName = this.target.name; projection.set({ - __resolveType: new Cypher.Literal(targetNodeName), + __resolveType: + UNION_UNIFICATION_ENABLED && context.neo4jGraphQLContext.labelManager?.hasMainType(targetNodeName) + ? new Cypher.Property(context.target, "mainType") + : new Cypher.Literal(targetNodeName), __id: Cypher.id(context.target), }); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts new file mode 100644 index 0000000000..4724441783 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts @@ -0,0 +1,120 @@ +import Cypher from "@neo4j/cypher-builder"; +import { groupBy, uniq, compact, zip } from "lodash"; +import { UNION_UNIFICATION_ENABLED } from "../optimizationSettings"; +import type { Neo4jGraphQLTranslationContext } from "../../../../../types/neo4j-graphql-translation-context"; + +function getMilliSeconds(hrTime: [number, number]) { + return (hrTime[0] * 1000000 + hrTime[1] / 1000.0) / 1000.0; +} + +interface UniqSubQueriesEntry { + child: T; + unifyViaDataModelType?: string; + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[]; +} + +interface Target { + name: string; + getLabels: (...args: any[]) => string[]; +} + +// +// This eliminiates sub-queries if they are EXACTLY equal. +// Note that we recursively calling this function, so basically we repeatedly +// compact the tree right to left and then top to bottom. +// Furthermore after compaction we need to recompute with different selectors. +// +// This is obviously not very efficient - but it's easy and robust. +// +export function uniqSubQueries( + context: Neo4jGraphQLTranslationContext, + matchUsingInterfaceOrUnion: string | undefined, + children: T[], + targetExtractor: (t: T) => Target, + nestedSubqueriesProducer: (subs: UniqSubQueriesEntry[]) => Cypher.Clause[][] +): Cypher.Clause[][] { + if (!UNION_UNIFICATION_ENABLED || !matchUsingInterfaceOrUnion || children.length < 2) { + return nestedSubqueriesProducer(children.map((c) => ({ child: c }))); + } + + const hrStart = process.hrtime(); + + // TODO: This way of doing things is really slow! - around 50ms for a realistic query. + // It seems like the most expensive part is building the subquery twice! + + const nestedSubqueries = nestedSubqueriesProducer( + children.map((c) => ({ child: c, unifyViaDataModelType: matchUsingInterfaceOrUnion })) + ); + + const nestedSubqueriesWithChildren = zip(nestedSubqueries, children); + + const groupedChildren = compact( + uniq( + Object.values( + groupBy(nestedSubqueriesWithChildren, ([sqLst, child]) => { + if (!child) { + throw new Error("optimizer: child missing. should not happen."); + } + if (!sqLst) { + throw new Error("optimizer: sqLst missing. should not happen."); + } + return sqLst + .map((sq) => { + const { cypher, params } = sq.build(); + // console.log(">>> hopefully similiar parts: ", cypher); + if (Object.keys(params).length > 0) { + return `${cypher}:${JSON.stringify(params)}`; // group by query. + } + return cypher; // group by query. + }) + .join(";"); + }) + ).map((group) => compact(group.map((g) => g[1]))) + ) + ); + + const { labelManager } = context; + + function computeExclusionLabels(groupedChildren: T[][], g: T[]): (Cypher.LabelExpr | string[])[] { + const labels = groupedChildren + .filter((cur) => cur !== g) + .flatMap((cur) => + cur.map((it) => { + const target = targetExtractor(it); + console.log(">>> BUILDING exclusion label for " + target.name); + if (labelManager) { + return labelManager.getLabelSelectorExpressionObject(target.name); + } + return target.getLabels(); + }) + ); + return labels; + } + + const res = groupedChildren.map((g) => ({ + child: g[0] as T, + ...(g.length > 1 + ? ({ + unifyViaDataModelType: matchUsingInterfaceOrUnion, + exclusionPredicates: (matchNode) => { + const exclusions = computeExclusionLabels(groupedChildren, g); + if (exclusions.length > 0) { + return [ + Cypher.not( + Cypher.or( + ...exclusions.map((orGroup) => Cypher.and(matchNode.hasLabelsOf(orGroup, true))) + ) + ), + ]; + } + return []; + }, + } satisfies Partial>) + : {}), + })); + + const time = getMilliSeconds(process.hrtime(hrStart)); + console.log(">>> optimization took (ms): ", time); + + return nestedSubqueriesProducer(res); +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts b/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts new file mode 100644 index 0000000000..3fdb0c7d89 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts @@ -0,0 +1,5 @@ +export const UNION_UNIFICATION_ENABLED = process.env.OPTIMIZE_UNIONS !== "0"; +export const READ_LOWER_TARGET_INTERFACE_ENABLED = process.env.OPTIMIZE_TARGET_INTERFACE !== "0"; + +console.log(">>> UNION_UNIFICATION_ENABLED: ", UNION_UNIFICATION_ENABLED); +console.log(">>> READ_LOWER_TARGET_INTERFACE_ENABLED: ", READ_LOWER_TARGET_INTERFACE_ENABLED); diff --git a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts index 7afbf0f8a9..d272a7202d 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts @@ -30,7 +30,10 @@ export abstract class EntitySelection extends QueryASTNode { /** Apply selection over the given context, returns the updated context and the selection clause * TODO: Improve naming */ - public abstract apply(context: QueryASTContext): { + public abstract apply( + context: QueryASTContext, + matchByInterfaceOrUnion?: string + ): { nestedContext: QueryASTContext; selection: SelectionClause; }; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts index e967fe7df8..2d168c98ce 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts @@ -24,6 +24,7 @@ import { hasTarget } from "../../utils/context-has-target"; import { createNodeFromEntity, createRelationshipFromEntity } from "../../utils/create-node-from-entity"; import type { QueryASTContext } from "../QueryASTContext"; import { EntitySelection, type SelectionClause } from "./EntitySelection"; +import { READ_LOWER_TARGET_INTERFACE_ENABLED } from "../operations/optimizationSettings"; export class RelationshipSelection extends EntitySelection { private relationship: RelationshipAdapter; @@ -54,7 +55,10 @@ export class RelationshipSelection extends EntitySelection { this.optional = optional ?? false; } - public apply(context: QueryASTContext): { + public apply( + context: QueryASTContext, + matchByInterfaceOrUnion?: string + ): { nestedContext: QueryASTContext; selection: SelectionClause; } { @@ -62,9 +66,26 @@ export class RelationshipSelection extends EntitySelection { const relVar = createRelationshipFromEntity(this.relationship); const relationshipTarget = this.targetOverride ?? this.relationship.target; - const targetNode = createNodeFromEntity(relationshipTarget, context.neo4jGraphQLContext, this.alias); const relDirection = this.relationship.getCypherDirection(this.directed); + const lowerToTargetType = + matchByInterfaceOrUnion ?? + (READ_LOWER_TARGET_INTERFACE_ENABLED + ? context.neo4jGraphQLContext.labelManager?.getLowerTargetInterfaceIfSafeRelationship( + this.relationship.source.name, + this.relationship.name + ) + : null); + + const targetNode = + lowerToTargetType && context.neo4jGraphQLContext.labelManager + ? new Cypher.Node({ + labels: context.neo4jGraphQLContext.labelManager.getLabelSelectorExpressionObject( + lowerToTargetType + ), + }) + : createNodeFromEntity(relationshipTarget, context.neo4jGraphQLContext, this.alias); + const pattern = new Cypher.Pattern(context.target) .withoutLabels() .related(relVar) diff --git a/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts b/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts index e7cdcb6678..723134147e 100644 --- a/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts +++ b/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts @@ -30,7 +30,13 @@ export function createNodeFromEntity( name?: string ): Cypher.Node { const nodeLabels = entity instanceof ConcreteEntityAdapter ? entity.getLabels() : [entity.name]; - const labels = neo4jGraphQLContext ? mapLabelsWithContext(nodeLabels, neo4jGraphQLContext) : nodeLabels; + const hasContextLabels = nodeLabels.some((l) => l.startsWith("$")); + const _labels = neo4jGraphQLContext ? mapLabelsWithContext(nodeLabels, neo4jGraphQLContext) : nodeLabels; + + const labels = + hasContextLabels || !neo4jGraphQLContext?.labelManager + ? _labels + : neo4jGraphQLContext.labelManager.getLabelSelectorExpressionObject(entity.name); if (name) { return new Cypher.NamedNode(name, { labels }); diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index d8798d29ff..60e0dc1d96 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -71,7 +71,12 @@ export default async function translateUpdate({ const createStrs: string[] = []; let deleteStr = ""; const assumeReconnecting = Boolean(connectInput) && Boolean(disconnectInput); - const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); + + const labels = context.labelManager + ? context.labelManager.getLabelSelectorExpressionObject(node.name) + : node.getLabels(context); + + const matchNode = new Cypher.NamedNode(varName, { labels }); const where = resolveTree.args.where as GraphQLWhereArg | undefined; const topLevelMatch = translateTopLevelMatch({ matchNode, node, context, operation: "UPDATE", where }); matchAndWhereStr = topLevelMatch.cypher; diff --git a/packages/graphql/src/types/neo4j-graphql-context.ts b/packages/graphql/src/types/neo4j-graphql-context.ts index e5bf71f1db..26b51f8a4d 100644 --- a/packages/graphql/src/types/neo4j-graphql-context.ts +++ b/packages/graphql/src/types/neo4j-graphql-context.ts @@ -17,11 +17,33 @@ * limitations under the License. */ +import type { LabelExpr } from "@neo4j/cypher-builder"; import type { CypherQueryOptions } from "."; import type { ExecutionContext, Neo4jGraphQLSessionConfig } from "../classes/Executor"; import type { Neo4jGraphQLContextInterface } from "./neo4j-graphql-context-interface"; +type DataModelType = any; // TODO! + +interface LabelSelectorExpressionOptions { + restrict?: DataModelType; +} + +export interface LabelManager { + getLowerTargetInterfaceIfSafeRelationship: ( + dataModelType: DataModelType, + fieldName: string + ) => DataModelType | null; + hasMainType: (dataModelType: DataModelType) => boolean; + getLabelSelectorExpressionObject: ( + dataModelType: DataModelType, + options?: LabelSelectorExpressionOptions + ) => LabelExpr | string[]; + getLabelSelectorExpression: (dataModelType: DataModelType, options?: LabelSelectorExpressionOptions) => string; +} + export interface Neo4jGraphQLContext extends Neo4jGraphQLContextInterface { + labelManager?: LabelManager; + /** * Parameters to be used when querying with Cypher. * diff --git a/yarn.lock b/yarn.lock index ddad2d804e..e2f3b440d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3566,10 +3566,10 @@ __metadata: languageName: node linkType: hard -"@neo4j/cypher-builder@npm:^1.7.1": - version: 1.7.1 - resolution: "@neo4j/cypher-builder@npm:1.7.1" - checksum: 545953bffec407c65ac3531868e8853376ecf254dfa15cfabb4e929417d643164147c0bfd3bb28fdb37521bfc8573842d441d947513bf58c516287da133bf2c2 +"@neo4j/cypher-builder@file:../../../cypher-builder::locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql": + version: 1.7.4 + resolution: "@neo4j/cypher-builder@file:../../../cypher-builder#../../../cypher-builder::hash=630c22&locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql" + checksum: cd5bb11ac447ea45c200e1c9c747e12cba245ea0f780796398a91894bc66b4b0618e2edc485bd4bd7bc5b5193523147f91e63bdcbfdf0e623e552f7971c8fe62 languageName: node linkType: hard @@ -3713,7 +3713,7 @@ __metadata: "@graphql-tools/resolvers-composition": ^7.0.0 "@graphql-tools/schema": 10.0.2 "@graphql-tools/utils": ^10.0.0 - "@neo4j/cypher-builder": ^1.7.1 + "@neo4j/cypher-builder": ../../../cypher-builder "@types/deep-equal": 1.0.4 "@types/is-uuid": 1.0.2 "@types/jest": 29.5.11 @@ -3758,6 +3758,7 @@ __metadata: typescript-memoize: ^1.1.1 ws: 8.16.0 peerDependencies: + "@neo4j/cypher-builder": ^1.7.0 graphql: ^16.0.0 neo4j-driver: ^5.8.0 languageName: unknown