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

Experimental mutation API: Fixes for data input objects #536

Merged
merged 9 commits into from
Nov 9, 2020
21 changes: 15 additions & 6 deletions src/augment/types/node/mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
/**
* An enum describing the names of node type mutations
*/
const NodeMutation = {
export const NodeMutation = {
CREATE: 'Create',
UPDATE: 'Update',
DELETE: 'Delete',
Expand Down Expand Up @@ -213,20 +213,29 @@ const buildNodeMutationObjectArguments = ({ typeName, operationName = '' }) => {
}
}
};
const propertyInputConfig = {
const propertyCreateInputConfig = {
name: 'data',
type: {
name: `_${typeName}Data`,
name: `_${typeName}Create`,
wrappers: {
[TypeWrappers.NON_NULL_NAMED_TYPE]: true
}
}
};
const propertyUpdateInputConfig = {
name: 'data',
type: {
name: `_${typeName}Update`,
wrappers: {
[TypeWrappers.NON_NULL_NAMED_TYPE]: true
}
}
};
if (operationName === NodeMutation.CREATE) {
args.push(propertyInputConfig);
args.push(propertyCreateInputConfig);
} else if (operationName === NodeMutation.UPDATE) {
args.push(nodeSelectionConfig);
args.push(propertyInputConfig);
args.push(propertyUpdateInputConfig);
} else if (operationName === NodeMutation.MERGE) {
const keySelectionInputConfig = {
name: 'where',
Expand All @@ -238,7 +247,7 @@ const buildNodeMutationObjectArguments = ({ typeName, operationName = '' }) => {
}
};
args.push(keySelectionInputConfig);
args.push(propertyInputConfig);
args.push(propertyCreateInputConfig);
} else if (operationName === NodeMutation.DELETE) {
args.push(nodeSelectionConfig);
}
Expand Down
50 changes: 43 additions & 7 deletions src/augment/types/node/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
buildPropertyFilters,
buildLogicalFilterInputValues
} from '../../input-values';
import { NodeMutation } from './mutation';

