Skip to content
This repository was archived by the owner on Sep 3, 2021. It is now read-only.

Commit bdcb515

Browse files
Declarative primary keys, constraints and indexes with @id, @unique, and @index directives (#499)
* generates definitions for @id, @unique, and @index adds validation function used during augmentation and within assertSchema to throw custom errors for inappropriate or redundant directive combinations * adds a helper for obtaining all fields of a type, including extensions * updates field set used for getting primary key for node mutation api * adds custom errors for invalid key directive combinations uses getTypeFields to update field set used for getting primary key for node selection input type generated for relationship mutations * new file for node selection functions * uses getTypeFields to update field sets used to get primary key * adds custom errors for using key directives on relationship types * Update types.js * adds assertSchema for calling apoc.schema.assert during server setup uses a new schemaAssert function to generate the Cypher statement for calling apoc.schema.assert * adds schemaAssert for generating the Cypher for calling apoc.schema.assert() * removes the use of isNeo4jTypeField * small refactor of getPrimaryKey arguments and possiblySetFirstId * small refactor of possiblySetFirstId, moves key selection functions also factors out an old inappropriate helper function, _getNamedType, replacing it with unwrapNamedType * Update cypherTestHelpers.js * Update testSchema.js * new test for assertSchema using @id, @unique, and @index in testSchema * Update augmentSchemaTest.test.js * adds @id, @unique, and @index removes relationship mutation tests that uses temporal field node selection * fix comment typo * removes unused import * adds schema assertion errors for fields on @relation types * changes initial test and first example to @id * adds tests for schema assertion errors * Update augmentSchemaTest.test.js * fixes names for key directive tests * update test name
1 parent c432905 commit bdcb515

File tree

18 files changed

+1672
-758
lines changed

18 files changed

+1672
-758
lines changed

src/augment/directives.js

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
buildDirective,
1111
buildName
1212
} from './ast';
13+
import { ApolloError } from 'apollo-server-errors';
1314

1415
/**
1516
* An enum describing the names of directive definitions and instances
@@ -23,7 +24,10 @@ export const DirectiveDefinition = {
2324
IS_AUTHENTICATED: 'isAuthenticated',
2425
HAS_ROLE: 'hasRole',
2526
HAS_SCOPE: 'hasScope',
26-
ADDITIONAL_LABELS: 'additionalLabels'
27+
ADDITIONAL_LABELS: 'additionalLabels',
28+
ID: 'id',
29+
UNIQUE: 'unique',
30+
INDEX: 'index'
2731
};
2832

2933
// The name of Role type used in authorization logic
@@ -37,24 +41,84 @@ const RelationshipDirectionField = {
3741
TO: 'to'
3842
};
3943

40-
/**
41-
* A predicate function for cypher directive fields
42-
*/
4344
export const isCypherField = ({ directives = [] }) =>
4445
getDirective({
4546
directives,
4647
name: DirectiveDefinition.CYPHER
4748
});
4849

49-
/**
50-
* A predicate function for neo4j_ignore directive fields
51-
*/
5250
export const isIgnoredField = ({ directives = [] }) =>
5351
getDirective({
5452
directives,
5553
name: DirectiveDefinition.NEO4J_IGNORE
5654
});
5755

