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

Implement @created and @updated directives for automatic server timestamps #364

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2,795 changes: 1,281 additions & 1,514 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/augment/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
export const DirectiveDefinition = {
CYPHER: 'cypher',
RELATION: 'relation',
CREATED: 'created',
UPDATED: 'updated',
MUTATION_META: 'MutationMeta',
NEO4J_IGNORE: 'neo4j_ignore',
IS_AUTHENTICATED: 'isAuthenticated',
Expand Down Expand Up @@ -315,6 +317,20 @@ const directiveDefinitionBuilderMap = {
locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT]
};
},
[DirectiveDefinition.CREATED]: ({ config }) => {
return {
name: DirectiveDefinition.CREATED,
args: [],
locations: [DirectiveLocation.FIELD_DEFINITION]
};
},
[DirectiveDefinition.UPDATED]: ({ config }) => {
return {
name: DirectiveDefinition.UPDATED,
args: [],
locations: [DirectiveLocation.FIELD_DEFINITION]
};
},
[DirectiveDefinition.ADDITIONAL_LABELS]: ({ config }) => {
return {
name: DirectiveDefinition.ADDITIONAL_LABELS,
Expand Down
4 changes: 1 addition & 3 deletions src/augment/input-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,7 @@ export const buildFilters = ({ fieldName, fieldConfig, filterTypes = [] }) => {
[TypeWrappers.LIST_TYPE]: true
};
} else if (isPointDistanceFilter) {
fieldConfig.type.name = `${Neo4jTypeName}${
SpatialType.POINT
}DistanceFilter`;
fieldConfig.type.name = `${Neo4jTypeName}${SpatialType.POINT}DistanceFilter`;
}
inputValues.push(
buildInputValue({
Expand Down
73 changes: 72 additions & 1 deletion src/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
relationDirective,
typeIdentifiers,
getAdditionalLabels,
getCreatedUpdatedDirectiveFields,
getInterfaceDerivedTypeNames,
getPayloadSelections,
isGraphqlObjectType
Expand Down Expand Up @@ -1526,6 +1527,15 @@ const nodeCreate = ({
schemaType,
resolveInfo
});
const { createdField, updatedField } = getCreatedUpdatedDirectiveFields(
resolveInfo
);
if (createdField) {
paramStatements.push(`${createdField}: timestamp()`);
}
if (updatedField) {
paramStatements.push(`${updatedField}: timestamp()`);
}

params = { ...preparedParams, ...subParams };
const query = `
Expand Down Expand Up @@ -1651,6 +1661,16 @@ const relationshipCreate = ({
paramKey: 'data',
resolveInfo
});
const { createdField, updatedField } = getCreatedUpdatedDirectiveFields(
resolveInfo
);
if (createdField) {
paramStatements.push(`${createdField}: timestamp()`);
}
if (updatedField) {
paramStatements.push(`${updatedField}: timestamp()`);
}

const schemaTypeName = safeVar(schemaType);
const fromVariable = safeVar(fromVar);
const fromAdditionalLabels = getAdditionalLabels(
Expand Down Expand Up @@ -1949,6 +1969,32 @@ const relationshipMergeOrUpdate = ({
} else if (isUpdateMutation(resolveInfo)) {
cypherOperation = 'MATCH';
}

const { createdField, updatedField } = getCreatedUpdatedDirectiveFields(
resolveInfo
);
let timestampSet = '';
if (cypherOperation === 'MERGE') {
const onCreateSetParams = [createdField, updatedField]
.map(field => {
if (field) {
return `${field}: timestamp()`;
}
return null;
})
.filter(param => !!param);
if (onCreateSetParams.length > 0) {
timestampSet += `\nON CREATE SET ${relationshipVariable} += {${onCreateSetParams.join(
','
)}} `;
}
if (updatedField) {
timestampSet += `\nON MATCH SET ${relationshipVariable}.${updatedField} = timestamp() `;
}
} else if (cypherOperation === 'MATCH' && updatedField) {
paramStatements.push(`${updatedField}: timestamp()`);
}

params = { ...preparedParams, ...subParams };
query = `
MATCH (${fromVariable}:${fromLabel}${
Expand All @@ -1964,7 +2010,7 @@ const relationshipMergeOrUpdate = ({
? `) WHERE ${toNodeNeo4jTypeClauses.join(' AND ')} `
: ` {${toParam}: $to.${toParam}})`
}
${cypherOperation} (${fromVariable})-[${relationshipVariable}:${relationshipLabel}]->(${toVariable})${
${cypherOperation} (${fromVariable})-[${relationshipVariable}:${relationshipLabel}]->(${toVariable})${timestampSet}${
paramStatements.length > 0
? `
SET ${relationshipVariable} += {${paramStatements.join(',')}} `
Expand Down Expand Up @@ -2025,6 +2071,31 @@ const nodeMergeOrUpdate = ({
: `{${primaryKeyArgName}: $params.${primaryKeyArgName}})`
}
`;

const { createdField, updatedField } = getCreatedUpdatedDirectiveFields(
resolveInfo
);
if (cypherOperation === 'MERGE') {
const onCreateSetParams = [createdField, updatedField]
.map(field => {
if (field) {
return `${field}: timestamp()`;
}
return null;
})
.filter(param => !!param);
if (onCreateSetParams.length > 0) {
query += `ON CREATE SET ${safeVariableName} += {${onCreateSetParams.join(
','
)}} `;
}
if (updatedField) {
query += `ON MATCH SET ${safeVariableName}.${updatedField} = timestamp() `;
}
} else if (cypherOperation === 'MATCH' && updatedField) {
paramUpdateStatements.push(`${updatedField}: timestamp()`);
}

if (paramUpdateStatements.length > 0) {
query += `SET ${safeVariableName} += {${paramUpdateStatements.join(',')}} `;
}
Expand Down
22 changes: 22 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,28 @@ export const getMutationCypherDirective = resolveInfo => {
});
};

export const getCreatedUpdatedDirectiveFields = resolveInfo => {
const fields = resolveInfo.schema.getType(resolveInfo.returnType).getFields();
let createdField, updatedField;
Object.keys(fields).forEach(fieldKey => {
const field = fields[fieldKey];
const type = _getNamedType(field.astNode.type).name.value;
if (type === '_Neo4jDateTime') {
const name = field.astNode.name.value;
field.astNode.directives.forEach(directive => {
switch (directive.name.value) {
case 'created':
createdField = createdField || name;
break;
case 'updated':
updatedField = updatedField || name;
}
});
}
});
return { createdField, updatedField };
};

function argumentValue(selection, name, variableValues) {
let args = selection ? selection.arguments : [];
let arg = args.find(a => a.name.value === name);
Expand Down
8 changes: 8 additions & 0 deletions test/helpers/cypherTestHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export function augmentedSchemaCypherTestRunner(
},
SpatialNode: checkCypherQuery,
State: checkCypherQuery,
SuperHero: checkCypherQuery,
Power: checkCypherQuery,
CasedType: checkCypherQuery,
Camera: checkCypherQuery,
CustomCameras: checkCypherQuery,
Expand Down Expand Up @@ -228,6 +230,12 @@ export function augmentedSchemaCypherTestRunner(
MergeUserFriends: checkCypherMutation,
UpdateUserFriends: checkCypherMutation,
RemoveUserFriends: checkCypherMutation,
CreateSuperHero: checkCypherMutation,
MergeSuperHero: checkCypherMutation,
UpdateSuperHero: checkCypherMutation,
AddPowerEndowment: checkCypherMutation,
MergePowerEndowment: checkCypherMutation,
UpdatePowerEndowment: checkCypherMutation,
AddActorKnows: checkCypherMutation,
MergeActorKnows: checkCypherMutation,
RemoveActorKnows: checkCypherMutation,
Expand Down
21 changes: 21 additions & 0 deletions test/helpers/testSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,27 @@ export const testSchema = `
to: Movie
}

type SuperHero {
id: ID!
name: String!
created: DateTime @created
updated: DateTime @updated
}

type Power {
id: ID!
title: String!
endowment: [Endowment]
}

type Endowment @relation(name: "ENDOWED_TO") {
from: Power!
to: SuperHero!
strength: Int!
since: DateTime @created
modified: DateTime @updated
}

enum BookGenre {
Mystery
Science
Expand Down
Loading