Skip to content

Alias connections #368

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

Merged
merged 4 commits into from
Jul 30, 2021
Merged
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
19 changes: 9 additions & 10 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
EnumTypeDefinitionNode,
GraphQLInt,
GraphQLNonNull,
GraphQLResolveInfo,
GraphQLSchema,
InputObjectTypeDefinitionNode,
InterfaceTypeDefinitionNode,
Expand Down Expand Up @@ -69,7 +70,7 @@ import getFieldTypeMeta from "./get-field-type-meta";
import Relationship, { RelationshipField } from "../classes/Relationship";
import getRelationshipFieldMeta from "./get-relationship-field-meta";
import getWhereFields from "./get-where-fields";
import { createConnectionWithEdgeProperties } from "./pagination";
import { connectionFieldResolver } from "./pagination";
import { validateDocument } from "./validation";

function makeAugmentedSchema(
Expand Down Expand Up @@ -1149,15 +1150,13 @@ function makeAugmentedSchema(
[connectionField.fieldName]: {
type: connection.NonNull,
args: composeNodeArgs,
resolve: (source, args: ConnectionQueryArgs) => {
const { totalCount: count, edges } = source[connectionField.fieldName];

const totalCount = isInt(count) ? count.toNumber() : count;

return {
totalCount,
...createConnectionWithEdgeProperties(edges, args, totalCount),
};
resolve: (source, args: ConnectionQueryArgs, ctx, info: GraphQLResolveInfo) => {
return connectionFieldResolver({
connectionField,
args,
info,
source,
});
},
},
});
Expand Down
29 changes: 24 additions & 5 deletions packages/graphql/src/schema/pagination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,25 @@ describe("cursor-pagination", () => {
const totalCount = 50;

expect(() => {
createConnectionWithEdgeProperties(arraySlice, args, totalCount);
createConnectionWithEdgeProperties({
source: { edges: arraySlice },
args,
totalCount,
selectionSet: undefined,
});
}).toThrow('Argument "first" must be a non-negative integer');
});

test("it returns all elements if the cursors are invalid", () => {
const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } }));
const args = { after: "invalid" };
const totalCount = 50;
const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount);

