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/unique directive #547

Merged
merged 28 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d3bd915
Add definition and basic docs for `@unique` directive
darrellwarde Oct 20, 2021
a73b490
Basic solution for asserting unique constraints into existence
darrellwarde Oct 21, 2021
cd82b46
Account for `label` argument in `@node` directive
darrellwarde Oct 21, 2021
d1f6f9f
Documentation work
darrellwarde Oct 21, 2021
6a821fa
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 22, 2021
807a4c2
Create database specifically for constraints tests to avoid failures
darrellwarde Oct 22, 2021
d08875c
Add unique node property constraints for `@id` directive, and throw e…
darrellwarde Oct 22, 2021
6f44a90
Compatibility with `@alias`
darrellwarde Oct 22, 2021
7330048
Add WAIT clause after CREATE DATABASE to ensure database exists for t…
darrellwarde Oct 22, 2021
d68a92a
Run Enterprise instead of Community Docker images so that we can crea…
darrellwarde Oct 22, 2021
ac27208
Swap Cypher WAIT for in-code sleep as not compatible with all versions
darrellwarde Oct 22, 2021
537160f
Move constraint filtering for tests into code instead of Cypher, not …
darrellwarde Oct 22, 2021
7cf2228
Alternative solution for fetching constraints using db.constraints
darrellwarde Oct 25, 2021
f4e8285
Refactor test files to use db.constraints, and update all GitHub Actions
darrellwarde Oct 25, 2021
08c9790
Refactor assertConstraints
darrellwarde Oct 25, 2021
71b4d9f
Rename to assertIndexesAndConstraints
darrellwarde Oct 25, 2021
b972616
Throw errors other than no multi-database support in integration tests
darrellwarde Oct 26, 2021
dff40e7
Add debug logging to constraint assertion, and check directive combin…
darrellwarde Oct 26, 2021
bdc1fb2
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 26, 2021
c076d13
Constraint validation error handling
darrellwarde Oct 26, 2021
43c6224
Move unique parsing into function
darrellwarde Oct 27, 2021
a985803
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 27, 2021
460e8fb
Add uniqueFields getter to Node
darrellwarde Oct 27, 2021
7976038
Fix test with varying expected outcomes based on environment
darrellwarde Oct 27, 2021
16acfeb
Small refactors
darrellwarde Oct 27, 2021
d1c8d88
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 27, 2021
efe2c04
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 28, 2021
ba91769
Merge branch 'master' into feature/unique-directive
darrellwarde Oct 28, 2021
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
1 change: 1 addition & 0 deletions docs/modules/ROOT/content-nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*** xref:type-definitions/cypher.adoc[]
*** xref:type-definitions/default-values.adoc[]
*** xref:type-definitions/database-mapping.adoc[]
*** xref:type-definitions/constraints.adoc[]
** xref:queries.adoc[]
** xref:mutations/index.adoc[]
*** xref:mutations/create.adoc[]
Expand Down
145 changes: 145 additions & 0 deletions docs/modules/ROOT/pages/api-reference/neo4jgraphql.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,148 @@ const { CypherRuntime } = require("@neo4j/graphql");
- `CypherReplanning.FORCE` +
- `CypherReplanning.SKIP`
|===

[[api-reference-checkneo4jcompat]]
== `checkNeo4jCompat`

Asynchronous function to check the compatibility of the specified DBMS, that either resolves to `void` in a successful scenario, or throws an error if the database is not compatible with the Neo4j GraphQL Library.

Takes an `input` object as a parameter, the supported fields of which are described below.

=== Example

Given any valid type definitions saved to the variable `typeDefs` and a valid driver instance saved to the variable `driver`, the following will confirm database compatibility:

[source, javascript, indent=0]
----
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
await neoSchema.checkNeo4jCompat();
----

[[api-reference-checkneo4jcompat-input]]
=== Input

Accepts the arguments below:

|===
|Name and Type |Description

|`driver` +
+
Type: https://neo4j.com/docs/javascript-manual/current/[`Driver`]
|An instance of a Neo4j driver.

|`driverConfig` +
+
Type: xref::api-reference/neo4jgraphql.adoc#api-reference-checkneo4jcompat-input-driverconfig[`DriverConfig`]
|Additional driver configuration options.
|===

[[api-reference-checkneo4jcompat-input-driverconfig]]
==== `DriverConfig`

|===
|Name and Type |Description

|`database` +
+
Type: `string`
|The name of the database within the DBMS to connect to.