56+
export const isRelationField = ({ directives = [] }) =>
57+
getDirective({
58+
directives,
59+
name: DirectiveDefinition.RELATION
60+
});
61+
62+
export const isPrimaryKeyField = ({ directives = [] }) =>
63+
getDirective({
64+
directives,
65+
name: DirectiveDefinition.ID
66+
});
67+
68+
export const isUniqueField = ({ directives = [] }) =>
69+
getDirective({
70+
directives,
71+
name: DirectiveDefinition.UNIQUE
72+
});
73+
74+
export const isIndexedField = ({ directives = [] }) =>
75+
getDirective({
76+
directives,
77+
name: DirectiveDefinition.INDEX
78+
});
79+
80+
export const validateFieldDirectives = ({ fields = [], directives = [] }) => {
81+
const primaryKeyFields = fields.filter(field =>
82+
isPrimaryKeyField({
83+
directives: field.directives
84+
})
85+
);
86+
if (primaryKeyFields.length > 1)
87+
throw new ApolloError(
88+
`The @id directive can only be used once per node type.`
89+
);
90+
const isPrimaryKey = isPrimaryKeyField({ directives });
91+
const isUnique = isUniqueField({ directives });
92+
const isIndex = isIndexedField({ directives });
93+
const isComputed = isCypherField({ directives });
94+
if (isComputed) {
95+
if (isPrimaryKey)
96+
throw new ApolloError(
97+
`The @id directive cannot be used with the @cypher directive because computed fields are not stored as properties.`
98+
);
99+
if (isUnique)
100+
throw new ApolloError(
101+
`The @unique directive cannot be used with the @cypher directive because computed fields are not stored as properties.`
102+
);
103+
if (isIndex)
104+
throw new ApolloError(
105+
`The @index directive cannot used with the @cypher directive because computed fields are not stored as properties.`
106+
);
107+
}
108+
if (isPrimaryKey && isUnique)
109+
throw new ApolloError(
110+
`The @id and @unique directive combined are redunant. The @id directive already sets a unique property constraint and an index.`
111+
);
112+
if (isPrimaryKey && isIndex)
113+
throw new ApolloError(
114+
`The @id and @index directive combined are redundant. The @id directive already sets a unique property constraint and an index.`
115+
);
116+
if (isUnique && isIndex)
117+
throw new ApolloError(
118+
`The @unique and @index directive combined are redunant. The @unique directive sets both a unique property constraint and an index.`
119+
);
120+
};
121+
58122
/**
59123
* The main export for augmenting directive definitions
60124
*/
@@ -363,6 +427,24 @@ const directiveDefinitionBuilderMap = {
363427
name: DirectiveDefinition.NEO4J_IGNORE,
364428
locations: [DirectiveLocation.FIELD_DEFINITION]
365429
};
430+
},
431+
[DirectiveDefinition.ID]: ({ config }) => {
432+
return {
433+
name: DirectiveDefinition.ID,
434+
locations: [DirectiveLocation.FIELD_DEFINITION]
435+
};
436+
},
437+
[DirectiveDefinition.UNIQUE]: ({ config }) => {
438+
return {
439+
name: DirectiveDefinition.UNIQUE,
440+
locations: [DirectiveLocation.FIELD_DEFINITION]
441+
};
442+
},
443+
[DirectiveDefinition.INDEX]: ({ config }) => {
444+
return {
445+
name: DirectiveDefinition.INDEX,
446+
locations: [DirectiveLocation.FIELD_DEFINITION]
447+
};
366448
}
367449
};
368450

src/augment/fields.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,20 @@ export const propertyFieldExists = ({
267267
);
268268
});
269269
};
270+
271+
export const getTypeFields = ({
272+
typeName = '',
273+
definition = {},
274+
typeExtensionDefinitionMap = {}
275+
}) => {
276+
const allFields = [];
277+
const fields = definition.fields;
278+
if (fields && fields.length) {
279+
// if there are .fields, return them
280+
allFields.push(...fields);
281+
const extensions = typeExtensionDefinitionMap[typeName] || [];
282+
// also return any .fields of extensions of this type
283+
extensions.forEach(extension => allFields.push(...extension.fields));
284+
}
285+
return allFields;
286+
};

src/augment/types/node/mutation.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import {
1111
useAuthDirective,
1212
isCypherField
1313
} from '../../directives';
14-
import { getPrimaryKey } from '../../../utils';
14+
import { getPrimaryKey } from './selection';
1515
import { shouldAugmentType } from '../../augment';
1616
import { OperationType } from '../../types/types';
1717
import {
1818
TypeWrappers,
1919
getFieldDefinition,
2020
isNeo4jIDField,
21-
getTypeExtensionFieldDefinition
21+
getTypeExtensionFieldDefinition,
22+
getTypeFields
2223
} from '../../fields';
2324