const result = createConnectionWithEdgeProperties({
source: { edges: arraySlice },
args,
totalCount,
selectionSet: undefined,
});
expect(result).toStrictEqual({
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })),
pageInfo: {
Expand All @@ -102,7 +111,12 @@ describe("cursor-pagination", () => {
const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } }));
const args = {};
const totalCount = 50;
const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount);
const result = createConnectionWithEdgeProperties({
source: { edges: arraySlice },
args,
totalCount,
selectionSet: undefined,
});
expect(result).toStrictEqual({
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })),
pageInfo: {
Expand All @@ -117,7 +131,12 @@ describe("cursor-pagination", () => {
const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } }));
const args = { after: offsetToCursor(10) };
const totalCount = 50;
const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount);
const result = createConnectionWithEdgeProperties({
source: { edges: arraySlice },
args,
totalCount,
selectionSet: undefined,
});
expect(result).toStrictEqual({
edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index + 11) })),
pageInfo: {
Expand Down
99 changes: 83 additions & 16 deletions packages/graphql/src/schema/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,65 @@
* limitations under the License.
*/

import { FieldNode, GraphQLResolveInfo, SelectionSetNode } from "graphql";
import { getOffsetWithDefault, offsetToCursor } from "graphql-relay/connection/arrayConnection";
import { Integer, isInt } from "neo4j-driver";
import { ConnectionField, ConnectionQueryArgs } from "../types";

function getAliasKey({ selectionSet, key }: { selectionSet: SelectionSetNode | undefined; key: string }): string {
const selection = (selectionSet?.selections || []).find(
(x) => x.kind === "Field" && x.name.value === key
) as FieldNode;

if (selection?.alias) {
return selection.alias.value;
}

return key;
}

export function connectionFieldResolver({
connectionField,
source,
args,
info,
}: {
connectionField: ConnectionField;
source: any;
args: ConnectionQueryArgs;
info: GraphQLResolveInfo;
}) {
const firstField = info.fieldNodes[0];
const selectionSet = firstField.selectionSet;

let value = source[connectionField.fieldName];
if (firstField.alias) {
value = source[firstField.alias.value];
}

const totalCountKey = getAliasKey({ selectionSet, key: "totalCount" });
const totalCount = value.totalCount;

return {
[totalCountKey]: isInt(totalCount) ? totalCount.toNumber() : totalCount,
...createConnectionWithEdgeProperties({ source: value, selectionSet, args, totalCount }),
};
}

/**
* Adapted from graphql-relay-js ConnectionFromArraySlice
*/
export function createConnectionWithEdgeProperties(
arraySlice: { node: Record<string, any>; [key: string]: any }[] = [],
args: { after?: string; first?: number } = {},
totalCount: number
) {
export function createConnectionWithEdgeProperties({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really argue with this as it obviously works... But it seems a "shame" that this is no longer really "Adapted from graphql-relay-js ConnectionFromArraySlice" as per the function comment.

Very open-ended question, was there no other way this could have been done whilst maintaining the original function? I'm just intrigued as to how the function from the graphql-relay package achieves aliasing, because it must do I imagine?

selectionSet,
source,
args = {},
totalCount,
}: {
selectionSet: SelectionSetNode | undefined;
source: any;
args: { after?: string; first?: number };
totalCount: number;
}) {
const { after, first } = args;

if ((first as number) < 0) {
Expand All @@ -40,24 +88,43 @@ export function createConnectionWithEdgeProperties(
// increment the last cursor position by one for the sliceStart
const sliceStart = lastEdgeCursor + 1;

const sliceEnd = sliceStart + ((first as number) || arraySlice.length);
const edges = source?.edges || [];

const edges = arraySlice.map((value, index) => {
const selections = selectionSet?.selections || [];

const edgesField = selections.find((x) => x.kind === "Field" && x.name.value === "edges") as FieldNode;
const cursorKey = getAliasKey({ selectionSet: edgesField?.selectionSet, key: "cursor" });
const nodeKey = getAliasKey({ selectionSet: edgesField?.selectionSet, key: "node" });

const sliceEnd = sliceStart + ((first as number) || edges.length);

const mappedEdges = edges.map((value, index) => {
return {
...value,
cursor: offsetToCursor(sliceStart + index),
...(value.node ? { [nodeKey]: value.node } : {}),
[cursorKey]: offsetToCursor(sliceStart + index),
};
});

const firstEdge = edges[0];
const lastEdge = edges[edges.length - 1];
const startCursor = mappedEdges[0]?.cursor;
const endCursor = mappedEdges[mappedEdges.length - 1]?.cursor;

const pageInfoKey = getAliasKey({ selectionSet, key: "pageInfo" });
const edgesKey = getAliasKey({ selectionSet, key: "edges" });
const pageInfoField = selections.find((x) => x.kind === "Field" && x.name.value === "pageInfo") as FieldNode;
const pageInfoSelectionSet = pageInfoField?.selectionSet;
const startCursorKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "startCursor" });
const endCursorKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "endCursor" });
const hasPreviousPageKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "hasPreviousPage" });
const hasNextPageKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "hasNextPage" });

return {
edges,
pageInfo: {
startCursor: firstEdge?.cursor,
endCursor: lastEdge?.cursor,
hasPreviousPage: lastEdgeCursor > 0,
hasNextPage: typeof first === "number" ? sliceEnd < totalCount : false,
[edgesKey]: mappedEdges,
[pageInfoKey]: {
[startCursorKey]: startCursor,
[endCursorKey]: endCursor,
[hasPreviousPageKey]: lastEdgeCursor > 0,
[hasNextPageKey]: typeof first === "number" ? sliceEnd < totalCount : false,
},
};
}
Expand Down
12 changes: 9 additions & 3 deletions packages/graphql/src/translate/create-projection-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,9 +389,15 @@ function createProjectionAndParams({

if (connectionField) {
if (!inRelationshipProjection) {
if (!res.meta.connectionFields) res.meta.connectionFields = [];
res.meta.connectionFields.push(field as ResolveTree);
res.projection.push(literalElements ? `${field.name}: ${field.name}` : `${field.name}`);
if (!res.meta.connectionFields) {
res.meta.connectionFields = [];
}

const f = field as ResolveTree;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels to me like instead of this reassignment, field should be of type ResolveTree in the reducer arguments instead of any.


res.meta.connectionFields.push(f);
res.projection.push(literalElements ? `${f.alias}: ${f.alias}` : `${f.alias}`);

return res;
}

Expand Down
Loading