/**
* Gets a single field for use as a primary key
Expand Down Expand Up @@ -192,12 +193,19 @@ export const buildNodeSelectionInputTypes = ({
definition,
typeExtensionDefinitionMap
});
// Used by Create, Update, Merge
generatedTypeMap = buildNodeDataInputObject({
// Used by Create, Merge
generatedTypeMap = buildNodeCreateInputObject({
typeName,
propertyInputValues,
generatedTypeMap
});
// Used by Update
generatedTypeMap = buildNodeUpdateInputObject({
typeName,
propertyInputValues,
generatedTypeMap
});

// Used by Update, Delete
generatedTypeMap = buildNodeSelectionInputObject({
typeName,
Expand Down Expand Up @@ -306,19 +314,47 @@ const buildNodeKeySelectionInputObject = ({
return generatedTypeMap;
};

const buildNodeDataInputObject = ({
const buildNodeCreateInputObject = ({
typeName,
propertyInputValues = [],
generatedTypeMap
}) => {
const propertyInputName = `_${typeName}Create`;
const inputValues = propertyInputValues.map(field => {
const { name, type, directives } = field;
const isPrimaryKey = directives.some(
directive => directive.name.value === 'id'
);
// keep nonnull and list type wrappers for Create and Merge node mutation,
// expect for a primary key, so it could be generated if not provided
if (isPrimaryKey) type.wrappers = {};
return buildInputValue({
name: buildName({ name }),
type: buildNamedType(type)
});
});
if (inputValues.length) {
generatedTypeMap[propertyInputName] = buildInputObjectType({
name: buildName({ name: propertyInputName }),
fields: inputValues
});
}
return generatedTypeMap;
};

const buildNodeUpdateInputObject = ({
typeName,
propertyInputValues = [],
generatedTypeMap
}) => {
const propertyInputName = `_${typeName}Data`;
const propertyInputName = `_${typeName}Update`;
const inputValues = propertyInputValues.map(field => {
const { name, type } = field;
// set all fields to optional, keep list type wrappers
type.wrappers[TypeWrappers.NON_NULL_NAMED_TYPE] = false;
return buildInputValue({
name: buildName({ name }),
type: buildNamedType({
name: type.name
})
type: buildNamedType(type)
});
});
if (inputValues.length) {
Expand Down
85 changes: 65 additions & 20 deletions src/translate.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ import {
isListTypeField,
TypeWrappers
} from './augment/fields';
import {
isPrimaryKeyField,
isUniqueField,
isIndexedField
} from './augment/directives';
import {
analyzeMutationArguments,
isNeo4jTypeArgument,
Expand Down Expand Up @@ -1740,14 +1745,24 @@ const nodeMergeOrUpdate = ({
// config.experimental
// no need to use .params key in this argument design
params = params.params;
const inputTranslation = translateNodeInputArgument({
dataArgument,
variableName,
params,
typeMap,
resolveInfo,
context
});
const [propertyStatements, generatePrimaryKey] = translateNodeInputArgument(
{
selectionArgument,
dataArgument,
params,
primaryKey,
typeMap,
fieldMap,
resolveInfo,
context
}
);
let onMatchStatements = ``;
if (propertyStatements.length > 0) {
onMatchStatements = `SET ${safeVar(
variableName
)} += {${propertyStatements.join(',')}} `;
}
if (isMergeMutation(resolveInfo)) {
const unwrappedType = unwrapNamedType({ type: selectionArgument.type });
const name = unwrappedType.name;
Expand All @@ -1761,9 +1776,20 @@ const nodeMergeOrUpdate = ({
resolveInfo,
cypherParams: getCypherParams(context)
});
query = `${cypherOperation} (${safeVariableName}:${safeLabelName}{${selectionExpression.join(
','
)}})${inputTranslation}\n`;
// generatePrimaryKey is either empty or contains a call to apoc.create.uuid for @id key
const onCreateProps = [...propertyStatements, ...generatePrimaryKey];
let onCreateStatements = ``;
if (onCreateProps.length > 0) {
onCreateStatements = `SET ${safeVar(
variableName
)} += {${onCreateProps.join(',')}}`;
}
const keySelectionStatement = selectionExpression.join(',');
query = `${cypherOperation} (${safeVariableName}:${safeLabelName}{${keySelectionStatement}})
ON CREATE
${onCreateStatements}
ON MATCH
${onMatchStatements}`;
} else {
const [predicate, serializedFilter] = translateNodeSelectionArgument({
variableName,
Expand All @@ -1772,7 +1798,8 @@ const nodeMergeOrUpdate = ({
schemaType,
resolveInfo
});
query = `${cypherOperation} (${safeVariableName}:${safeLabelName})${predicate}${inputTranslation}\n`;
query = `${cypherOperation} (${safeVariableName}:${safeLabelName})${predicate}
${onMatchStatements}\n`;
params = { ...params, ...serializedFilter };
}
} else {
Expand Down Expand Up @@ -1858,9 +1885,10 @@ RETURN ${safeVariableName}`;
};

const translateNodeInputArgument = ({
selectionArgument = {},
dataArgument = {},
variableName,
params,
primaryKey,
typeMap,
resolveInfo,
context
Expand All @@ -1870,20 +1898,37 @@ const translateNodeInputArgument = ({
const inputType = typeMap[name];
const inputValues = inputType.getFields();
const updateArgs = Object.values(inputValues).map(arg => arg.astNode);
let translation = '';
const paramUpdateStatements = buildCypherParameters({
let propertyStatements = buildCypherParameters({
args: updateArgs,
params,
paramKey: 'data',
resolveInfo,
cypherParams: getCypherParams(context)
});
if (paramUpdateStatements.length > 0) {
translation = `\nSET ${safeVar(
variableName
)} += {${paramUpdateStatements.join(',')}} `;
let primaryKeyStatement = [];
if (isMergeMutation(resolveInfo)) {
const unwrappedType = unwrapNamedType({ type: selectionArgument.type });
const name = unwrappedType.name;
const inputType = typeMap[name];
const inputValues = inputType.getFields();
const selectionArgs = Object.values(inputValues).map(arg => arg.astNode);
// check key selection values for @id key argument
const primaryKeySelectionValue = setPrimaryKeyValue({
args: selectionArgs,
params: params['where'],
primaryKey
});
const primaryKeyValue = setPrimaryKeyValue({
args: updateArgs,
params: params['data'],
primaryKey
});
if (primaryKeySelectionValue.length && primaryKeyValue.length) {
// apoc.create.uuid() statement returned for both, so a value exists in neither
primaryKeyStatement = primaryKeySelectionValue;
}
}
return translation;
return [propertyStatements, primaryKeyStatement];
};

const translateNodeSelectionArgument = ({
Expand Down
37 changes: 25 additions & 12 deletions test/helpers/experimental/augmentSchemaTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,38 @@ export function cypherTestRunner(
testSchema +
`
type Mutation {
CreateUser(data: _UserData!): User @hasScope(scopes: ["User: Create", "create:user"])
UpdateUser(where: _UserWhere!, data: _UserData!): User @hasScope(scopes: ["User: Update", "update:user"])
CreateUser(data: _UserCreate!): User @hasScope(scopes: ["User: Create", "create:user"])
UpdateUser(where: _UserWhere!, data: _UserUpdate!): User @hasScope(scopes: ["User: Update", "update:user"])
DeleteUser(where: _UserWhere!): User @hasScope(scopes: ["User: Delete", "delete:user"])
MergeUser(where: _UserWhere!, data: _UserData!): User @hasScope(scopes: ["User: Merge", "merge:user"])
MergeUser(where: _UserKeys!, data: _UserCreate!): User @hasScope(scopes: ["User: Merge", "merge:user"])
}

type Query {
User: [User] @hasScope(scopes: ["User: Read", "read:user"])
}

input _UserCreate {
idField: ID
name: String
names: [String]
birthday: _Neo4jDateTimeInput
birthdays: [_Neo4jDateTimeInput]
uniqueString: String!
indexedInt: Int
extensionString: String!
}

input _UserUpdate {
idField: ID
name: String
names: [String]
birthday: _Neo4jDateTimeInput
birthdays: [_Neo4jDateTimeInput]
uniqueString: String
indexedInt: Int
extensionString: String
}

input _UserWhere {
AND: [_UserWhere!]
OR: [_UserWhere!]
Expand Down Expand Up @@ -81,15 +103,6 @@ export function cypherTestRunner(
uniqueString: String
indexedInt: Int
}

input _UserData {
idField: ID
name: String
birthday: _Neo4jDateTimeInput
uniqueString: String
indexedInt: Int
extensionString: String
}

`;

Expand Down
2 changes: 2 additions & 0 deletions test/helpers/experimental/testSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export const testSchema = `
type User {
idField: ID! @id
name: String
names: [String]
birthday: DateTime
birthdays: [DateTime]
uniqueString: String! @unique
indexedInt: Int @index
liked: [Movie!]! @relation(
Expand Down
Loading