Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add Required Fields For Custom Resolver #842

Merged
merged 28 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5b8eab6
add: requiredFields to ignoredFields
dmoree Jan 17, 2022
4f0129e
add: validation of requiredField as scalar type
dmoree Jan 17, 2022
552c6a9
add: required fields to projection
dmoree Jan 18, 2022
115611b
add: integration tests
dmoree Jan 18, 2022
e8fdb61
add: tck tests for `@ignore`
dmoree Jan 21, 2022
2ae885b
Merge branch 'dev' into feature/ignore-directive-required-fields
dmoree Jan 23, 2022
25756ba
fix: account for aliased fields
dmoree Jan 23, 2022
3b16df0
add: isScalar isEnum to cypher field
dmoree Jan 23, 2022
510727a
add: cypher fields to allowable required fields
dmoree Jan 23, 2022
cbfcd58
add: `@cypher` field integration test
dmoree Jan 24, 2022
ae3bfea
add: documentation of `require` argument.
dmoree Jan 24, 2022
ffa0c38
fix: random failure
dmoree Jan 24, 2022
28500d9
Merge branch 'dev' into feature/ignore-directive-required-fields
dmoree Jan 25, 2022
1a8c772
refactor: to use cypher field `isScalar` `isEnum`
dmoree Jan 25, 2022
e1a76ba
Merge branch 'dev' into feature/ignore-directive-required-fields
dmoree Feb 1, 2022
3c00576
add: utility fnc to remove duplicates from array
dmoree Feb 1, 2022
ce1a9d7
add: get meta from `@ignore` field.
dmoree Feb 1, 2022
a9f37e2
refactor: utilitize resolve tree helper functions
dmoree Feb 1, 2022
15a37bb
Merge branch 'dev' into feature/ignore-directive-required-fields
dmoree Feb 3, 2022
339cbc2
refactor: reducer for projection
dmoree Feb 3, 2022
59e8d9b
refactor: utils
dmoree Feb 3, 2022
3b465a2
add: unit tests of resolve tree utils
dmoree Feb 3, 2022
e23c507
Merge branch 'dev' into feature/ignore-directive-required-fields
angrykoala Feb 4, 2022
54155f0
Merge branch 'dev' into feature/ignore-directive-required-fields
dmoree Feb 7, 2022
407525e
Merge branch 'feature/ignore-directive-required-fields' of https://gi…
dmoree Feb 7, 2022
2b4091a
refactor: ignore argument `require` to `dependsOn`
dmoree Feb 7, 2022
87eba1f
Merge branch 'dev' into feature/ignore-directive-required-fields
angrykoala Feb 8, 2022
17f0954
Update packages/graphql/src/schema/get-ignore-meta.ts
dmoree Feb 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/modules/ROOT/pages/custom-resolvers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@

The library will autogenerate Query and Mutation resolvers, so you don’t need to implement those resolvers yourself. However, if you would like additional behaviours besides the autogenerated CRUD operations, you can specify custom resolvers for these scenarios.

*A note on custom resolvers*

> Due to the nature of the Cypher generation in this library, you must query any fields used in a custom resolver. For example, in the first example below calculating `fullName`, `firstName` and `lastName` must be included in the selection set when querying `fullName`. Without this being the case, `firstName` and `lastName` will be undefined in the custom resolver.

== Custom object type field resolver

If you would like to add a field to an object type which is resolved from existing values in the type, rather than storing new values, you should mark it with an xref::type-definitions/access-control.adoc#type-definitions-access-control-ignore[`@ignore`] directive and define a custom resolver for it.
If you would like to add a field to an object type which is resolved from existing values in the type, rather than storing new values, you should mark it with an xref::type-definitions/access-control.adoc#type-definitions-access-control-ignore[`@ignore`] directive and define a custom resolver for it. Any fields that the source object in the custom resolver depends on must be included in the array passed to the `dependsOn` argument. This is to ensure that during the Cypher generation process the required fields will be fetched from the database. Take for instance a simple schema:

[source, javascript, indent=0]
----
const typeDefs = `
type User {
firstName: String!
lastName: String!
fullName: String! @ignore
fullName: String! @ignore(dependsOn: ["firstName", "lastName"])
}
`;

Expand All @@ -35,6 +31,10 @@ const neoSchema = new Neo4jGraphQL({
});
----

