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

Declarative primary keys, constraints and indexes with @id, @unique, and @index directives #499

Merged
merged 27 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e6b5731
generates definitions for @id, @unique, and @index
michaeldgraham Aug 23, 2020
5cf51b3
adds a helper for obtaining all fields of a type, including extensions
michaeldgraham Aug 23, 2020
e1d7b1d
updates field set used for getting primary key for node mutation api
michaeldgraham Aug 23, 2020
ebd4e8e
adds custom errors for invalid key directive combinations
michaeldgraham Aug 23, 2020
ab0b9d6
new file for node selection functions
michaeldgraham Aug 23, 2020
367307d
uses getTypeFields to update field sets used to get primary key
michaeldgraham Aug 23, 2020
4723e95
adds custom errors for using key directives on relationship types
michaeldgraham Aug 23, 2020
6ba28fc
Update types.js
michaeldgraham Aug 23, 2020
b5b75b2
adds assertSchema for calling apoc.schema.assert during server setup
michaeldgraham Aug 23, 2020
fd62a54
adds schemaAssert for generating the Cypher for calling apoc.schema.a…
michaeldgraham Aug 23, 2020
9581cac
removes the use of isNeo4jTypeField
michaeldgraham Aug 23, 2020
5dc9a54
small refactor of getPrimaryKey arguments and possiblySetFirstId
michaeldgraham Aug 23, 2020
983e273
small refactor of possiblySetFirstId, moves key selection functions
michaeldgraham Aug 23, 2020
ede97e6
Update cypherTestHelpers.js
michaeldgraham Aug 23, 2020
7ae259a
Update testSchema.js
michaeldgraham Aug 23, 2020
ccc2b0e
new test for assertSchema using @id, @unique, and @index in testSchema
michaeldgraham Aug 23, 2020
b90b0c1
Update augmentSchemaTest.test.js
michaeldgraham Aug 23, 2020
cbd5709
adds @id, @unique, and @index
michaeldgraham Aug 23, 2020
2aa8275
fix comment typo
michaeldgraham Aug 24, 2020
57c63f8
removes unused import
michaeldgraham Aug 24, 2020
c6adcd2
adds schema assertion errors for fields on @relation types
michaeldgraham Aug 24, 2020
1447b57
changes initial test and first example to @id
michaeldgraham Aug 24, 2020
9fb1da0
adds tests for schema assertion errors
michaeldgraham Aug 24, 2020
55b05de
Update augmentSchemaTest.test.js
michaeldgraham Aug 24, 2020
fe55150
fixes names for key directive tests
michaeldgraham Aug 24, 2020
c3c3abb
Merge branch 'master' of https://github.com/michaeldgraham/neo4j-grap…
michaeldgraham Aug 24, 2020
0a82512
update test name
michaeldgraham Aug 24, 2020
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
96 changes: 89 additions & 7 deletions src/augment/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
buildDirective,
buildName
} from './ast';
import { ApolloError } from 'apollo-server-errors';

/**
* An enum describing the names of directive definitions and instances
Expand All @@ -23,7 +24,10 @@ export const DirectiveDefinition = {
IS_AUTHENTICATED: 'isAuthenticated',
HAS_ROLE: 'hasRole',
HAS_SCOPE: 'hasScope',
ADDITIONAL_LABELS: 'additionalLabels'
ADDITIONAL_LABELS: 'additionalLabels',
ID: 'id',
UNIQUE: 'unique',
INDEX: 'index'
};

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

/**
* A predicate function for cypher directive fields
*/
export const isCypherField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.CYPHER
});

/**
* A predicate function for neo4j_ignore directive fields
*/
export const isIgnoredField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.NEO4J_IGNORE
});

export const isRelationField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.RELATION
});

export const isPrimaryKeyField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.ID
});

export const isUniqueField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.UNIQUE
});

export const isIndexedField = ({ directives = [] }) =>
getDirective({
directives,
name: DirectiveDefinition.INDEX
});

export const validateFieldDirectives = ({ fields = [], directives = [] }) => {
const primaryKeyFields = fields.filter(field =>
isPrimaryKeyField({
directives: field.directives
})
);
if (primaryKeyFields.length > 1)
throw new ApolloError(
`The @id directive can only be used once per node type.`
);
const isPrimaryKey = isPrimaryKeyField({ directives });
const isUnique = isUniqueField({ directives });
const isIndex = isIndexedField({ directives });
const isComputed = isCypherField({ directives });
if (isComputed) {
if (isPrimaryKey)
throw new ApolloError(
`The @id directive cannot be used with the @cypher directive because computed fields are not stored as properties.`
);
if (isUnique)
throw new ApolloError(
`The @unique directive cannot be used with the @cypher directive because computed fields are not stored as properties.`
);
if (isIndex)
throw new ApolloError(
`The @index directive cannot used with the @cypher directive because computed fields are not stored as properties.`
);
}
if (isPrimaryKey && isUnique)
throw new ApolloError(
`The @id and @unique directive combined are redunant. The @id directive already sets a unique property constraint and an index.`
);
if (isPrimaryKey && isIndex)
throw new ApolloError(
`The @id and @index directive combined are redundant. The @id directive already sets a unique property constraint and an index.`
);
if (isUnique && isIndex)
throw new ApolloError(
`The @unique and @index directive combined are redunant. The @unique directive sets both a unique property constraint and an index.`
);
};

