Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error when only selecting info on update and create operations #568

Merged
merged 3 commits into from
Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 4 additions & 4 deletions packages/graphql/src/schema/resolvers/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ export default function createResolver({ node }: { node: Node }) {
context,
});

const responseField = info.fieldNodes[0].selectionSet?.selections.find(
const nodeProjection = info.fieldNodes[0].selectionSet?.selections.find(
(selection) => selection.kind === "Field" && selection.name.value === node.getPlural({ camelCase: true })
) as FieldNode; // Field exist by construction and must be selected as it is the only field.
) as FieldNode;

const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value;
const nodeKey = nodeProjection?.alias ? nodeProjection.alias.value : nodeProjection?.name?.value;

return {
info: {
bookmark: executeResult.bookmark,
...executeResult.statistics,
},
[responseKey]: Object.values(executeResult.records[0] || {}),
...(nodeProjection ? { [nodeKey]: Object.values(executeResult.records[0] || {}) } : {}),
};
}

Expand Down
8 changes: 4 additions & 4 deletions packages/graphql/src/schema/resolvers/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ export default function updateResolver({ node }: { node: Node }) {
context,
});

const responseField = info.fieldNodes[0].selectionSet?.selections.find(
const nodeProjection = info.fieldNodes[0].selectionSet?.selections.find(
(selection) => selection.kind === "Field" && selection.name.value === node.getPlural({ camelCase: true })
) as FieldNode; // Field exist by construction and must be selected as it is the only field.
) as FieldNode;

const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value;
const nodeKey = nodeProjection?.alias ? nodeProjection.alias.value : nodeProjection?.name?.value;

return {
info: {
bookmark: executeResult.bookmark,
...executeResult.statistics,
},
[responseKey]: executeResult.records.map((x) => x.this),
...(nodeProjection ? { [nodeKey]: executeResult.records.map((x) => x.this) } : {}),
};
}

Expand Down
150 changes: 78 additions & 72 deletions packages/graphql/src/translate/translate-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,18 @@ import createConnectionAndParams from "./connection/create-connection-and-params
import createInterfaceProjectionAndParams from "./create-interface-projection-and-params";

