Skip to content

Commit

Permalink
Stronger typings for context (#3779)
Browse files Browse the repository at this point in the history
* Pass info into Executor rather than putting in context

* Typings for fulltext in current context

* Remove unused context entries

* Remove some unneeded context references

* Add context types

* Switch to new context typings

* Remove legacy Context type
  • Loading branch information
darrellwarde authored Aug 15, 2023
1 parent 1eefde4 commit 28fd15e
Show file tree
Hide file tree
Showing 83 changed files with 526 additions and 473 deletions.
14 changes: 9 additions & 5 deletions packages/graphql/src/classes/CallbackBucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
* limitations under the License.
*/

import type { Context, Neo4jGraphQLCallbacks } from "../types";
import type { Neo4jGraphQLCallbacks } from "../types";
import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context";
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";

export interface Callback {
functionName: string;
Expand All @@ -27,9 +29,9 @@ export interface Callback {

export class CallbackBucket {
public callbacks: Callback[];
private context: Context;
private context: Neo4jGraphQLTranslationContext;

constructor(context: Context) {
constructor(context: Neo4jGraphQLTranslationContext) {
this.context = context;
this.callbacks = [];
}
Expand All @@ -46,10 +48,12 @@ export class CallbackBucket {

await Promise.all(
this.callbacks.map(async (cb) => {
const callbackFunction = (this.context?.callbacks as Neo4jGraphQLCallbacks)[cb.functionName] as (
const callbackFunction = (this.context.features.populatedBy?.callbacks as Neo4jGraphQLCallbacks)[
cb.functionName
] as (
parent?: Record<string, unknown>,
args?: Record<string, never>,
context?: Record<string, unknown>
context?: Neo4jGraphQLContext
) => Promise<any>;
const param = await callbackFunction(cb.parent, {}, this.context);

Expand Down
12 changes: 3 additions & 9 deletions packages/graphql/src/classes/Node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -861,7 +861,6 @@ describe("Node", () => {
jwt: {
movielabel: "Movie",
},
myKey: "key",
})
.instance();

Expand All @@ -881,14 +880,10 @@ describe("Node", () => {
})
.instance();

const context = new ContextBuilder()
.with({
myKey: "Movie",
})
.instance();
const context = new ContextBuilder().instance();

const labels = node.getLabels(context);
const labelString = node.getLabelString(context);
const labels = node.getLabels({ ...context, myKey: "Movie" } as Record<string, any>);
const labelString = node.getLabelString({ ...context, myKey: "Movie" } as Record<string, any>);

expect(labels).toEqual(["Movie"]);
expect(labelString).toBe(":Movie");
Expand All @@ -908,7 +903,6 @@ describe("Node", () => {
jwt: {
movielabel: "Movie",
},
myKey: "key",
})
.instance();

Expand Down
7 changes: 3 additions & 4 deletions packages/graphql/src/classes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import type { DirectiveNode, NamedTypeNode } from "graphql";
import pluralize from "pluralize";
import type {
ConnectionField,
Context,
CustomEnumField,
CustomScalarField,
CypherField,
Expand All @@ -45,6 +44,7 @@ import type { NodeDirective } from "./NodeDirective";
import type { QueryOptionsDirective } from "./QueryOptionsDirective";
import type { SchemaConfiguration } from "../schema/schema-configuration";
import { leadingUnderscores } from "../utils/leading-underscore";
import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context";

export interface NodeConstructor extends GraphElementConstructor {
name: string;
Expand Down Expand Up @@ -235,7 +235,6 @@ class Node extends GraphElement {
};
}


public get fulltextTypeNames(): FulltextTypeNames {
return {
result: `${this.pascalCaseSingular}FulltextResult`,
Expand Down Expand Up @@ -284,11 +283,11 @@ class Node extends GraphElement {
};
}

public getLabelString(context: Context): string {
public getLabelString(context: Neo4jGraphQLContext): string {
return this.nodeDirective?.getLabelsString(this.name, context) || `:${this.name}`;
}

public getLabels(context: Context): string[] {
public getLabels(context: Neo4jGraphQLContext): string[] {
return this.nodeDirective?.getLabels(this.name, context) || [this.name];
}

Expand Down
8 changes: 6 additions & 2 deletions packages/graphql/src/classes/NodeDirective.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ describe("NodeDirective", () => {
});

test("should escape context labels", () => {
const context = new ContextBuilder({ escapeTest1: "123-321", escapeTest2: "He`l`lo" }).instance();
const context = new ContextBuilder().instance();
const instance = new NodeDirective({
labels: ["label", "$context.escapeTest1", "$context.escapeTest2"],
});
const labelString = instance.getLabelsString("label", context);
const labelString = instance.getLabelsString("label", {
...context,
escapeTest1: "123-321",
escapeTest2: "He`l`lo",
} as Record<string, any>);
expect(labelString).toBe(":label:`123-321`:`He``l``lo`");
});

Expand Down
8 changes: 4 additions & 4 deletions packages/graphql/src/classes/NodeDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

import dotProp from "dot-prop";
import { Neo4jGraphQLError } from "./Error";
import type { Context } from "../types";
import Cypher from "@neo4j/cypher-builder";
import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context";

export interface NodeDirectiveConstructor {
labels?: string[];
Expand All @@ -33,20 +33,20 @@ export class NodeDirective {
this.labels = input.labels || [];
}

public getLabelsString(typeName: string, context: Context): string {
public getLabelsString(typeName: string, context: Neo4jGraphQLContext): string {
if (!typeName) {
throw new Neo4jGraphQLError("Could not generate label string in @node directive due to empty typeName");
}
const labels = this.getLabels(typeName, context).map((l) => this.escapeLabel(l));
return `:${labels.join(":")}`;
}

public getLabels(typeName: string, context: Context): string[] {
public getLabels(typeName: string, context: Neo4jGraphQLContext): string[] {
const labels = !this.labels.length ? [typeName] : this.labels;
return this.mapLabelsWithContext(labels, context);
}

private mapLabelsWithContext(labels: string[], context: Context): string[] {
private mapLabelsWithContext(labels: string[], context: Neo4jGraphQLContext): string[] {
return labels.map((label: string) => {
if (label.startsWith("$")) {
// Trim $context. OR $ off the beginning of the string
Expand Down
26 changes: 21 additions & 5 deletions packages/graphql/src/classes/Subgraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ import type {
import { Kind, parse, print } from "graphql";
import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel";
import { translateResolveReference } from "../translate/translate-resolve-reference";
import type { Context, Node } from "../types";
import type { Node } from "../types";
import { execute } from "../utils";
import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree";
import { isInArray } from "../utils/is-in-array";
import type { Neo4jGraphQLComposedContext } from "../schema/resolvers/wrapper";
import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context";

// TODO fetch the directive names from the spec
const federationDirectiveNames = [
Expand All @@ -53,7 +55,11 @@ const federationDirectiveNames = [

type FederationDirectiveName = (typeof federationDirectiveNames)[number];

type ReferenceResolver = (reference, context: Context, info: GraphQLResolveInfo) => Promise<unknown>;
type ReferenceResolver = (
reference,
context: Neo4jGraphQLComposedContext,
info: GraphQLResolveInfo
) => Promise<unknown>;

export class Subgraph {
private importArgument: Map<FederationDirectiveName, string>;
Expand Down Expand Up @@ -127,7 +133,11 @@ export class Subgraph {
}

private getReferenceResolver(nodes: Node[]): ReferenceResolver {
const __resolveReference = async (reference, context: Context, info: GraphQLResolveInfo): Promise<unknown> => {
const __resolveReference = async (
reference,
context: Neo4jGraphQLComposedContext,
info: GraphQLResolveInfo
): Promise<unknown> => {
const { __typename } = reference;

const node = nodes.find((n) => n.name === __typename);
Expand All @@ -136,19 +146,25 @@ export class Subgraph {
throw new Error("Unable to find matching node");
}

context.resolveTree = getNeo4jResolveTree(info);
(context as Neo4jGraphQLTranslationContext).resolveTree = getNeo4jResolveTree(info);

const { cypher, params } = translateResolveReference({ context, node, reference });
const { cypher, params } = translateResolveReference({
context: context as Neo4jGraphQLTranslationContext,
node,
reference,
});

const executeResult = await execute({
cypher,
params,
defaultAccessMode: "READ",
context,
info,
});

return executeResult.records[0]?.this;
};

return __resolveReference;
}

Expand Down
11 changes: 8 additions & 3 deletions packages/graphql/src/schema/create-global-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@
import type { GraphQLResolveInfo } from "graphql";
import type { ObjectTypeComposerFieldConfigAsObjectDefinition, SchemaComposer } from "graphql-compose";
import { nodeDefinitions } from "graphql-relay";
import type { Context, Node } from "../types";
import type { Node } from "../types";
import { globalNodeResolver } from "./resolvers/query/global-node";
import type { Neo4jGraphQLComposedContext } from "./resolvers/wrapper";

// returns true if globalNodeFields added or false if not
export function addGlobalNodeFields(nodes: Node[], composer: SchemaComposer): boolean {
const globalNodes = nodes.filter((n) => n.isGlobalNode);

if (globalNodes.length === 0) return false;

const fetchById = (id: string, context: Context, info: GraphQLResolveInfo) => {
const fetchById = (id: string, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) => {
const resolver = globalNodeResolver({ nodes: globalNodes });
return resolver.resolve(null, { id }, context, info);
};
Expand All @@ -40,7 +41,11 @@ export function addGlobalNodeFields(nodes: Node[], composer: SchemaComposer): bo

composer.createInterfaceTC(nodeInterface);
composer.Query.addFields({
node: nodeField as ObjectTypeComposerFieldConfigAsObjectDefinition<null, Context, { id: string }>,
node: nodeField as ObjectTypeComposerFieldConfigAsObjectDefinition<
null,
Neo4jGraphQLComposedContext,
{ id: string }
>,
});
return true;
}
4 changes: 2 additions & 2 deletions packages/graphql/src/schema/parse/parse-fulltext-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import type { ArgumentNode, DirectiveNode, ObjectTypeDefinitionNode } from "graphql";
import type { FullText, FulltextIndex } from "../../types";
import type { FullText, FulltextContext } from "../../types";
import type { ObjectFields } from "../get-obj-field-meta";
import { parseValueNode } from "../../schema-model/parser/parse-value-node";

Expand All @@ -38,7 +38,7 @@ function parseFulltextDirective({
definition: ObjectTypeDefinitionNode;
}): FullText {
const indexesArg = directive.arguments?.find((arg) => arg.name.value === "indexes") as ArgumentNode;
const value = parseValueNode(indexesArg.value) as FulltextIndex[];
const value = parseValueNode(indexesArg.value) as FulltextContext[];
const compatibleFields = nodeFields.primitiveFields.filter(
(f) => ["String", "ID"].includes(f.typeMeta.name) && !f.typeMeta.array
);
Expand Down
16 changes: 11 additions & 5 deletions packages/graphql/src/schema/resolvers/field/cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@

import type { GraphQLResolveInfo } from "graphql";
import { execute } from "../../../utils";
import type { Context, CypherField } from "../../../types";
import type { CypherField } from "../../../types";
import { graphqlArgsToCompose } from "../../to-compose";
import { isNeoInt } from "../../../utils/utils";
import { translateTopLevelCypher } from "../../../translate";
import type { Neo4jGraphQLComposedContext } from "../wrapper";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";
import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";

export function cypherResolver({
field,
Expand All @@ -33,22 +36,25 @@ export function cypherResolver({
statement: string;
type: "Query" | "Mutation";
}) {
async function resolve(_root: any, args: any, _context: unknown, info: GraphQLResolveInfo) {
const context = _context as Context;
async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) {
const resolveTree = getNeo4jResolveTree(info);

(context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree;

const { cypher, params } = translateTopLevelCypher({
context,
info,
context: context as Neo4jGraphQLTranslationContext,
field,
args,
type,
statement,
});

const executeResult = await execute({
cypher,
params,
defaultAccessMode: "WRITE",
context,
info,
});

const values = executeResult.result.records.map((record) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/graphql/src/schema/resolvers/field/defaultField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
*/

import type { FieldNode, GraphQLResolveInfo } from "graphql";
import type { Neo4jGraphQLContext } from "../../../types/neo4j-graphql-context";

/**
* Based on the default field resolver used by graphql-js that accounts for aliased fields
* @link https://github.com/graphql/graphql-js/blob/main/src/execution/execute.ts#L999-L1015
*/

export function defaultFieldResolver(source: any, args: any, context: unknown, info: GraphQLResolveInfo) {
export function defaultFieldResolver(source, args, context: Neo4jGraphQLContext, info: GraphQLResolveInfo) {
if ((typeof source === "object" && source !== null) || typeof source === "function") {
const fieldNode = info.fieldNodes[0] as FieldNode;

Expand Down
4 changes: 2 additions & 2 deletions packages/graphql/src/schema/resolvers/field/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import type { GraphQLResolveInfo } from "graphql";
import { defaultFieldResolver } from "./defaultField";
import { isNeoInt } from "../../../utils/utils";
import type { Context } from "../../../types";
import type { Neo4jGraphQLContext } from "../../../types/neo4j-graphql-context";

function serializeValue(value) {
if (isNeoInt(value)) {
Expand All @@ -34,7 +34,7 @@ function serializeValue(value) {
return value;
}

export function idResolver(source, args, context: Context, info: GraphQLResolveInfo) {
export function idResolver(source, args, context: Neo4jGraphQLContext, info: GraphQLResolveInfo) {
const value = defaultFieldResolver(source, args, context, info);

if (Array.isArray(value)) {
Expand Down
16 changes: 10 additions & 6 deletions packages/graphql/src/schema/resolvers/mutation/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ import { Kind, type FieldNode, type GraphQLResolveInfo } from "graphql";
import { execute } from "../../../utils";
import { translateCreate } from "../../../translate";
import type { Node } from "../../../classes";
import type { Context } from "../../../types";
import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";
import { publishEventsToSubscriptionMechanism } from "../../subscriptions/publish-events-to-subscription-mechanism";
import type { Neo4jGraphQLComposedContext } from "../wrapper";
import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";
import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context";

export function createResolver({ node }: { node: Node }) {
async function resolve(_root: any, args: any, _context: unknown, info: GraphQLResolveInfo) {
const context = _context as Context;
context.resolveTree = getNeo4jResolveTree(info, { args });
const { cypher, params } = await translateCreate({ context, node });
async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) {
const resolveTree = getNeo4jResolveTree(info, { args });

(context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree;

const { cypher, params } = await translateCreate({ context: context as Neo4jGraphQLTranslationContext, node });

const executeResult = await execute({
cypher,
params,
defaultAccessMode: "WRITE",
context,
info,
});

const nodeProjection = info.fieldNodes[0]?.selectionSet?.selections.find(
Expand Down
Loading

0 comments on commit 28fd15e

Please sign in to comment.