Skip to content

Commit

Permalink
Merge most recent changes from master into 2.0.0 (#306)
Browse files Browse the repository at this point in the history
* 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 <gaspard@gmail.com>
Co-authored-by: Oskar Hane <oh@oskarhane.com>
Co-authored-by: Daniel Starns <danielstarns@hotmail.com>
Co-authored-by: Neo Technology Build Agent <continuous-integration+build-agent@neotechnology.com>
Co-authored-by: Arnaud Gissinger <37625778+mathix420@users.noreply.github.com>
  • Loading branch information
6 people authored Jul 8, 2021
1 parent fe77c58 commit b0fe269
Show file tree
Hide file tree
Showing 29 changed files with 1,172 additions and 179 deletions.
1 change: 0 additions & 1 deletion .github/CODEOWNERS

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ packages/package-tests/**/package-lock.json
!.yarn/sdks
!.yarn/versions
.pnp.*

tsconfig.tsbuildinfo
33 changes: 30 additions & 3 deletions docs/asciidoc/type-definitions/cypher.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ directive @cypher(
) on FIELD_DEFINITION
----

== Character Escaping

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:

[source, graphql]
----
type Example {
string: String!
@cypher(
statement: """
RETURN \\"field-level string\\"
"""
)
}
type Query {
string: String!
@cypher(
statement: """
RETURN \\"Query-level string\\"
"""
)
}
----

Note the double-backslash (`\\`) before each double quote (`"`).

== Globals

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

Inject into context:
Inject into context:

[source, typescript]
----
const server = new ApolloServer({
typeDefs,
typeDefs,
context: () => {
return {
cypherParams: { userId: "user-id-01" }
Expand All @@ -73,7 +100,7 @@ const server = new ApolloServer({
});
----

Use in cypher query:
Use in cypher query:

[source, graphql]
----
Expand Down
10 changes: 7 additions & 3 deletions examples/neo-push/client/src/components/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,6 @@ function SignIn() {
onChange={(e) => setPassword(e.target.value)}
/>
</Form.Group>
<Button block variant="outline-secondary" onClick={() => history.push(constants.SIGN_UP_PAGE)}>
Sign Up
</Button>
<Button className="mt-3" variant="primary" type="submit">
Sign In
</Button>
Expand All @@ -90,6 +87,13 @@ function SignIn() {
{error}
</Alert>
)}
<hr />
<p>
Go to{" "}
<Alert.Link onClick={() => history.push(constants.SIGN_UP_PAGE)}>
Sign Up
</Alert.Link> instead
</p>
</Card>
</Form>
</Row>
Expand Down
10 changes: 7 additions & 3 deletions examples/neo-push/client/src/components/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ function SignUp() {
onChange={(e) => setPasswordConfirm(e.target.value)}
/>
</Form.Group>
<Button block variant="outline-secondary" onClick={() => history.push(constants.SIGN_IN_PAGE)}>
Sign In
</Button>
<Button className="mt-3" variant="primary" type="submit">
Sign Up
</Button>
Expand All @@ -103,6 +100,13 @@ function SignUp() {
{error}
</Alert>
)}
<hr />
<p>
Go to{" "}
<Alert.Link onClick={() => history.push(constants.SIGN_IN_PAGE)}>
Sign In
</Alert.Link> instead
</p>
</Card>
</Form>
</Row>
Expand Down
1 change: 1 addition & 0 deletions packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"rimraf": "3.0.2",
"semver": "7.3.5",
"ts-jest": "26.1.4",
"ts-node": "^10.0.0",
"typescript": "3.9.7"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/auth/get-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function getJWT(context: Context): any {
return result;
}

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

Expand Down
22 changes: 19 additions & 3 deletions packages/graphql/src/classes/Neo4jGraphQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import Debug from "debug";
import { Driver } from "neo4j-driver";
import { DocumentNode, GraphQLResolveInfo, GraphQLSchema, parse, printSchema, print } from "graphql";
import { addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema";
import { addResolversToSchema, addSchemaLevelResolver, IExecutableSchemaDefinition } from "@graphql-tools/schema";
import type { DriverConfig } from "../types";
import { makeAugmentedSchema } from "../schema";
import Node from "./Node";
Expand All @@ -29,6 +29,7 @@ import { checkNeo4jCompat } from "../utils";
import { getJWT } from "../auth/index";
import { DEBUG_GRAPHQL } from "../constants";
import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree";
import createAuthParam from "../translate/create-auth-param";

const debug = Debug(DEBUG_GRAPHQL);

Expand Down Expand Up @@ -63,7 +64,7 @@ class Neo4jGraphQL {
public config?: Neo4jGraphQLConfig;

constructor(input: Neo4jGraphQLConstructor) {
const { config = {}, driver, ...schemaDefinition } = input;
const { config = {}, driver, resolvers, ...schemaDefinition } = input;
const { nodes, relationships, schema } = makeAugmentedSchema(schemaDefinition, {
enableRegex: config.enableRegex,
});
Expand All @@ -72,7 +73,20 @@ class Neo4jGraphQL {
this.config = config;
this.nodes = nodes;
this.relationships = relationships;
this.schema = this.createWrappedSchema({ schema, config });
this.schema = schema;
/*
addResolversToSchema must be first, so that custom resolvers also get schema level resolvers
*/
if (resolvers) {
if (Array.isArray(resolvers)) {
resolvers.forEach((r) => {
this.schema = addResolversToSchema(this.schema, r);
});
} else {
this.schema = addResolversToSchema(this.schema, resolvers);
}
}
this.schema = this.createWrappedSchema({ schema: this.schema, config });
this.document = parse(printSchema(schema));
}

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

context.jwt = getJWT(context);

context.auth = createAuthParam({ context });
});
}

Expand Down
3 changes: 2 additions & 1 deletion packages/graphql/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ export const REQUIRED_APOC_FUNCTIONS = [
"apoc.cypher.runFirstColumn",
"apoc.coll.sortMulti",
"apoc.date.convertFormat",
"apoc.map.values",
];
export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when"];
export const REQUIRED_APOC_PROCEDURES = ["apoc.util.validate", "apoc.do.when", "apoc.cypher.doIt"];
export const DEBUG_AUTH = `${DEBUG_PREFIX}:auth`;
export const DEBUG_GRAPHQL = `${DEBUG_PREFIX}:graphql`;
export const DEBUG_EXECUTE = `${DEBUG_PREFIX}:execute`;
6 changes: 4 additions & 2 deletions packages/graphql/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
* limitations under the License.
*/

import * as pack from "../package.json";

const environment = {
NPM_PACKAGE_VERSION: process.env.NPM_PACKAGE_VERSION as string,
NPM_PACKAGE_NAME: process.env.NPM_PACKAGE_NAME as string,
NPM_PACKAGE_VERSION: pack.version,
NPM_PACKAGE_NAME: pack.name,
};

export default environment;
16 changes: 3 additions & 13 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import { findResolver, createResolver, deleteResolver, cypherResolver, updateRes
import checkNodeImplementsInterfaces from "./check-node-implements-interfaces";
import * as Scalars from "./scalars";
import parseExcludeDirective from "./parse-exclude-directive";
import wrapCustomResolvers from "./wrap-custom-resolvers";
import getCustomResolvers from "./get-custom-resolvers";
import getObjFieldMeta from "./get-obj-field-meta";
import * as point from "./point";
Expand All @@ -63,7 +62,7 @@ import { createConnectionWithEdgeProperties } from "./pagination";
// import validateTypeDefs from "./validation";

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

const nodeNames = nodes.map((x) => x.name);

const relationshipFields = new Map<string, RelationshipField[]>();

relationshipProperties.forEach((relationship) => {
Expand Down Expand Up @@ -1108,6 +1105,7 @@ function makeAugmentedSchema(
const customResolver = cypherResolver({
field,
statement: field.statement,
type: type as "Query" | "Mutation",
});

const composedField = objectFieldsToComposeFields([field])[field.fieldName];
Expand Down Expand Up @@ -1138,7 +1136,7 @@ function makeAugmentedSchema(
composer.delete("Mutation");
}
const generatedTypeDefs = composer.toSDL();
let generatedResolvers: any = {
const generatedResolvers = {
...composer.getResolveMethods(),
...Object.entries(Scalars).reduce((res, [name, scalar]) => {
if (generatedTypeDefs.includes(`scalar ${name}\n`)) {
Expand All @@ -1148,14 +1146,6 @@ function makeAugmentedSchema(
}, {}),
};

if (resolvers) {
generatedResolvers = wrapCustomResolvers({
generatedResolvers,
nodeNames,
resolvers,
});
}

unions.forEach((union) => {
if (!generatedResolvers[union.name.value]) {
// eslint-disable-next-line no-underscore-dangle
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/schema/resolvers/cypher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("Cypher resolver", () => {
arguments: [],
};

const result = cypherResolver({ field, statement: "" });
const result = cypherResolver({ field, statement: "", type: "Query" });
expect(result.type).toEqual(field.typeMeta.pretty);
expect(result.resolve).toBeInstanceOf(Function);
expect(result.args).toMatchObject({});
Expand Down
71 changes: 69 additions & 2 deletions packages/graphql/src/schema/resolvers/cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,87 @@ import { graphqlArgsToCompose } from "../to-compose";
import createAuthAndParams from "../../translate/create-auth-and-params";
import createAuthParam from "../../translate/create-auth-param";
import { AUTH_FORBIDDEN_ERROR } from "../../constants";
import createProjectionAndParams from "../../translate/create-projection-and-params";

export default function cypherResolver({ field, statement }: { field: BaseField; statement: string }) {
export default function cypherResolver({
field,
statement,
type,
}: {
field: BaseField;
statement: string;
type: "Query" | "Mutation";
}) {
async function resolve(_root: any, args: any, _context: unknown) {
const context = _context as Context;
const {
resolveTree: { fieldsByTypeName },
} = context;
const cypherStrs: string[] = [];
let params = { ...args, auth: createAuthParam({ context }), cypherParams: context.cypherParams };
let projectionStr = "";
let projectionAuthStr = "";
const isPrimitive = ["ID", "String", "Boolean", "Float", "Int", "DateTime", "BigInt"].includes(
field.typeMeta.name
);

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

cypherStrs.push(statement);
const referenceNode = context.neoSchema.nodes.find((x) => x.name === field.typeMeta.name);
if (referenceNode) {
const recurse = createProjectionAndParams({
fieldsByTypeName,
node: referenceNode,
context,
varName: `this`,
});
[projectionStr] = recurse;
params = { ...params, ...recurse[1] };
if (recurse[2]?.authValidateStrs?.length) {
projectionAuthStr = recurse[2].authValidateStrs.join(" AND ");
}
}

const initApocParamsStrs = ["auth: $auth", ...(context.cypherParams ? ["cypherParams: $cypherParams"] : [])];
const apocParams = Object.entries(args).reduce(
(r: { strs: string[]; params: any }, entry) => {
return {
strs: [...r.strs, `${entry[0]}: $${entry[0]}`],
params: { ...r.params, [entry[0]]: entry[1] },
};
},
{ strs: initApocParamsStrs, params }
) as { strs: string[]; params: any };
const apocParamsStr = `{${apocParams.strs.length ? `${apocParams.strs.join(", ")}` : ""}}`;

const expectMultipleValues = referenceNode && field.typeMeta.array ? "true" : "false";
if (type === "Query") {
cypherStrs.push(`
WITH apoc.cypher.runFirstColumn("${statement}", ${apocParamsStr}, ${expectMultipleValues}) as x
UNWIND x as this
`);
} else {
cypherStrs.push(`
CALL apoc.cypher.doIt("${statement}", ${apocParamsStr}) YIELD value
WITH apoc.map.values(value, [keys(value)[0]])[0] AS this
`);
}

if (projectionAuthStr) {
cypherStrs.push(
`WHERE apoc.util.validatePredicate(NOT(${projectionAuthStr}), "${AUTH_FORBIDDEN_ERROR}", [0])`
);
}

if (!isPrimitive) {
cypherStrs.push(`RETURN this ${projectionStr} AS this`);
} else {
cypherStrs.push(`RETURN this`);
}

const result = await execute({
cypher: cypherStrs.join("\n"),
Expand Down
Loading

0 comments on commit b0fe269

Please sign in to comment.