function translateCreate({ context, node }: { context: Context; node: Node }): [string, any] {
const { resolveTree } = context;
const connectionStrs: string[] = [];
const interfaceStrs: string[] = [];
let connectionParams: any;
let interfaceParams: any;

const { resolveTree } = context;
const mutationResponse =
resolveTree.fieldsByTypeName[`Create${node.getPlural({ camelCase: false })}MutationResponse`];

// Due to potential aliasing of returned object in response we look through fields of CreateMutationResponse
// and find field where field.name ~ node.name which exists by construction
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { fieldsByTypeName } = Object.values(
resolveTree.fieldsByTypeName[`Create${node.getPlural({ camelCase: false })}MutationResponse`]
).find((field) => field.name === node.getPlural({ camelCase: true }))!;
const nodeProjection = Object.values(mutationResponse).find(
(field) => field.name === node.getPlural({ camelCase: true })
);
danstarns marked this conversation as resolved.
Show resolved Hide resolved

const { createStrs, params } = (resolveTree.args.input as any[]).reduce(
(res, input, index) => {
Expand Down Expand Up @@ -68,57 +67,78 @@ function translateCreate({ context, node }: { context: Context; node: Node }): [
params: any;
};

/* so projection params don't conflict with create params. We only need to call createProjectionAndParams once. */
let projAuth = "";
const projection = createProjectionAndParams({
node,
context,
fieldsByTypeName,
varName: "REPLACE_ME",
});
if (projection[2]?.authValidateStrs?.length) {
projAuth = `CALL apoc.util.validate(NOT(${projection[2].authValidateStrs.join(
" AND "
)}), "${AUTH_FORBIDDEN_ERROR}", [0])`;
}

const replacedProjectionParams = Object.entries(projection[1]).reduce((res, [key, value]) => {
return { ...res, [key.replace("REPLACE_ME", "projection")]: value };
}, {});

if (projection[2]?.connectionFields?.length) {
projection[2].connectionFields.forEach((connectionResolveTree) => {
const connectionField = node.connectionFields.find(
(x) => x.fieldName === connectionResolveTree.name
) as ConnectionField;
const connection = createConnectionAndParams({
resolveTree: connectionResolveTree,
field: connectionField,
context,
nodeVariable: "REPLACE_ME",
});
connectionStrs.push(connection[0]);
if (!connectionParams) connectionParams = {};
connectionParams = { ...connectionParams, ...connection[1] };
let replacedProjectionParams: Record<string, unknown> = {};
let projectionStr: string | undefined = undefined;
let authCalls: string | undefined = undefined;
danstarns marked this conversation as resolved.
Show resolved Hide resolved

if (nodeProjection) {
let projAuth = "";
const projection = createProjectionAndParams({
node,
context,
fieldsByTypeName: nodeProjection.fieldsByTypeName,
varName: "REPLACE_ME",
});
}

if (projection[2]?.interfaceFields?.length) {
projection[2].interfaceFields.forEach((interfaceResolveTree) => {
const relationshipField = node.relationFields.find(
(x) => x.fieldName === interfaceResolveTree.name
) as RelationField;
const interfaceProjection = createInterfaceProjectionAndParams({
resolveTree: interfaceResolveTree,
field: relationshipField,
context,
node,
nodeVariable: "REPLACE_ME",
if (projection[2]?.authValidateStrs?.length) {
projAuth = `CALL apoc.util.validate(NOT(${projection[2].authValidateStrs.join(
" AND "
)}), "${AUTH_FORBIDDEN_ERROR}", [0])`;
}

replacedProjectionParams = Object.entries(projection[1]).reduce((res, [key, value]) => {
return { ...res, [key.replace("REPLACE_ME", "projection")]: value };
}, {});

projectionStr = createStrs
.map(
(_, i) =>
`\nthis${i} ${projection[0]
// First look to see if projection param is being reassigned
// e.g. in an apoc.cypher.runFirstColumn function call used in createProjection->connectionField
.replace(/REPLACE_ME(?=\w+: \$REPLACE_ME)/g, "projection")
.replace(/\$REPLACE_ME/g, "$projection")
.replace(/REPLACE_ME/g, `this${i}`)} AS this${i}`
)
.join(", ");

authCalls = createStrs
.map((_, i) => projAuth.replace(/\$REPLACE_ME/g, "$projection").replace(/REPLACE_ME/g, `this${i}`))
.join("\n");

if (projection[2]?.connectionFields?.length) {
projection[2].connectionFields.forEach((connectionResolveTree) => {
const connectionField = node.connectionFields.find(
(x) => x.fieldName === connectionResolveTree.name
) as ConnectionField;
const connection = createConnectionAndParams({
resolveTree: connectionResolveTree,
field: connectionField,
context,
nodeVariable: "REPLACE_ME",
});
connectionStrs.push(connection[0]);
if (!connectionParams) connectionParams = {};
connectionParams = { ...connectionParams, ...connection[1] };
});
interfaceStrs.push(interfaceProjection.cypher);
if (!interfaceParams) interfaceParams = {};
interfaceParams = { ...interfaceParams, ...interfaceProjection.params };
});
}

if (projection[2]?.interfaceFields?.length) {
projection[2].interfaceFields.forEach((interfaceResolveTree) => {
const relationshipField = node.relationFields.find(
(x) => x.fieldName === interfaceResolveTree.name
) as RelationField;
const interfaceProjection = createInterfaceProjectionAndParams({
resolveTree: interfaceResolveTree,
field: relationshipField,
context,
node,
nodeVariable: "REPLACE_ME",
});
interfaceStrs.push(interfaceProjection.cypher);
if (!interfaceParams) interfaceParams = {};
interfaceParams = { ...interfaceParams, ...interfaceProjection.params };
});
}
}

const replacedConnectionStrs = connectionStrs.length
Expand Down Expand Up @@ -163,28 +183,14 @@ function translateCreate({ context, node }: { context: Context; node: Node }): [
}, {})
: {};

const projectionStr = createStrs
.map(
(_, i) =>
`\nthis${i} ${projection[0]
// First look to see if projection param is being reassigned
// e.g. in an apoc.cypher.runFirstColumn function call used in createProjection->connectionField
.replace(/REPLACE_ME(?=\w+: \$REPLACE_ME)/g, "projection")
.replace(/\$REPLACE_ME/g, "$projection")
.replace(/REPLACE_ME/g, `this${i}`)} AS this${i}`
)
.join(", ");

const authCalls = createStrs
.map((_, i) => projAuth.replace(/\$REPLACE_ME/g, "$projection").replace(/REPLACE_ME/g, `this${i}`))
.join("\n");
const returnStatement = nodeProjection ? `RETURN ${projectionStr}` : "RETURN 'Query cannot conclude with CALL'";

const cypher = [
`${createStrs.join("\n")}`,
authCalls,
...replacedConnectionStrs,
...replacedInterfaceStrs,
`\nRETURN ${projectionStr}`,
returnStatement,
];

return [
Expand Down
102 changes: 54 additions & 48 deletions packages/graphql/src/translate/translate-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [
const interfaceStrs: string[] = [];
let updateArgs = {};

// Due to potential aliasing of returned object in response we look through fields of UpdateMutationResponse
// and find field where field.name ~ node.name which exists by construction
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { fieldsByTypeName } = Object.values(
resolveTree.fieldsByTypeName[`Update${node.getPlural({ camelCase: false })}MutationResponse`]
).find((field) => field.name === node.getPlural({ camelCase: true }))!;
const mutationResponse =
resolveTree.fieldsByTypeName[`Update${node.getPlural({ camelCase: false })}MutationResponse`];

const nodeProjection = Object.values(mutationResponse).find(
(field) => field.name === node.getPlural({ camelCase: true })
);

if (whereInput) {
const where = createWhereAndParams({
Expand Down Expand Up @@ -327,53 +327,59 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [
};
}

const projection = createProjectionAndParams({
node,
context,
fieldsByTypeName,
varName,
});
[projStr] = projection;
cypherParams = { ...cypherParams, ...projection[1] };
if (projection[2]?.authValidateStrs?.length) {
projAuth = `CALL apoc.util.validate(NOT(${projection[2].authValidateStrs.join(
" AND "
)}), "${AUTH_FORBIDDEN_ERROR}", [0])`;
}
if (nodeProjection?.fieldsByTypeName) {
const projection = createProjectionAndParams({
node,
context,
fieldsByTypeName: nodeProjection.fieldsByTypeName,
varName,
});
[projStr] = projection;
cypherParams = { ...cypherParams, ...projection[1] };
if (projection[2]?.authValidateStrs?.length) {
projAuth = `CALL apoc.util.validate(NOT(${projection[2].authValidateStrs.join(
" AND "
)}), "${AUTH_FORBIDDEN_ERROR}", [0])`;
}

if (projection[2]?.connectionFields?.length) {
projection[2].connectionFields.forEach((connectionResolveTree) => {
const connectionField = node.connectionFields.find(
(x) => x.fieldName === connectionResolveTree.name
) as ConnectionField;
const connection = createConnectionAndParams({
resolveTree: connectionResolveTree,
field: connectionField,
context,
nodeVariable: varName,
if (projection[2]?.connectionFields?.length) {
projection[2].connectionFields.forEach((connectionResolveTree) => {
const connectionField = node.connectionFields.find(
(x) => x.fieldName === connectionResolveTree.name
) as ConnectionField;
const connection = createConnectionAndParams({
resolveTree: connectionResolveTree,
field: connectionField,
context,
nodeVariable: varName,
});
connectionStrs.push(connection[0]);
cypherParams = { ...cypherParams, ...connection[1] };
});
connectionStrs.push(connection[0]);
cypherParams = { ...cypherParams, ...connection[1] };
});
}
}

if (projection[2]?.interfaceFields?.length) {
projection[2].interfaceFields.forEach((interfaceResolveTree) => {
const relationshipField = node.relationFields.find(
(x) => x.fieldName === interfaceResolveTree.name
) as RelationField;
const interfaceProjection = createInterfaceProjectionAndParams({
resolveTree: interfaceResolveTree,
field: relationshipField,
context,
node,
nodeVariable: varName,
if (projection[2]?.interfaceFields?.length) {
projection[2].interfaceFields.forEach((interfaceResolveTree) => {
const relationshipField = node.relationFields.find(
(x) => x.fieldName === interfaceResolveTree.name
) as RelationField;
const interfaceProjection = createInterfaceProjectionAndParams({
resolveTree: interfaceResolveTree,
field: relationshipField,
context,
node,
nodeVariable: varName,
});
interfaceStrs.push(interfaceProjection.cypher);
cypherParams = { ...cypherParams, ...interfaceProjection.params };
});
interfaceStrs.push(interfaceProjection.cypher);
cypherParams = { ...cypherParams, ...interfaceProjection.params };
});
}
}

const returnStatement = nodeProjection
? `RETURN ${varName} ${projStr} AS ${varName}`
: `RETURN 'Query cannot conclude with CALL'`;

const cypher = [
matchStr,
whereStr,
Expand All @@ -386,7 +392,7 @@ function translateUpdate({ node, context }: { node: Node; context: Context }): [
...(projAuth ? [projAuth] : []),
...connectionStrs,
...interfaceStrs,
`RETURN ${varName} ${projStr} AS ${varName}`,
returnStatement,
];

return [
Expand Down
Loading