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

Reserved properties #396

Merged
merged 4 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 27 additions & 17 deletions docs/asciidoc/ogm/selection-set.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ When using the OGM, you do not have to provide a selection set by default. Doing

[source, graphql]
----
type Node {
type Movie {
danstarns marked this conversation as resolved.
Show resolved Hide resolved
id: ID
name: String
relatedNodes: [Node] @relationship(type: "HAS_NODE", direction: OUT)
genres: [Genre] @relationship(type: "IN_GENRE", direction: OUT)
customCypher: String! @cypher(statement: "RETURN someCustomData")
}

type Genre {
name: String
}
----

Neither relationship fields nor custom Cypher fields are included in the generated selection set, as they could be computationally expensive. So, given the type definition above, the generated selection set would be:
Expand Down Expand Up @@ -71,33 +75,36 @@ const driver = neo4j.driver(
);

const typeDefs = `
type Node {
type Movie {
id: ID
name: String
relatedNodes: [Node] @relationship(type: "HAS_NODE", direction: OUT)
genres: [Genre] @relationship(type: "IN_GENRE", direction: OUT)
customCypher: String! @cypher(statement: "RETURN someCustomData")
}

type Genre {
name: String
}
`;

const ogm = new OGM({ typeDefs, driver });
const Node = ogm.model("Node");
const Movie = ogm.model("Movie");

const selectionSet = `
{
id
name
relatedNodes {
id
genres {
name
}
customCypher
}
`;

const nodes = await Node.find({ selectionSet });
const movies = await Movie.find({ selectionSet });
----

Note that the argument `selectionSet` is passed every invocation of the `Node.find()` function.
Note that the argument `selectionSet` is passed every invocation of the `Movie.find()` function.

== Selection set as a static

Expand All @@ -114,32 +121,35 @@ const driver = neo4j.driver(
);

const typeDefs = `
type Node {
type Movie {
id: ID
name: String
relatedNodes: [Node] @relationship(type: "HAS_NODE", direction: OUT)
genres: [Genre] @relationship(type: "IN_GENRE", direction: OUT)
customCypher: String! @cypher(statement: "RETURN someCustomData")
}

type Genre {
name: String
}
`;

const ogm = new OGM({ typeDefs, driver });
const Node = ogm.model("Node");
const Movie = ogm.model("Movie");

const selectionSet = `
{
id
name
relatedNodes {
id
genres {
name
}
customCypher
}
`;

Node.setSelectionSet(selectionSet)
Movie.setSelectionSet(selectionSet)

const nodes = await Node.find();
const movies = await Movie.find();
----

Note that despite not passing this selection set into `Node.find()`, the requested fields will be returned on each request.
Note that despite not passing this selection set into `Movie.find()`, the requested fields will be returned on each request.
19 changes: 19 additions & 0 deletions packages/graphql/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,22 @@ export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when", "
export const DEBUG_AUTH = `${DEBUG_PREFIX}:auth`;
export const DEBUG_GRAPHQL = `${DEBUG_PREFIX}:graphql`;
export const DEBUG_EXECUTE = `${DEBUG_PREFIX}:execute`;

// [0]Name [1]Error
export const RESERVED_TYPE_NAMES = [
[
"PageInfo",
"Type or Interface with name `PageInfo` reserved to support the pagination model of connections. See https://relay.dev/graphql/connections.htm#sec-Reserved-Types for more information.",
],
[
"Connection",
'Type or Interface with name ending "Connection" are reserved to support the pagination model of connections. See https://relay.dev/graphql/connections.htm#sec-Reserved-Types for more information.',
],
["Node", "Type or Interface with name 'Node' reserved to support relay See https://relay.dev/graphql/"],
];

// [0]Field [1]Error
export const RESERVED_INTERFACE_FIELDS = [
["node", "Interface field name 'node' reserved to support relay See https://relay.dev/graphql/"],
["cursor", "Interface field name 'cursor' reserved to support relay See https://relay.dev/graphql/"],
];
166 changes: 158 additions & 8 deletions packages/graphql/src/schema/make-augmented-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { pluralize } from "graphql-compose";
import makeAugmentedSchema from "./make-augmented-schema";
import { Node } from "../classes";
import * as constants from "../constants";

describe("makeAugmentedSchema", () => {
test("should be a function", () => {
Expand Down Expand Up @@ -136,8 +137,8 @@ describe("makeAugmentedSchema", () => {

test("should throw cannot have auth directive on a relationship", () => {
const typeDefs = `
type Node {
node: Node @relationship(type: "NODE", direction: OUT) @auth(rules: [{operations: [CREATE]}])
type Movie {
movie: Movie @relationship(type: "NODE", direction: OUT) @auth(rules: [{operations: [CREATE]}])
}
`;

Expand All @@ -147,7 +148,7 @@ describe("makeAugmentedSchema", () => {
describe("REGEX", () => {
test("should remove the MATCHES filter by default", () => {
const typeDefs = `
type Node {
type Movie {
name: String
}
`;
Expand All @@ -157,7 +158,7 @@ describe("makeAugmentedSchema", () => {
const document = parse(printSchema(neoSchema.schema));

const nodeWhereInput = document.definitions.find(
(x) => x.kind === "InputObjectTypeDefinition" && x.name.value === "NodeWhere"
(x) => x.kind === "InputObjectTypeDefinition" && x.name.value === "MovieWhere"
) as InputObjectTypeDefinitionNode;

const matchesField = nodeWhereInput.fields?.find((x) => x.name.value.endsWith("_MATCHES"));
Expand All @@ -167,7 +168,7 @@ describe("makeAugmentedSchema", () => {

test("should add the MATCHES filter when NEO4J_GRAPHQL_ENABLE_REGEX is set", () => {
const typeDefs = `
type Node {
type User {
name: String
}
`;
Expand All @@ -177,7 +178,7 @@ describe("makeAugmentedSchema", () => {
const document = parse(printSchema(neoSchema.schema));

const nodeWhereInput = document.definitions.find(
(x) => x.kind === "InputObjectTypeDefinition" && x.name.value === "NodeWhere"
(x) => x.kind === "InputObjectTypeDefinition" && x.name.value === "UserWhere"
) as InputObjectTypeDefinitionNode;

const matchesField = nodeWhereInput.fields?.find((x) => x.name.value.endsWith("_MATCHES"));
Expand All @@ -191,12 +192,12 @@ describe("makeAugmentedSchema", () => {
// https://github.com/neo4j/graphql/issues/158

const typeDefs = `
type Node {
type Movie {
createdAt: DateTime
}

type Query {
nodes: [Node] @cypher(statement: "")
movies: [Movie] @cypher(statement: "")
}
`;

Expand Down Expand Up @@ -305,4 +306,153 @@ describe("makeAugmentedSchema", () => {
"Cannot have @cypher directive on relationship property"
);
});

describe("Reserved Names", () => {
describe("Node", () => {
test("should throw when using PageInfo as node name", () => {
const typeDefs = `
type PageInfo {
id: ID
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "PageInfo") as string[])[1]
);
});

test("should throw when using Connection in a node name", () => {
const typeDefs = `
type NodeConnection {
id: ID
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "Connection") as string[])[1]
);
});

test("should throw when using Node as node name", () => {
const typeDefs = `
type Node {
id: ID
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "Node") as string[])[1]
);
});
});

describe("Interface", () => {
test("should throw when using PageInfo as relationship properties interface name", () => {
const typeDefs = `
type Movie {
id: ID
actors: [Actor] @relationship(type: "ACTED_IN", direction: OUT, properties: "PageInfo")
}

interface PageInfo {
screenTime: Int
}

type Actor {
name: String
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "PageInfo") as string[])[1]
);
});

test("should throw when using Connection in a properties interface name", () => {
const typeDefs = `
type Movie {
id: ID
actors: [Actor] @relationship(type: "ACTED_IN", direction: OUT, properties: "NodeConnection")
}

interface NodeConnection {
screenTime: Int
}

type Actor {
name: String
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "Connection") as string[])[1]
);
});

test("should throw when using Node as relationship properties interface name", () => {
const typeDefs = `
type Movie {
id: ID
actors: [Actor] @relationship(type: "ACTED_IN", direction: OUT, properties: "Node")
}

interface Node {
screenTime: Int
}

type Actor {
name: String
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_TYPE_NAMES.find((x) => x[0] === "Node") as string[])[1]
);
});

describe("Fields", () => {
test("should throw when using 'node' as a relationship property", () => {
const typeDefs = `
type Movie {
id: ID
actors: [Actor] @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

interface ActedIn {
node: ID
}

type Actor {
name: String
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_INTERFACE_FIELDS.find((x) => x[0] === "node") as string[])[1]
);
});

test("should throw when using 'cursor' as a relationship property", () => {
const typeDefs = `
type Movie {
id: ID
actors: [Actor] @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

interface ActedIn {
cursor: ID
}

type Actor {
name: String
}
`;

expect(() => makeAugmentedSchema({ typeDefs })).toThrow(
(constants.RESERVED_INTERFACE_FIELDS.find((x) => x[0] === "cursor") as string[])[1]
);
});
});
});
});
});
Loading