2425
/**
@@ -46,7 +47,12 @@ export const augmentNodeMutationAPI = ({
4647
typeExtensionDefinitionMap,
4748
config
4849
}) => {
49-
const primaryKey = getPrimaryKey(definition);
50+
const fields = getTypeFields({
51+
typeName,
52+
definition,
53+
typeExtensionDefinitionMap
54+
});
55+
const primaryKey = getPrimaryKey({ fields });
5056
const mutationTypeName = OperationType.MUTATION;
5157
const mutationType = operationTypeMap[mutationTypeName];
5258
const mutationTypeNameLower = mutationTypeName.toLowerCase();

src/augment/types/node/node.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
TypeWrappers,
1212
unwrapNamedType,
1313
isPropertyTypeField,
14-
buildNeo4jSystemIDField
14+
buildNeo4jSystemIDField,
15+
getTypeFields
1516
} from '../../fields';
1617
import {
1718
FilteringArgument,
@@ -23,7 +24,11 @@ import {
2324
getRelationName,
2425
getDirective,
2526
isIgnoredField,
26-
DirectiveDefinition
27+
isPrimaryKeyField,
28+
isUniqueField,
29+
isIndexedField,
30+
DirectiveDefinition,
31+
validateFieldDirectives
2732
} from '../../directives';
2833
import {
2934
buildName,
@@ -40,7 +45,8 @@ import {
4045
isObjectTypeExtensionDefinition,
4146
isInterfaceTypeExtensionDefinition
4247
} from '../../types/types';
43-
import { getPrimaryKey } from '../../../utils';
48+
import { getPrimaryKey } from './selection';
49+
import { ApolloError } from 'apollo-server-errors';
4450

4551
/**
4652
* The main export for the augmentation process of a GraphQL
@@ -216,7 +222,8 @@ export const augmentNodeTypeFields = ({
216222
let fieldType = field.type;
217223
let fieldArguments = field.arguments;
218224
const fieldDirectives = field.directives;
219-
if (!isIgnoredField({ directives: fieldDirectives })) {
225+
const isIgnored = isIgnoredField({ directives: fieldDirectives });
226+
if (!isIgnored) {
220227
isIgnoredType = false;
221228
const fieldName = field.name.value;
222229
const unwrappedType = unwrapNamedType({ type: fieldType });
@@ -236,6 +243,10 @@ export const augmentNodeTypeFields = ({
236243
type: outputType
237244
})
238245
) {
246+
validateFieldDirectives({
247+
fields,
248+
directives: fieldDirectives
249+
});
239250
nodeInputTypeMap = augmentInputTypePropertyFields({
240251
inputTypeMap: nodeInputTypeMap,
241252
fieldName,
@@ -361,6 +372,21 @@ const augmentNodeTypeField = ({
361372
relationshipDirective,
362373
outputTypeWrappers
363374
}) => {
375+
const isPrimaryKey = isPrimaryKeyField({ directives: fieldDirectives });
376+
const isUnique = isUniqueField({ directives: fieldDirectives });
377+
const isIndex = isIndexedField({ directives: fieldDirectives });
378+
if (isPrimaryKey)
379+
throw new ApolloError(
380+
`The @id directive cannot be used on @relation fields.`
381+
);
382+
if (isUnique)
383+
throw new ApolloError(
384+
`The @unique directive cannot be used on @relation fields.`
385+
);
386+
if (isIndex)
387+
throw new ApolloError(
388+
`The @index directive cannot be used on @relation fields.`
389+
);
364390
const isUnionType = isUnionTypeDefinition({ definition: outputDefinition });
365391
fieldArguments = augmentNodeTypeFieldArguments({
366392
fieldArguments,
@@ -458,6 +484,7 @@ const augmentNodeTypeAPI = ({
458484
typeName,
459485
propertyInputValues,
460486
generatedTypeMap,
487+
typeExtensionDefinitionMap,
461488
config
462489
});
463490
}
@@ -490,12 +517,18 @@ const buildNodeSelectionInputType = ({
490517
typeName,
491518
propertyInputValues,
492519
generatedTypeMap,
520+
typeExtensionDefinitionMap,
493521
config
494522
}) => {
495523
const mutationTypeName = OperationType.MUTATION;
496524
const mutationTypeNameLower = mutationTypeName.toLowerCase();
497525
if (shouldAugmentType(config, mutationTypeNameLower, typeName)) {
498-
const primaryKey = getPrimaryKey(definition);
526+
const fields = getTypeFields({
527+
typeName,
528+
definition,
529+
typeExtensionDefinitionMap
530+
});
531+
const primaryKey = getPrimaryKey({ fields });
499532
const propertyInputName = `_${typeName}Input`;
500533
if (primaryKey) {
501534
const primaryKeyName = primaryKey.name.value;

0 commit comments

Comments
 (0)