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
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
@@ -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',
@@ -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,
4 changes: 1 addition & 3 deletions src/augment/input-values.js
Original file line number Diff line number Diff line change
@@ -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({
73 changes: 72 additions & 1 deletion src/translate.js
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ import {
relationDirective,
typeIdentifiers,
getAdditionalLabels,
getCreatedUpdatedDirectiveFields,
getInterfaceDerivedTypeNames,
getPayloadSelections,
isGraphqlObjectType
@@ -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 = `
@@ -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(
@@ -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}${
@@ -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(',')}} `
@@ -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(',')}} `;
}
22 changes: 22 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions test/helpers/cypherTestHelpers.js
Original file line number Diff line number Diff line change
@@ -186,6 +186,8 @@ export function augmentedSchemaCypherTestRunner(
},
SpatialNode: checkCypherQuery,
State: checkCypherQuery,
SuperHero: checkCypherQuery,
Power: checkCypherQuery,
CasedType: checkCypherQuery,
Camera: checkCypherQuery,
CustomCameras: checkCypherQuery,
@@ -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,
21 changes: 21 additions & 0 deletions test/helpers/testSchema.js
Original file line number Diff line number Diff line change
@@ -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
Loading