/**
* The main export for augmenting directive definitions
*/
Expand Down Expand Up @@ -363,6 +427,24 @@ const directiveDefinitionBuilderMap = {
name: DirectiveDefinition.NEO4J_IGNORE,
locations: [DirectiveLocation.FIELD_DEFINITION]
};
},
[DirectiveDefinition.ID]: ({ config }) => {
return {
name: DirectiveDefinition.ID,
locations: [DirectiveLocation.FIELD_DEFINITION]
};
},
[DirectiveDefinition.UNIQUE]: ({ config }) => {
return {
name: DirectiveDefinition.UNIQUE,
locations: [DirectiveLocation.FIELD_DEFINITION]
};
},
[DirectiveDefinition.INDEX]: ({ config }) => {
return {
name: DirectiveDefinition.INDEX,
locations: [DirectiveLocation.FIELD_DEFINITION]
};
}
};

Expand Down
17 changes: 17 additions & 0 deletions src/augment/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,20 @@ export const propertyFieldExists = ({
);
});
};

export const getTypeFields = ({
typeName = '',
definition = {},
typeExtensionDefinitionMap = {}
}) => {
const allFields = [];
const fields = definition.fields;
if (fields && fields.length) {
// if there are .fields, return them
allFields.push(...fields);
const extensions = typeExtensionDefinitionMap[typeName] || [];
// also return any .fields of extensions of this type
extensions.forEach(extension => allFields.push(...extension.fields));
}
return allFields;
};
12 changes: 9 additions & 3 deletions src/augment/types/node/mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import {
useAuthDirective,
isCypherField
} from '../../directives';
import { getPrimaryKey } from '../../../utils';
import { getPrimaryKey } from './selection';
import { shouldAugmentType } from '../../augment';
import { OperationType } from '../../types/types';
import {
TypeWrappers,
getFieldDefinition,
isNeo4jIDField,
getTypeExtensionFieldDefinition
getTypeExtensionFieldDefinition,
getTypeFields
} from '../../fields';

/**
Expand Down Expand Up @@ -46,7 +47,12 @@ export const augmentNodeMutationAPI = ({
typeExtensionDefinitionMap,
config
}) => {
const primaryKey = getPrimaryKey(definition);
const fields = getTypeFields({
typeName,
definition,
typeExtensionDefinitionMap
});
const primaryKey = getPrimaryKey({ fields });
const mutationTypeName = OperationType.MUTATION;
const mutationType = operationTypeMap[mutationTypeName];
const mutationTypeNameLower = mutationTypeName.toLowerCase();
Expand Down
43 changes: 38 additions & 5 deletions src/augment/types/node/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
TypeWrappers,
unwrapNamedType,
isPropertyTypeField,
buildNeo4jSystemIDField
buildNeo4jSystemIDField,
getTypeFields
} from '../../fields';
import {
FilteringArgument,
Expand All @@ -23,7 +24,11 @@ import {
getRelationName,
getDirective,
isIgnoredField,
DirectiveDefinition
isPrimaryKeyField,
isUniqueField,
isIndexedField,
DirectiveDefinition,
validateFieldDirectives
} from '../../directives';
import {
buildName,
Expand All @@ -40,7 +45,8 @@ import {
isObjectTypeExtensionDefinition,
isInterfaceTypeExtensionDefinition
} from '../../types/types';
import { getPrimaryKey } from '../../../utils';
import { getPrimaryKey } from './selection';
import { ApolloError } from 'apollo-server-errors';

/**
* The main export for the augmentation process of a GraphQL
Expand Down Expand Up @@ -216,7 +222,8 @@ export const augmentNodeTypeFields = ({
let fieldType = field.type;
let fieldArguments = field.arguments;
const fieldDirectives = field.directives;
if (!isIgnoredField({ directives: fieldDirectives })) {
const isIgnored = isIgnoredField({ directives: fieldDirectives });
if (!isIgnored) {
isIgnoredType = false;
const fieldName = field.name.value;
const unwrappedType = unwrapNamedType({ type: fieldType });
Expand All @@ -236,6 +243,10 @@ export const augmentNodeTypeFields = ({
type: outputType
})
) {
validateFieldDirectives({
fields,
directives: fieldDirectives
});
nodeInputTypeMap = augmentInputTypePropertyFields({
inputTypeMap: nodeInputTypeMap,
fieldName,
Expand Down Expand Up @@ -361,6 +372,21 @@ const augmentNodeTypeField = ({
relationshipDirective,
outputTypeWrappers
}) => {
const isPrimaryKey = isPrimaryKeyField({ directives: fieldDirectives });
const isUnique = isUniqueField({ directives: fieldDirectives });
const isIndex = isIndexedField({ directives: fieldDirectives });
if (isPrimaryKey)
throw new ApolloError(
`The @id directive cannot be used on @relation fields.`
);
if (isUnique)
throw new ApolloError(
`The @unique directive cannot be used on @relation fields.`
);
if (isIndex)
throw new ApolloError(
`The @index directive cannot be used on @relation fields.`
);
const isUnionType = isUnionTypeDefinition({ definition: outputDefinition });
fieldArguments = augmentNodeTypeFieldArguments({
fieldArguments,
Expand Down Expand Up @@ -458,6 +484,7 @@ const augmentNodeTypeAPI = ({
typeName,
propertyInputValues,
generatedTypeMap,
typeExtensionDefinitionMap,
config
});
}
Expand Down Expand Up @@ -490,12 +517,18 @@ const buildNodeSelectionInputType = ({
typeName,
propertyInputValues,
generatedTypeMap,
typeExtensionDefinitionMap,
config
}) => {
const mutationTypeName = OperationType.MUTATION;
const mutationTypeNameLower = mutationTypeName.toLowerCase();
if (shouldAugmentType(config, mutationTypeNameLower, typeName)) {
const primaryKey = getPrimaryKey(definition);
const fields = getTypeFields({
typeName,
definition,
typeExtensionDefinitionMap
});
const primaryKey = getPrimaryKey({ fields });
const propertyInputName = `_${typeName}Input`;
if (primaryKey) {
const primaryKeyName = primaryKey.name.value;
Expand Down
Loading