Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into Refactor-map
Browse files Browse the repository at this point in the history
  • Loading branch information
angrykoala committed Apr 5, 2023
2 parents 95c9cc3 + 283980e commit 8545cf6
Show file tree
Hide file tree
Showing 119 changed files with 14,773 additions and 2,204 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-impalas-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql-toolbox": minor
---

feat: Toolbox, Docs explorer GraphiQL v2 component
5 changes: 5 additions & 0 deletions .changeset/shaggy-dingos-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/cypher-builder": patch
---

Adds `divide`, `multiply`, `mod`, `pow` to the Math Operators.
5 changes: 5 additions & 0 deletions .changeset/tender-keys-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/cypher-builder": patch
---

Add inequality operator (<>) with Cypher.neq
5 changes: 5 additions & 0 deletions .changeset/wicked-experts-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/cypher-builder": minor
---

Map projections inject the leading dot (.) in the map fields automatically.
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@
"dotenv": "16.0.3",
"express": "4.18.2",
"hyperlink": "5.0.4",
"neo4j-driver": "5.6.0"
"neo4j-driver": "5.7.0"
}
}
262 changes: 262 additions & 0 deletions docs/rfcs/rfc-037-read-write-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# Read/write controls

## Problem

The current `@exclude` directive does not offer the level of granularity that our users desire. We have a number of GitHub issues proposing new features in this area:

* [#1672](https://github.com/neo4j/graphql/issues/1672) proposes the need to selectively disable aggregation and connection fields
* [#2804](https://github.com/neo4j/graphql/issues/2804) proposes the need to selectively disable filtering and sorting
* [#1034](https://github.com/neo4j/graphql/issues/1034) proposes being able to use `@exclude` on field definitions
* [#850](https://github.com/neo4j/graphql/issues/850) proposes being able to use `@exclude` on relationship fields

## Advanced use case summary

```gql
type User @write(operations: [CREATE, UPDATE]) {
name: String!
comments: [Comment!]! @relationship(type: "HAS_AUTHOR", direction: IN) @aggregations
posts: [Post!]! @relationship(type: "HAS_AUTHOR", direction: IN) @aggregations
}

type Post @write(operations: [CREATE, UPDATE, DELETE]) {
content: String!
comments: [Comment!]! @relationship(type: "HAS_COMMENT", direction: OUT) @write(operations: [CREATE])
author: User! @relationship(type: "HAS_AUTHOR", direction: OUT) @write(operations: [CREATE_RELATIONSHIP])
}

type Comment @write(operations: [UPDATE, DELETE]) {
content: String!
post: Post! @relationship(type: "HAS_COMMENT", direction: IN)
author: User! @relationship(type: "HAS_AUTHOR", direction: OUT) @write(operations: [CREATE_RELATIONSHIP])
}

extend schema @readonly
```

* `extend schema @readonly` makes the entire schema read-only
* The following Mutation Fields are added:
* `createUsers`, `updateUsers`
* `createPosts`, `updatePosts`, `deletePosts`
* `updateComments`, `deleteComments`
* All relationships _from_ `User` are read-only, but with added aggregation fields
* `Comment` nodes can _only_ be created when coming from a `Post`
* The `post` field from `Comment` is read-only
* The `author` field _must_ be connected to a `User` when coming from either a `Post` or `Comment`

This highlights some disadvantages:

* Despite `CREATE` and `UPDATE` being specified on `User`, we ended up overwriting them for all relationships pointing to `User`

## Solution

It is likely that a single directive for this functionality is no longer desired, given the ever-expanding capabilities of the library.

This RFC will discuss some options on how functionality might be split.

### Globals

There are currently `@readonly` and `@writeonly` directive which are only applicable to fields. These will be extended, with a toggle argument added for usage in overriding situations:

```gql
directive @readonly on SCHEMA | OBJECT | FIELD_DEFINITION

directive @writeonly(
enabled: Boolean! = true
) on SCHEMA | OBJECT | FIELD_DEFINITION
```

Note that the `@readonly` argument does not need a toggle argument because the `@write` directive can be used for this purpose. This reduces the number of sugar syntax directives.

Only one of the above can be used in any given location.

To disable all Mutations (and thus, all nested operations):

```gql
extend schema @readonly
```

To disable all Queries, except for the Relay `node` Query:

```gql
extend schema @writeonly
```

To disable all nested write operations across a particular relationship:

```gql
type Movie {
title: String!
}

type Actor {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) @readonly
}
```

To re-enable reads for the `Actor.movies` relationship in the following example where the `Movie` type has been set as write-only:

```gql
type Movie @writeonly {
title: String!
}

type Actor {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) @writeonly(enabled: false)
}
```

### Aggregations

At present, it is the intention to only introduce toggling for aggregation queries.

Due to future intentions of API design in the library, Connections will become the primary currency, and we want to normalize their usage as opposed to encourage them being disabled.

We will introduce a single directive to toggle aggregations:

```gql
directive @aggregations (
enabled: Boolean! = true
) on SCHEMA | OBJECT | FIELD_DEFINITION
```

At present, the proposal is that aggregations will be disabled by default.

To enable them for the entire schema, as is the current behaviour:

```gql
extend schema @aggregations
```

To enable them for a particular type:

```gql
type Movie @aggregations {
title: String!
}

type Actor {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT)
}
```

The above will enable:

* The `moviesAggregate` root-level Query field
* The nested `Actor.moviesAggregate` field

Finally, aggregations can be toggled for a relationship field:

```gql
type Movie {
title: String!
}

type Actor {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) @aggregations
}
```

The above will enable only the `Actor.moviesAggregate` field. The use of the `@aggregations` directive on a non-relationship field will throw an error.

Finally, the `@aggregations` directive can be used to override the toggle at a higher level:

```gql
type Movie @aggregations(enabled: false) {
title: String!
}

type Actor {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) @aggregations
}

extend type schema @aggregations
```

The above will:

* Enable all root-level and nested aggregation fields
* However, the `Query.moviesAggregate` and `Actor.moviesAggregate` fields will be disabled by the `@aggregations` directive on the `Movie` type
* However, the `Actor.moviesAggregate` field will once again be enabled by the `@aggregations` directive on `Actor.movies`

### Write operations

This option takes an unopinionated view of root-level versus nested operations, encompassing them all in a single directive. These operations are:

* `CREATE`
* `UPDATE`
* `DELETE`
* `CREATE_RELATIONSHIP`
* `DELETE_RELATIONSHIP`

The directive for toggling these operations will be defined as:

```gql
enum WriteOperation {
CREATE
DELETE
UPDATE
CREATE_RELATIONSHIP
DELETE_RELATIONSHIP
}

directive @write(
operations: [WriteOperation!]! = [CREATE, UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]
) on SCHEMA | OBJECT | FIELD_DEFINITION
```

To remove create operations from both the Mutation type and also nested operations, one can simply do:

```gql
extend schema @write(operations: [UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP])
```

For a particular type, you can remove its create operation from the Mutation type and also from any nested relationship operations pointing _to_ it by doing:

```gql
type User @write(operations: [UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]) {
name: String!
}
```

Finally, you can remove nested operations for a particular relationship:

```gql
type User {
name: String!
}

type Post {
author: User! @relationship(type: "HAS_AUTHOR", direction: OUT) @write(operations: [UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP])
}
```

It should be noted, that when used on a field definition, it is only valid when combined with a `@relationship` directive, and this should be confirmed during type definition validation.

The `@write` directive can also be used in multiple locations to introduce complex rules:

```gql
type Comment @write(operations: [CREATE, DELETE, UPDATE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]) {
content: String!
}

type Post {
comments: [Comment!]! @relationship(type: "HAS_COMMENT", direction: OUT) @write(operations: [CREATE])
}

extend schema @write(operations: [CREATE, UPDATE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP])
```

In the above example:

* All delete operations are removed at the schema level
* The `deleteComments` Mutation field, however, is reintroduced by the `@write` directive on the `Comment` type
* When performing a nested operation from the `Post` type, you can only create a `Comment`, and nothing else

#### `connectOrCreate`

The existence of the `connectOrCreate` operation will depend on both the `CREATE` and `CREATE_RELATIONSHIP` operations being present in the `@write` directive.
2 changes: 1 addition & 1 deletion examples/migration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"@neo4j/graphql": "^3.17.2",
"apollo-server": "3.12.0",
"graphql": "16.6.0",
"neo4j-driver": "5.6.0"
"neo4j-driver": "5.7.0"
}
}
2 changes: 1 addition & 1 deletion examples/neo-push/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"express-rate-limit": "^6.5.2",
"graphql": "16.6.0",
"jsonwebtoken": "9.0.0",
"neo4j-driver": "5.6.0"
"neo4j-driver": "5.7.0"
},
"devDependencies": {
"@faker-js/faker": "7.6.0",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
},
"devDependencies": {
"@tsconfig/node16": "1.0.3",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@typescript-eslint/eslint-plugin": "5.57.1",
"@typescript-eslint/parser": "5.57.1",
"concurrently": "8.0.1",
"dotenv": "16.0.3",
"eslint": "8.37.0",
Expand All @@ -49,7 +49,7 @@
"husky": "8.0.3",
"jest": "29.5.0",
"lint-staged": "13.2.0",
"neo4j-driver": "5.6.0",
"neo4j-driver": "5.7.0",
"npm-run-all": "4.1.5",
"prettier": "2.8.7",
"set-tz": "0.2.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/cypher-builder/src/Cypher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export { MapProjection } from "./expressions/map/MapProjection";
export { or, and, not, xor } from "./expressions/operations/boolean";
export {
eq,
neq,
gt,
gte,
lt,
Expand All @@ -76,7 +77,7 @@ export {
endsWith,
matches,
} from "./expressions/operations/comparison";
export { plus, minus } from "./expressions/operations/math";
export { plus, minus, divide, multiply, mod, pow } from "./expressions/operations/math";

// --Functions
export { CypherFunction as Function } from "./expressions/functions/CypherFunctions";
Expand Down
19 changes: 17 additions & 2 deletions packages/cypher-builder/src/expressions/map/MapProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("Map Projection", () => {
test("Project map with properties in projection and extra values", () => {
const node = new Cypher.Node({});

const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), [".title", ".name"], {
const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), ["title", "name"], {
namedValue: Cypher.count(node),
});
const queryResult = new TestClause(mapProjection).build();
Expand All @@ -64,7 +64,7 @@ describe("Map Projection", () => {
const mapVar = new Cypher.Variable();
const node = new Cypher.Node({});

const mapProjection = new Cypher.MapProjection(mapVar, [".title", ".name"], {
const mapProjection = new Cypher.MapProjection(mapVar, ["title", "name"], {
namedValue: Cypher.count(node),
});

Expand All @@ -76,4 +76,19 @@ describe("Map Projection", () => {

expect(queryResult.params).toMatchInlineSnapshot(`Object {}`);
});

test("Convert to map with properties in projection and extra values", () => {
const node = new Cypher.Node({});

const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), ["title", "name"], {
namedValue: Cypher.count(node),
});
const queryResult = new TestClause(mapProjection.toMap()).build();

expect(queryResult.cypher).toMatchInlineSnapshot(
`"{ title: var0.title, name: var0.name, namedValue: count(this1) }"`
);

expect(queryResult.params).toMatchInlineSnapshot(`Object {}`);
});
});
Loading

0 comments on commit 8545cf6

Please sign in to comment.