|`bookmarks` +
+
Type: `string` or `Array<string>`
|One or more bookmarks to use for the connection.
|===

[[api-reference-assertconstraints]]
== `assertConstraints`

Asynchronous function to assert the existence of database constraints, that either resolves to `void` in a successful scenario, or throws an error if the necessary consraints do not exist following its execution.

Takes an `input` object as a parameter, the supported fields of which are described below.

=== Example

Given the following type definitions saved to the variable `typeDefs` and a valid driver instance saved to the variable `driver`:

[source, graphql, indent=0]
----
type Book {
isbn: String! @unique
}
----

And the construction of a `Neo4jGraphQL`, using:

[source, javascript, indent=0]
----
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
----

The following will check whether a unique node property constraint exists for label "Book" and property "isbn", and throw an error if it does not:

[source, javascript, indent=0]
----
await neoSchema.assertConstraints();
----

The next example will create the constraint if it does not exist:

[source, javascript, indent=0]
----
await neoSchema.assertConstraints({ options: { create: true } });
----

[[api-reference-assertconstraints-input]]
=== Input

Accepts the arguments below:

|===
|Name and Type |Description

|`driver` +
+
Type: https://neo4j.com/docs/javascript-manual/current/[`Driver`]
|An instance of a Neo4j driver.

|`driverConfig` +
+
Type: xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-driverconfig[`DriverConfig`]
|Additional driver configuration options.

|`options` +
+
Type: xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-assertconstraintsoptions[`AssertConstraintsOptions`]
|Options for the execution of `assertConstraints`.
|===

[[api-reference-assertconstraints-input-driverconfig]]
==== `DriverConfig`

|===
|Name and Type |Description

|`database` +
+
Type: `string`
|The name of the database within the DBMS to connect to.

|`bookmarks` +
+
Type: `string` or `Array<string>`
|One or more bookmarks to use for the connection.
|===

[[api-reference-assertconstraints-input-assertconstraintsoptions]]
==== `AssertConstraintsOptions`

|===
|Name and Type |Description

|`create` +
+
Type: `boolean`
|Whether or not to create constraints if they do not yet exist. Disabled by default.
|===
6 changes: 6 additions & 0 deletions docs/modules/ROOT/pages/directives.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ The `@timestamp` directive flags fields to be used to store timestamps on create

Reference: xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-timestamp[`@timestamp`]

== `@unique`

The `@unique` directive indicates that there should be a uniqueness constraint in the database for the fields that it is applied to.

Reference: xref::type-definitions/constraints.adoc#type-definitions-constraints-unique[Unique node property constraints]

== `@writeonly`

The `@writeonly` directive marks fields as write-only.
Expand Down
21 changes: 18 additions & 3 deletions docs/modules/ROOT/pages/type-definitions/autogeneration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
[[type-definitions-autogeneration-id]]
== `@id`

This directive marks a field as the unique identifier for an object type, and by default enables autogenerated of IDs for the field.
This directive marks a field as the unique identifier for an object type, and by default; enables autogeneration of IDs for the field and implies that a unique node property constraint should exist for the property.

The format of each generated ID is a UUID generated by https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-randomuuid[randomUUID() function].

If autogeneration for an ID field is enabled, the field will not be present in input types for Mutations.

See xref::type-definitions/constraints.adoc#type-definitions-constraints-unique[Unique node property constraints] for details on how to assert the existence of the necessary database constraints for relevant fields.

=== Definition

[source, graphql, indent=0]
----
"""Indicates that the field is the unique identifier for the object type, and additionally enables the autogeneration of IDs."""
directive @id(autogenerate: Boolean! = true) on FIELD_DEFINITION
"""Indicates that the field is an identifier for the object type. By default; autogenerated, and has a unique node property constraint in the database."""
directive @id(
autogenerate: Boolean! = true
unique: Boolean! = true
) on FIELD_DEFINITION
----

=== Usage
Expand Down Expand Up @@ -48,6 +53,16 @@ type User {
}
----

You can disable the mapping of the `@id` directive to a unique node property constraint by setting the `unique` argument to `false`:

[source, graphql, indent=0]
----
type User {
id: ID! @id(unique: false)
username: String!
}
----

[[type-definitions-autogeneration-timestamp]]
== `@timestamp`

Expand Down
79 changes: 79 additions & 0 deletions docs/modules/ROOT/pages/type-definitions/constraints.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
[[type-definitions-constraints]]
= Constraints

[[type-definitions-constraints-unique]]
== Unique node property constraints

Unique node property constraints map to `@unique` directives used in your type definitions, which has the following definition:

[source, graphql, indent=0]
----
"""Informs @neo4j/graphql that there should be a uniqueness constraint in the database for the decorated field."""
directive @unique(
"""The name which should be used for this constraint. By default; type name, followed by an underscore, followed by the field name."""
constraintName: String
) on FIELD_DEFINITION
----

Additionally, the usage of the xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive by default implies that there should be a unique node property constraint in the database for that property.

Using this directive does not automatically ensure the existence of these constraints, and you will need to run a function on server startup. See the section xref::type-definitions/constraints.adoc#type-definitions-constraints-asserting[Asserting constraints] below for details.

=== `@unique` directive usage

`@unique` directives can only be used in GraphQL object types representing nodes, and will only be applied for the "main" label for the node. You can find some examples below.

In the following example, a unique constraint will be asserted for the label `Colour` and the property `hexadecimal`:

[source, graphql, indent=0]
----
type Colour {
hexadecimal: String! @unique
}
----

In the next example, a unique constraint with name `unique_colour` will be asserted for the label `Colour` and the property `hexadecimal`:

[source, graphql, indent=0]
----
type Colour {
hexadecimal: String! @unique(constraintName: "unique_colour")
}
----

The `@node` directive is used to change the database label mapping in this next example, so a unique constraint will be asserted for the label `Color` and the property `hexadecimal`:

[source, graphql, indent=0]
----
type Colour @node(label: "Color") {
hexadecimal: String! @unique
}
----

In the following example, the `additionalLabels` argument of the `@node` directive is ignored when it comes to asserting constraints, so the outcome is the same as the example above:

[source, graphql, indent=0]
----
type Colour @node(label: "Color", additionalLabels: ["Hue"]) {
hexadecimal: String! @unique
}
----

[[type-definitions-constraints-asserting]]
== Asserting constraints

In order to ensure that the specified constraints exist in the database, you will need to run the function `assertConstraints`, the full details of which can be found in the xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints[API reference]. A simple example to create the necessary constraints might look like the following, assuming a valid driver instance in the variable `driver`. This will create two constraints, one for each field decorated with `@id` or `@unique`:

[source, javascript, indent=0]
----
const typeDefs = gql`
type Colour {
id: ID! @id
hexadecimal: String! @unique
}
`;

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

await neoSchema.assertConstraints({ options: { create: true }});
----
14 changes: 14 additions & 0 deletions packages/graphql/src/classes/Neo4jGraphQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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";
import assertConstraints, { AssertConstraintOptions } from "./utils/asserts-constraints";

const debug = Debug(DEBUG_GRAPHQL);

Expand Down Expand Up @@ -170,6 +171,19 @@ class Neo4jGraphQL {

return checkNeo4jCompat({ driver, driverConfig });
}

async assertConstraints(
darrellwarde marked this conversation as resolved.
Show resolved Hide resolved
input: { driver?: Driver; driverConfig?: DriverConfig; options?: AssertConstraintOptions } = {}
): Promise<void> {
const driver = input.driver || this.driver;
const driverConfig = input.driverConfig || this.config?.driverConfig;

if (!driver) {
throw new Error("neo4j-driver Driver missing");
}

await assertConstraints({ driver, driverConfig, nodes: this.nodes, options: input.options });
}
}

export default Neo4jGraphQL;
10 changes: 10 additions & 0 deletions packages/graphql/src/classes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ type AuthableField =

type SortableField = PrimitiveField | CustomScalarField | CustomEnumField | TemporalField | PointField | CypherField;

type ConstrainableField = PrimitiveField | TemporalField | PointField;

class Node extends GraphElement {
public relationFields: RelationField[];
public connectionFields: ConnectionField[];
Expand Down Expand Up @@ -169,6 +171,10 @@ class Node extends GraphElement {
].filter((field) => !field.typeMeta.array);
}

public get constrainableFields(): ConstrainableField[] {
return [...this.primitiveFields, ...this.temporalFields, ...this.pointFields];
}

public getLabelString(context: Context): string {
return this.nodeDirective?.getLabelsString(this.name, context) || `:${this.name}`;
}
Expand All @@ -177,6 +183,10 @@ class Node extends GraphElement {
return this.nodeDirective?.getLabels(this.name, context) || [this.name];
}

public getMainLabel(): string {
return this.nodeDirective?.label || this.name;
}

public getPlural(options: { camelCase: boolean }): string {
// camelCase is optional in this case to maintain backward compatibility
if (this.nodeDirective?.plural) {
Expand Down
Loading