Here `fullName` is a computed value from the fields `firstName` and `lastName`. Specifying the `@ignore` directive on the field definition keeps `fullName` from being included in any `Query` or `Mutation` fields and hence as a property on the `:User` node in the database.

The inclusion of the fields `firstName` and `lastName` in the `dependsOn` argument means that in the definition of the resolver the properties `firstName` and `lastName` will always be defined on the `source` object. If these fields are not specified, this cannot be guaranteed.

== Custom Query/Mutation type field resolver

You can define additional custom Query and Mutation fields in your type definitions and provide custom resolvers for them. A prime use case for this is using the xref::ogm/index.adoc[OGM] to manipulate types and fields which are not available through the API. You can find an example of it being used in this capacity in the xref::ogm/examples/custom-resolvers.adoc[Custom Resolvers] example.
6 changes: 5 additions & 1 deletion docs/modules/ROOT/pages/type-definitions/access-control.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,16 @@ type User @exclude(operations: [CREATE, READ, UPDATE, DELETE]) {

This field will essentially be completely ignored during the generation of Query and Mutation fields, and will require another way to resolve the field, such as through the use of a custom resolver.

Any fields that the custom resolver depends on should be passed to the `dependsOn` argument to ensure that during the Cypher generation process those properties are selected from the database. Allowable fields are any returning a Scalar or Enum type including those defined using the xref::type-definitions/cypher.adoc#type-definitions-cypher[`@cypher`] directive.

=== Definition

[source, graphql, indent=0]
----
"""Instructs @neo4j/graphql to completely ignore a field definition, assuming that it will be fully accounted for by custom resolvers."""
directive @ignore on FIELD_DEFINITION
directive @ignore(
dependsOn: [String!]
) on FIELD_DEFINITION
----

[[type-definitions-access-control-readonly]]
Expand Down
6 changes: 3 additions & 3 deletions packages/graphql/src/classes/GraphElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type {
CustomScalarField,
TemporalField,
PointField,
BaseField,
IgnoredField,
} from "../types";

export interface GraphElementConstructor {
Expand All @@ -36,7 +36,7 @@ export interface GraphElementConstructor {
enumFields: CustomEnumField[];
temporalFields: TemporalField[];
pointFields: PointField[];
ignoredFields: BaseField[];
ignoredFields: IgnoredField[];
}

export abstract class GraphElement {
Expand All @@ -47,7 +47,7 @@ export abstract class GraphElement {
public enumFields: CustomEnumField[];
public temporalFields: TemporalField[];
public pointFields: PointField[];
public ignoredFields: BaseField[];
public ignoredFields: IgnoredField[];

constructor(input: GraphElementConstructor) {
this.name = input.name;
Expand Down
21 changes: 3 additions & 18 deletions packages/graphql/src/classes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ import type {
TemporalField,
PointField,
Auth,
BaseField,
Context,
FullText,
IgnoredField,
} from "../types";
import Exclude from "./Exclude";
import { GraphElement, GraphElementConstructor } from "./GraphElement";
Expand All @@ -57,7 +57,7 @@ export interface NodeConstructor extends GraphElementConstructor {
objectFields: ObjectField[];
temporalFields: TemporalField[];
pointFields: PointField[];
ignoredFields: BaseField[];
ignoredFields: IgnoredField[];
auth?: Auth;
fulltextDirective?: FullText;
exclude?: Exclude;
Expand Down Expand Up @@ -160,22 +160,7 @@ class Node extends GraphElement {
...this.enumFields,
...this.temporalFields,
...this.pointFields,
...this.cypherFields.filter((field) =>
[
"Boolean",
"ID",
"Int",
"BigInt",
"Float",
"String",
"DateTime",
"LocalDateTime",
"Time",
"LocalTime",
"Date",
"Duration",
].includes(field.typeMeta.name)
),
...this.cypherFields.filter((field) => field.isScalar || field.isEnum),
dmoree marked this conversation as resolved.
Show resolved Hide resolved
].filter((field) => !field.typeMeta.array);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/graphql/src/classes/Relationship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import type {
CustomEnumField,
CypherField,
CustomScalarField,
BaseField,
TemporalField,
IgnoredField,
} from "../types";
import { GraphElement } from "./GraphElement";

Expand All @@ -39,7 +39,7 @@ export interface RelationshipConstructor {
enumFields?: CustomEnumField[];
temporalFields?: TemporalField[];
pointFields?: PointField[];
ignoredFields?: BaseField[];
ignoredFields?: IgnoredField[];
}

class Relationship extends GraphElement {
Expand Down
15 changes: 15 additions & 0 deletions packages/graphql/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export const RESERVED_INTERFACE_FIELDS = [
["cursor", "Interface field name 'cursor' reserved to support relay See https://relay.dev/graphql/"],
];

export const SCALAR_TYPES = [
"Boolean",
"ID",
"String",
"Int",
"BigInt",
"Float",
"DateTime",
"LocalDateTime",
"Time",
"LocalTime",
"Date",
"Duration",
];

export const WHERE_AGGREGATION_OPERATORS = ["EQUAL", "GT", "GTE", "LT", "LTE"];

// Types that you can average
Expand Down
209 changes: 209 additions & 0 deletions packages/graphql/src/schema/get-ignore-meta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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 { FieldDefinitionNode, Kind } from "graphql";
import getIgnoreMeta, { ERROR_MESSAGE } from "./get-ignore-meta";

describe("getIgnoreMeta", () => {
test("should return undefined if no directive found", () => {
// @ts-ignore
const field: FieldDefinitionNode = {
directives: [
{
// @ts-ignore
name: { value: "RANDOM 1" },
},
{
// @ts-ignore
name: { value: "RANDOM 2" },
},
{
// @ts-ignore
name: { value: "RANDOM 3" },
},
{
// @ts-ignore
name: { value: "RANDOM 4" },
},
],
};

const result = getIgnoreMeta(field);

expect(result).toBeUndefined();
});

test("should throw if dependsOn not a list", () => {
const field: FieldDefinitionNode = {
directives: [
{
// @ts-ignore
name: {
value: "ignore",
// @ts-ignore
},
arguments: [
{
// @ts-ignore
name: { value: "dependsOn" },
// @ts-ignore
value: { kind: Kind.BOOLEAN },
},
],
},
{
// @ts-ignore
name: { value: "RANDOM 2" },
},
{
// @ts-ignore
name: { value: "RANDOM 3" },
},
{
// @ts-ignore
name: { value: "RANDOM 4" },
},
],
};

expect(() => getIgnoreMeta(field)).toThrow(ERROR_MESSAGE);
});

test("should throw if dependsOn not a list of strings", () => {
const field: FieldDefinitionNode = {
directives: [
{
// @ts-ignore
name: {
value: "ignore",
// @ts-ignore
},
arguments: [
{
// @ts-ignore
name: { value: "dependsOn" },
// @ts-ignore
value: {
kind: Kind.LIST,
values: [
{ kind: Kind.STRING, value: "field1" },
{ kind: Kind.STRING, value: "field2" },
{ kind: Kind.BOOLEAN, value: true },
],
},
},
],
},
{
// @ts-ignore
name: { value: "RANDOM 2" },
},
{
// @ts-ignore
name: { value: "RANDOM 3" },
},
{
// @ts-ignore
name: { value: "RANDOM 4" },
},
],
};

expect(() => getIgnoreMeta(field)).toThrow(ERROR_MESSAGE);
});

test("should return the correct meta if no dependsOn argument", () => {
const field: FieldDefinitionNode = {
directives: [
{
// @ts-ignore
name: {
value: "ignore",
// @ts-ignore
},
},
{
// @ts-ignore
name: { value: "RANDOM 2" },
},
{
// @ts-ignore
name: { value: "RANDOM 3" },
},
{
// @ts-ignore
name: { value: "RANDOM 4" },
},
],
};

const result = getIgnoreMeta(field);

expect(result).toMatchObject({
requiredFields: [],
});
});

test("should return the correct meta with dependsOn argument", () => {
const requiredFields = ["field1", "field2", "field3"];
const field: FieldDefinitionNode = {
directives: [
{
// @ts-ignore
name: {
value: "ignore",
// @ts-ignore
},
arguments: [
{
// @ts-ignore
name: { value: "dependsOn" },
// @ts-ignore
value: {
kind: Kind.LIST,
values: requiredFields.map((requiredField) => ({
kind: Kind.STRING,
value: requiredField,
})),
},
},
],
},
{
// @ts-ignore
name: { value: "RANDOM 2" },
},
{
// @ts-ignore
name: { value: "RANDOM 3" },
},
{
// @ts-ignore
name: { value: "RANDOM 4" },
},
],
};

const result = getIgnoreMeta(field);

expect(result).toMatchObject({
requiredFields,
});
});
});
Loading