Skip to content

Commit b0fe269

Browse files
darrellwardegasposkarhanedanstarnsNeo Technology Build Agent
authored
Merge most recent changes from master into 2.0.0 (#306)
* Missing copyright headers * Point to prerelease documentation for 2.0.0 branch * General housekeeping before release * Update code comment * Fix TCK tests broken from merge * fix(jwt): req.cookies might be undefined this fix prevents the app from crashing id req.cookies is undefined * Add scalars earlier in schema augmentation for use in types and interfaces without throwing Error (fixes DateTime relationship properties) * Changes to accomodate merge from master * fix: use package json for useragent name and version (#271) * fix: use package json for useragent name and version * fix: add userAgent support for <=4.2 and >=4.3 drivers * config: remove codeowners (#277) * Version update * fix(login): avoid confusion caused by secondary button (#265) * fix: losing params while creating Auth Predicate (#281) * fix: loosing params while creating Auth Predicate * fix: typos * fix: typo * feat: add projection to top level cypher directive (#251) * feat: add projection to top level queries and mutations using cypher directive * fix: add missing cypherParams * Fix for loss of scalar and field level resolvers (#297) * wrapCustomResolvers removed in favour of schema level resolver auth injection * Add test cases for this fix * Mention double escaping for @cypher directive * Version update Co-authored-by: gaspard <[email protected]> Co-authored-by: Oskar Hane <[email protected]> Co-authored-by: Daniel Starns <[email protected]> Co-authored-by: Neo Technology Build Agent <[email protected]> Co-authored-by: Arnaud Gissinger <[email protected]>
1 parent fe77c58 commit b0fe269

29 files changed

+1172
-179
lines changed

.github/CODEOWNERS

Lines changed: 0 additions & 1 deletion
This file was deleted.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,5 @@ packages/package-tests/**/package-lock.json
8989
!.yarn/sdks
9090
!.yarn/versions
9191
.pnp.*
92+
93+
tsconfig.tsbuildinfo

docs/asciidoc/type-definitions/cypher.adoc

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,33 @@ directive @cypher(
1414
) on FIELD_DEFINITION
1515
----
1616

17+
== Character Escaping
18+
19+
All double quotes must be _double escaped_ when used in a @cypher directive - once for GraphQL and once for the function in which we run the Cypher. For example, at its simplest:
20+
21+
[source, graphql]
22+
----
23+
type Example {
24+
string: String!
25+
@cypher(
26+
statement: """
27+
RETURN \\"field-level string\\"
28+
"""
29+
)
30+
}
31+
32+
type Query {
33+
string: String!
34+
@cypher(
35+
statement: """
36+
RETURN \\"Query-level string\\"
37+
"""
38+
)
39+
}
40+
----
41+
42+
Note the double-backslash (`\\`) before each double quote (`"`).
43+
1744
== Globals
1845

1946
Global variables are available for use within the Cypher statement.
@@ -59,12 +86,12 @@ type Query {
5986
=== `cypherParams`
6087
Use to inject values into the cypher query from the GraphQL context function.
6188

62-
Inject into context:
89+
Inject into context:
6390

6491
[source, typescript]
6592
----
6693
const server = new ApolloServer({
67-
typeDefs,
94+
typeDefs,
6895
context: () => {
6996
return {
7097
cypherParams: { userId: "user-id-01" }
@@ -73,7 +100,7 @@ const server = new ApolloServer({
73100
});
74101
----
75102

76-
Use in cypher query:
103+
Use in cypher query:
77104

78105
[source, graphql]
79106
----

examples/neo-push/client/src/components/SignIn.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,6 @@ function SignIn() {
7474
onChange={(e) => setPassword(e.target.value)}
7575
/>
7676
</Form.Group>
77-
<Button block variant="outline-secondary" onClick={() => history.push(constants.SIGN_UP_PAGE)}>
78-
Sign Up
79-
</Button>
8077
<Button className="mt-3" variant="primary" type="submit">
8178
Sign In
8279
</Button>
@@ -90,6 +87,13 @@ function SignIn() {
9087
{error}
9188
</Alert>
9289
)}
90+
<hr />
91+
<p>
92+
Go to{" "}
93+
<Alert.Link onClick={() => history.push(constants.SIGN_UP_PAGE)}>
94+
Sign Up
95+
</Alert.Link> instead
96+
</p>
9397
</Card>
9498
</Form>
9599
</Row>

examples/neo-push/client/src/components/SignUp.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,6 @@ function SignUp() {
8787
onChange={(e) => setPasswordConfirm(e.target.value)}
8888
/>
8989
</Form.Group>
90-
<Button block variant="outline-secondary" onClick={() => history.push(constants.SIGN_IN_PAGE)}>
91-
Sign In
92-
</Button>
9390
<Button className="mt-3" variant="primary" type="submit">
9491
Sign Up
9592
</Button>
@@ -103,6 +100,13 @@ function SignUp() {
103100
{error}
104101
</Alert>
105102
)}
103+
<hr />
104+
<p>
105+
Go to{" "}
106+
<Alert.Link onClick={() => history.push(constants.SIGN_IN_PAGE)}>
107+
Sign In
108+
</Alert.Link> instead
109+
</p>
106110
</Card>
107111
</Form>
108112
</Row>

packages/graphql/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"rimraf": "3.0.2",
5858
"semver": "7.3.5",
5959
"ts-jest": "26.1.4",
60+
"ts-node": "^10.0.0",
6061
"typescript": "3.9.7"
6162
},
6263
"dependencies": {

packages/graphql/src/auth/get-jwt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function getJWT(context: Context): any {
4949
return result;
5050
}
5151

52-
const authorization = (req.headers.authorization || req.headers.Authorization || req.cookies.token) as string;
52+
const authorization = (req.headers.authorization || req.headers.Authorization || req.cookies?.token) as string;
5353
if (!authorization) {
5454
debug("Could not get .authorization, .Authorization or .cookies.token from req");
5555

packages/graphql/src/classes/Neo4jGraphQL.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import Debug from "debug";
2121
import { Driver } from "neo4j-driver";
2222
import { DocumentNode, GraphQLResolveInfo, GraphQLSchema, parse, printSchema, print } from "graphql";
23-
import { addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema";
23+
import { addResolversToSchema, addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema";
2424
import type { DriverConfig } from "../types";
2525
import { makeAugmentedSchema } from "../schema";
2626
import Node from "./Node";
@@ -29,6 +29,7 @@ import { checkNeo4jCompat } from "../utils";
2929
import { getJWT } from "../auth/index";
3030
import { DEBUG_GRAPHQL } from "../constants";
3131
import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree";
32+
import createAuthParam from "../translate/create-auth-param";
3233

3334
const debug = Debug(DEBUG_GRAPHQL);
3435

@@ -63,7 +64,7 @@ class Neo4jGraphQL {
6364
public config?: Neo4jGraphQLConfig;
6465

6566
constructor(input: Neo4jGraphQLConstructor) {
66-
const { config = {}, driver, ...schemaDefinition } = input;
67+
const { config = {}, driver, resolvers, ...schemaDefinition } = input;
6768
const { nodes, relationships, schema } = makeAugmentedSchema(schemaDefinition, {
6869
enableRegex: config.enableRegex,
6970
});
@@ -72,7 +73,20 @@ class Neo4jGraphQL {
7273
this.config = config;
7374
this.nodes = nodes;
7475
this.relationships = relationships;
75-
this.schema = this.createWrappedSchema({ schema, config });
76+
this.schema = schema;
77+
/*
78+
addResolversToSchema must be first, so that custom resolvers also get schema level resolvers
79+
*/
80+
if (resolvers) {
81+
if (Array.isArray(resolvers)) {
82+
resolvers.forEach((r) => {
83+
this.schema = addResolversToSchema(this.schema, r);
84+
});
85+
} else {
86+
this.schema = addResolversToSchema(this.schema, resolvers);
87+
}
88+
}
89+
this.schema = this.createWrappedSchema({ schema: this.schema, config });
7690
this.document = parse(printSchema(schema));
7791
}
7892

@@ -124,6 +138,8 @@ class Neo4jGraphQL {
124138
context.resolveTree = getNeo4jResolveTree(resolveInfo);
125139

126140
context.jwt = getJWT(context);
141+
142+
context.auth = createAuthParam({ context });
127143
});
128144
}
129145

packages/graphql/src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ export const REQUIRED_APOC_FUNCTIONS = [
2828
"apoc.cypher.runFirstColumn",
2929
"apoc.coll.sortMulti",
3030
"apoc.date.convertFormat",
31+
"apoc.map.values",
3132
];
32-
export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when"];
33+
export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when", "apoc.cypher.doIt"];
3334
export const DEBUG_AUTH = `${DEBUG_PREFIX}:auth`;
3435
export const DEBUG_GRAPHQL = `${DEBUG_PREFIX}:graphql`;
3536
export const DEBUG_EXECUTE = `${DEBUG_PREFIX}:execute`;

packages/graphql/src/environment.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
* limitations under the License.
1818
*/
1919

20+
import * as pack from "../package.json";
21+
2022
const environment = {
21-
NPM_PACKAGE_VERSION: process.env.NPM_PACKAGE_VERSION as string,
22-
NPM_PACKAGE_NAME: process.env.NPM_PACKAGE_NAME as string,
23+
NPM_PACKAGE_VERSION: pack.version,
24+
NPM_PACKAGE_NAME: pack.name,
2325
};
2426

2527
export default environment;

packages/graphql/src/schema/make-augmented-schema.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import { findResolver, createResolver, deleteResolver, cypherResolver, updateRes
5050
import checkNodeImplementsInterfaces from "./check-node-implements-interfaces";
5151
import * as Scalars from "./scalars";
5252
import parseExcludeDirective from "./parse-exclude-directive";
53-
import wrapCustomResolvers from "./wrap-custom-resolvers";
5453
import getCustomResolvers from "./get-custom-resolvers";
5554
import getObjFieldMeta from "./get-obj-field-meta";
5655
import * as point from "./point";
@@ -63,7 +62,7 @@ import { createConnectionWithEdgeProperties } from "./pagination";
6362
// import validateTypeDefs from "./validation";
6463

6564
function makeAugmentedSchema(
66-
{ typeDefs, resolvers, ...schemaDefinition }: IExecutableSchemaDefinition,
65+
{ typeDefs, ...schemaDefinition }: IExecutableSchemaDefinition,
6766
{ enableRegex }: { enableRegex?: boolean } = {}
6867
): { schema: GraphQLSchema; nodes: Node[]; relationships: Relationship[] } {
6968
const document = mergeTypeDefs(Array.isArray(typeDefs) ? (typeDefs as string[]) : [typeDefs as string]);
@@ -249,8 +248,6 @@ function makeAugmentedSchema(
249248
const relationshipProperties = interfaces.filter((i) => relationshipPropertyInterfaceNames.has(i.name.value));
250249
interfaces = interfaces.filter((i) => !relationshipPropertyInterfaceNames.has(i.name.value));
251250

252-
const nodeNames = nodes.map((x) => x.name);
253-
254251
const relationshipFields = new Map<string, RelationshipField[]>();
255252

256253
relationshipProperties.forEach((relationship) => {
@@ -1108,6 +1105,7 @@ function makeAugmentedSchema(
11081105
const customResolver = cypherResolver({
11091106
field,
11101107
statement: field.statement,
1108+
type: type as "Query" | "Mutation",
11111109
});
11121110

11131111
const composedField = objectFieldsToComposeFields([field])[field.fieldName];
@@ -1138,7 +1136,7 @@ function makeAugmentedSchema(
11381136
composer.delete("Mutation");
11391137
}
11401138
const generatedTypeDefs = composer.toSDL();
1141-
let generatedResolvers: any = {
1139+
const generatedResolvers = {
11421140
...composer.getResolveMethods(),
11431141
...Object.entries(Scalars).reduce((res, [name, scalar]) => {
11441142
if (generatedTypeDefs.includes(`scalar ${name}\n`)) {
@@ -1148,14 +1146,6 @@ function makeAugmentedSchema(
11481146
}, {}),
11491147
};
11501148

1151-
if (resolvers) {
1152-
generatedResolvers = wrapCustomResolvers({
1153-
generatedResolvers,
1154-
nodeNames,
1155-
resolvers,
1156-
});
1157-
}
1158-
11591149
unions.forEach((union) => {
11601150
if (!generatedResolvers[union.name.value]) {
11611151
// eslint-disable-next-line no-underscore-dangle

packages/graphql/src/schema/resolvers/cypher.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe("Cypher resolver", () => {
2929
arguments: [],
3030
};
3131

32-
const result = cypherResolver({ field, statement: "" });
32+
const result = cypherResolver({ field, statement: "", type: "Query" });
3333
expect(result.type).toEqual(field.typeMeta.pretty);
3434
expect(result.resolve).toBeInstanceOf(Function);
3535
expect(result.args).toMatchObject({});

packages/graphql/src/schema/resolvers/cypher.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,87 @@ import { graphqlArgsToCompose } from "../to-compose";
2424
import createAuthAndParams from "../../translate/create-auth-and-params";
2525
import createAuthParam from "../../translate/create-auth-param";
2626
import { AUTH_FORBIDDEN_ERROR } from "../../constants";
27+
import createProjectionAndParams from "../../translate/create-projection-and-params";
2728

28-
export default function cypherResolver({ field, statement }: { field: BaseField; statement: string }) {
29+
export default function cypherResolver({
30+
field,
31+
statement,
32+
type,
33+
}: {
34+
field: BaseField;
35+
statement: string;
36+
type: "Query" | "Mutation";
37+
}) {
2938
async function resolve(_root: any, args: any, _context: unknown) {
3039
const context = _context as Context;
40+
const {
41+
resolveTree: { fieldsByTypeName },
42+
} = context;
3143
const cypherStrs: string[] = [];
3244
let params = { ...args, auth: createAuthParam({ context }), cypherParams: context.cypherParams };
45+
let projectionStr = "";
46+
let projectionAuthStr = "";
47+
const isPrimitive = ["ID", "String", "Boolean", "Float", "Int", "DateTime", "BigInt"].includes(
48+
field.typeMeta.name
49+
);
3350

3451
const preAuth = createAuthAndParams({ entity: field, context });
3552
if (preAuth[0]) {
3653
params = { ...params, ...preAuth[1] };
3754
cypherStrs.push(`CALL apoc.util.validate(NOT(${preAuth[0]}), "${AUTH_FORBIDDEN_ERROR}", [0])`);
3855
}
3956

40-
cypherStrs.push(statement);
57+
const referenceNode = context.neoSchema.nodes.find((x) => x.name === field.typeMeta.name);
58+
if (referenceNode) {
59+
const recurse = createProjectionAndParams({
60+
fieldsByTypeName,
61+
node: referenceNode,
62+
context,
63+
varName: `this`,
64+
});
65+
[projectionStr] = recurse;
66+
params = { ...params, ...recurse[1] };
67+
if (recurse[2]?.authValidateStrs?.length) {
68+
projectionAuthStr = recurse[2].authValidateStrs.join(" AND ");
69+
}
70+
}
71+
72+
const initApocParamsStrs = ["auth: $auth", ...(context.cypherParams ? ["cypherParams: $cypherParams"] : [])];
73+
const apocParams = Object.entries(args).reduce(
74+
(r: { strs: string[]; params: any }, entry) => {
75+
return {
76+
strs: [...r.strs, `${entry[0]}: $${entry[0]}`],
77+
params: { ...r.params, [entry[0]]: entry[1] },
78+
};
79+
},
80+
{ strs: initApocParamsStrs, params }
81+
) as { strs: string[]; params: any };
82+
const apocParamsStr = `{${apocParams.strs.length ? `${apocParams.strs.join(", ")}` : ""}}`;
83+
84+
const expectMultipleValues = referenceNode && field.typeMeta.array ? "true" : "false";
85+
if (type === "Query") {
86+
cypherStrs.push(`
87+
WITH apoc.cypher.runFirstColumn("${statement}", ${apocParamsStr}, ${expectMultipleValues}) as x
88+
UNWIND x as this
89+
`);
90+
} else {
91+
cypherStrs.push(`
92+
CALL apoc.cypher.doIt("${statement}", ${apocParamsStr}) YIELD value
93+
WITH apoc.map.values(value, [keys(value)[0]])[0] AS this
94+
`);
95+
}
96+
97+
if (projectionAuthStr) {
98+
cypherStrs.push(
99+
`WHERE apoc.util.validatePredicate(NOT(${projectionAuthStr}), "${AUTH_FORBIDDEN_ERROR}", [0])`
100+
);
101+
}
102+
103+
if (!isPrimitive) {
104+
cypherStrs.push(`RETURN this ${projectionStr} AS this`);
105+
} else {
106+
cypherStrs.push(`RETURN this`);
107+
}
41108

42109
const result = await execute({
43110
cypher: cypherStrs.join("\n"),

0 commit comments

Comments
 (0)