Skip to content

Commit

Permalink
Merge pull request #5271 from neo4j/global-id-v6
Browse files Browse the repository at this point in the history
Global id v6
  • Loading branch information
angrykoala authored Jul 1, 2024
2 parents 99c50f3 + d7462a3 commit 19917a0
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ResolveTree } from "graphql-parse-resolve-info";
import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity";
import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser";
import type { GraphQLTreeReadOperation } from "./graphql-tree";

export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser {
constructor({ entity }: { entity: ConcreteEntity }) {
super({ entity: entity });
}

/** Parse a resolveTree into a Neo4j GraphQLTree */
public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation {
const entityTypes = this.targetNode.typeNames;
resolveTree.fieldsByTypeName[entityTypes.node] = {
...resolveTree.fieldsByTypeName["Node"],
...resolveTree.fieldsByTypeName[entityTypes.node],
};
const node = resolveTree ? this.parseNode(resolveTree) : undefined;

return {
alias: resolveTree.alias,
args: {
where: {
edges: {
node: {
id: { equals: resolveTree.args.id as any },
},
},
},
},
name: resolveTree.name,
fields: {
connection: {
alias: "connection",
args: {},
fields: {
edges: {
alias: "edges",
args: {},
fields: {
node,
},
},
},
},
},
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import type { ResolveTree } from "graphql-parse-resolve-info";
import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity";
import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser";
import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser";
import type { GraphQLTree } from "./graphql-tree";

Expand All @@ -32,3 +33,14 @@ export function parseResolveInfoTree({
const parser = new TopLevelResolveTreeParser({ entity });
return parser.parseOperation(resolveTree);
}

export function parseGlobalNodeResolveInfoTree({
resolveTree,
entity,
}: {
resolveTree: ResolveTree;
entity: ConcreteEntity;
}): GraphQLTree {
const parser = new GlobalNodeResolveTreeParser({ entity });
return parser.parseOperation(resolveTree);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import type { ConnectionQueryArgs } from "../../types";
import { toGlobalId } from "../../utils/global-ids";

/** Maps the database id to globalId*/
export function generateGlobalIdResolver({ entity }: { entity: ConcreteEntity }) {
/** Maps the database id field to globalId */
export function generateGlobalIdFieldResolver({ entity }: { entity: ConcreteEntity }) {
return function resolve(source, _args: ConnectionQueryArgs, _ctx, _info: GraphQLResolveInfo) {
const globalAttribute = entity.globalIdField;
if (!globalAttribute) {
Expand Down
51 changes: 51 additions & 0 deletions packages/graphql/src/api-v6/resolvers/global-node-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { GraphQLResolveInfo } from "graphql";
import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import type { GlobalNodeArgs } from "../../types";
import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context";
import { execute } from "../../utils";
import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree";
import { fromGlobalId } from "../../utils/global-ids";
import { parseGlobalNodeResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree";
import { translateReadOperation } from "../translators/translate-read-operation";

/** Maps the database id field to globalId */
export function generateGlobalNodeResolver({ globalEntities }: { globalEntities: ConcreteEntity[] }) {
return async function resolve(
_source,
args: GlobalNodeArgs,
context: Neo4jGraphQLTranslationContext,
info: GraphQLResolveInfo
) {
const resolveTree = getNeo4jResolveTree(info, { args });
context.resolveTree = resolveTree;

const { typeName, field, id } = fromGlobalId(args.id);
if (!typeName || !field || !id) return null;

const entity = globalEntities.find((n) => n.name === typeName);
if (!entity) return null;

const graphQLTree = parseGlobalNodeResolveInfoTree({ resolveTree: context.resolveTree, entity });
const { cypher, params } = translateReadOperation({
context: context,
graphQLTree,
entity,
});
const executeResult = await execute({
cypher,
params,
defaultAccessMode: "READ",
context,
info,
});

let obj = null;

const thisValue = executeResult.records[0]?.this.connection.edges[0].node;

if (executeResult.records.length && thisValue) {
obj = { ...thisValue, id: args.id, __resolveType: entity.name };
}
return obj;
};
}
36 changes: 34 additions & 2 deletions packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
import type {
EnumTypeComposer,
InputTypeComposer,
InterfaceTypeComposer,
ListComposer,
NonNullComposer,
ObjectTypeComposer,
Expand Down Expand Up @@ -76,16 +77,44 @@ export class SchemaBuilder {
onCreate: () => {
fields: Record<string, FieldDefinition | string | WrappedComposer<ObjectTypeComposer>>;
description?: string;
iface?: InterfaceTypeComposer;
}
): ObjectTypeComposer {
return this.composer.getOrCreateOTC(name, (tc) => {
const { fields, description, iface } = onCreate();
if (fields) {
tc.addFields(fields);
}
if (description) {
tc.setDescription(description);
}
if (iface) {
tc.addInterface(iface);
}
});
}

public getOrCreateInterfaceType(
name: string,
onCreate: () => {
fields: Record<string, FieldDefinition | string | WrappedComposer<ObjectTypeComposer>>;
description?: string;
}
): InterfaceTypeComposer {
return this.composer.getOrCreateIFTC(name, (tc) => {
const { fields, description } = onCreate();

if (fields) {
tc.addFields(fields);
}
if (description) {
tc.setDescription(description);
}

// This is used for global node, not sure if needed for other interfaces
tc.setResolveType((obj) => {
return obj.__resolveType;
});
});
}

Expand Down Expand Up @@ -144,17 +173,20 @@ export class SchemaBuilder {
type,
args,
resolver,
description,
}: {
name: string;
type: ObjectTypeComposer;
args: Record<string, InputTypeComposer>;
type: ObjectTypeComposer | InterfaceTypeComposer;
args: Record<string, InputTypeComposer | string>;
resolver: (...args: any[]) => any;
description?: string;
}): void {
this.composer.Query.addFields({
[name]: {
type: type,
args,
resolve: resolver,
description,
},
});
}
Expand Down
19 changes: 19 additions & 0 deletions packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import type { GraphQLSchema } from "graphql";
import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel";
import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import { generateGlobalNodeResolver } from "../resolvers/global-node-resolver";
import { generateReadResolver } from "../resolvers/read-resolver";
import { SchemaBuilder } from "./SchemaBuilder";
import { SchemaTypes } from "./schema-types/SchemaTypes";
Expand All @@ -36,6 +37,8 @@ export class SchemaGenerator {
public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema {
const staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder });
this.generateEntityTypes(schemaModel, staticTypes);
this.generateGlobalNodeQuery(schemaModel, staticTypes);

return this.schemaBuilder.build();
}

Expand All @@ -62,4 +65,20 @@ export class SchemaGenerator {
entitySchemaTypes.addTopLevelQueryField(resolver);
}
}

private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void {
const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField);

if (globalEntities.length > 0) {
this.schemaBuilder.addQueryField({
name: "node",
type: staticTypes.globalNodeInterface,
args: {
id: "ID!",
},
description: "Fetches an object given its ID",
resolver: generateGlobalNodeResolver({ globalEntities }),
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@

import type { GraphQLInputType, GraphQLScalarType } from "graphql";
import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql";
import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose";
import type {
EnumTypeComposer,
InputTypeComposer,
InterfaceTypeComposer,
ListComposer,
ObjectTypeComposer,
} from "graphql-compose";
import { Memoize } from "typescript-memoize";
import { CartesianPointDistance } from "../../../graphql/input-objects/CartesianPointDistance";
import { CartesianPointInput } from "../../../graphql/input-objects/CartesianPointInput";
Expand Down Expand Up @@ -76,6 +82,16 @@ export class StaticSchemaTypes {
public get sortDirection(): EnumTypeComposer {
return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]);
}

public get globalNodeInterface(): InterfaceTypeComposer {
return this.schemaBuilder.getOrCreateInterfaceType("Node", () => {
return {
fields: {
id: "ID!",
},
};
});
}
}

class StaticFilterTypes {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { type GraphQLResolveInfo } from "graphql";
import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose";
import type { InputTypeComposer, InterfaceTypeComposer, ObjectTypeComposer } from "graphql-compose";
import { Memoize } from "typescript-memoize";
import type { Attribute } from "../../../schema-model/attribute/Attribute";
import type { AttributeType, Neo4jGraphQLScalarType } from "../../../schema-model/attribute/AttributeType";
Expand All @@ -33,7 +33,7 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity
import { idResolver } from "../../../schema/resolvers/field/id";
import { numericalResolver } from "../../../schema/resolvers/field/numerical";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import { generateGlobalIdResolver } from "../../resolvers/global-id-resolver";
import { generateGlobalIdFieldResolver } from "../../resolvers/global-id-field-resolver";
import type { TopLevelEntityTypeNames } from "../../schema-model/graphql-type-names/TopLevelEntityTypeNames";
import type { FieldDefinition, GraphQLResolver, SchemaBuilder } from "../SchemaBuilder";
import { EntitySchemaTypes } from "./EntitySchemaTypes";
Expand Down Expand Up @@ -107,8 +107,14 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes<TopLevelEntityT
const fields = this.getNodeFieldsDefinitions();
const relationships = this.getRelationshipFields();

let iface: InterfaceTypeComposer | undefined;
if (this.entity.isConcreteEntity() && this.entity.globalIdField) {
iface = this.schemaTypes.staticTypes.globalNodeInterface;
}

return {
fields: { ...fields, ...relationships },
iface,
};
});
}
Expand Down Expand Up @@ -198,7 +204,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes<TopLevelEntityT
type: "ID!",
args: {},
description: "",
resolve: generateGlobalIdResolver({ entity: this.entity }),
resolve: generateGlobalIdFieldResolver({ entity: this.entity }),
};
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/graphql/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ export interface ConnectionQueryArgs {
sort?: ConnectionSortArg[];
}

export interface GlobalNodeArgs {
id: string;
}

/**
* Representation of the options arg
* passed to resolvers.
Expand Down
Loading

0 comments on commit 19917a0

Please sign in to comment.