Skip to content

Commit f75947d

Browse files
authored
Merge pull request #531 from equalogic/flexible-edges
feat: allow passing edges directly to ConnectionBuilder.build()
2 parents ff2ceb9 + aaed72e commit f75947d

File tree

3 files changed

+178
-44
lines changed

3 files changed

+178
-44
lines changed

README.md

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ Now define a `ConnectionBuilder` class for your `Connection` object. The builder
8080
pagination arguments for the connection, and creating the cursors and `Edge` objects that make up the connection.
8181

8282
```ts
83-
import { ConnectionBuilder, Cursor, PageInfo, validateParamsUsingSchema } from 'nestjs-graphql-connection';
83+
import {
84+
ConnectionBuilder,
85+
Cursor,
86+
EdgeInputWithCursor,
87+
PageInfo,
88+
validateParamsUsingSchema,
89+
} from 'nestjs-graphql-connection';
8490

8591
export type PersonCursorParams = { id: string };
8692
export type PersonCursor = Cursor<PersonCursorParams>;
@@ -96,7 +102,7 @@ export class PersonConnectionBuilder extends ConnectionBuilder<
96102
return new PersonConnection(fields);
97103
}
98104

99-
public createEdge(fields: { node: TestNode; cursor: string }): TestEdge {
105+
public createEdge(fields: EdgeInputWithCursor<PersonEdge>): PersonEdge {
100106
return new PersonEdge(fields);
101107
}
102108

@@ -108,7 +114,7 @@ export class PersonConnectionBuilder extends ConnectionBuilder<
108114
// A cursor sent to or received from a client is represented as a base64-encoded, URL-style query string containing
109115
// one or more key/value pairs describing the referenced node's position in the result set (its ID, a date, etc.)
110116
// Validation is optional, but recommended to enforce that cursor values supplied by clients must be well-formed.
111-
// See documentation for Joi at https://joi.dev/api/?v=17#object
117+
// This example uses Joi for validation, see documentation at https://joi.dev/api/?v=17#object
112118
// The following schema accepts only an object matching the type { id: string }:
113119
const schema: Joi.ObjectSchema<PersonCursorParams> = Joi.object({
114120
id: Joi.string().empty('').required(),
@@ -168,7 +174,12 @@ determining what the last result was on page 9.
168174
To use offset cursors, extend your builder class from `OffsetPaginatedConnectionBuilder` instead of `ConnectionBuilder`:
169175

170176
```ts
171-
import { OffsetPaginatedConnectionBuilder, PageInfo, validateParamsUsingSchema } from 'nestjs-graphql-connection';
177+
import {
178+
EdgeInputWithCursor,
179+
OffsetPaginatedConnectionBuilder,
180+
PageInfo,
181+
validateParamsUsingSchema,
182+
} from 'nestjs-graphql-connection';
172183

173184
export class PersonConnectionBuilder extends OffsetPaginatedConnectionBuilder<
174185
PersonConnection,
@@ -180,7 +191,7 @@ export class PersonConnectionBuilder extends OffsetPaginatedConnectionBuilder<
180191
return new PersonConnection(fields);
181192
}
182193

183-
public createEdge(fields: { node: TestNode; cursor: string }): TestEdge {
194+
public createEdge(fields: EdgeInputWithCursor<PersonEdge>): PersonEdge {
184195
return new PersonEdge(fields);
185196
}
186197

@@ -236,24 +247,32 @@ properties -- such as the date that the friend was added, or the type of relatio
236247
this is analogous to having a Many-to-Many relation where the intermediate join table contains additional data columns
237248
beyond just the keys of the two joined tables.)
238249

239-
In this case your edge type would look like the following example. Notice that we pass a `{ createdAt: Date }` type
240-
argument to `createEdgeType`; this specifies typings for the fields that are allowed to be passed to your edge class's
241-
constructor for initialization when doing `new PersonFriendEdge({ ...fields })`.
250+
In this case your edge type would look like the following example. Notice that we also now define a
251+
`PersonFriendEdgeInterface` type which we pass as a generic argument to `createEdgeType`; this ensures correct typings
252+
for the fields that are allowed to be passed to your edge class's constructor for initialization when doing
253+
`new PersonFriendEdge({ ...fields })`.
242254

243255
```ts
244256
import { Field, GraphQLISODateTime, ObjectType } from '@nestjs/graphql';
245-
import { createEdgeType } from 'nestjs-graphql-connection';
257+
import { createEdgeType, EdgeInterface } from 'nestjs-graphql-connection';
246258
import { Person } from './entities';
247259

260+
export interface PersonFriendEdgeInterface extends EdgeInterface<Person> {
261+
createdAt: Date;
262+
}
263+
248264
@ObjectType()
249-
export class PersonFriendEdge extends createEdgeType<{ createdAt: Date }>(Person) {
265+
export class PersonFriendEdge
266+
extends createEdgeType<PersonFriendEdgeInterface>(Person)
267+
implements PersonFriendEdgeInterface
268+
{
250269
@Field(type => GraphQLISODateTime)
251270
public createdAt: Date;
252271
}
253272
```
254273

255-
`ConnectionBuilder` supports overriding the `createConnection()` and `createEdge()` methods when calling `build()`. This
256-
enables you to enrich the connection and edges with additional metadata at resolve time.
274+
To achieve this, you can pass an array of partial `edges` (instead of `nodes`) to `build()`. This enables you to
275+
provide values for any additional fields present on the edges.
257276

258277
The following example assumes you have a GraphQL schema that defines a `friends` field on your `Person` object, which
259278
resolves to a `PersonFriendConnection` containing the person's friends. In your database you would have a `friend` table
@@ -284,19 +303,34 @@ export class PersonResolver {
284303
// Return resolved PersonFriendConnection with edges and pageInfo
285304
return connectionBuilder.build({
286305
totalEdges,
287-
nodes: friends.map(friend => friend.otherPerson),
288-
createEdge: ({ node, cursor }) => {
289-
const friend = friends.find(friend => friend.otherPerson.id === node.id);
290-
291-
return new PersonFriendEdge({ node, cursor, createdAt: friend.createdAt });
292-
},
306+
edges: friends.map(friend => ({
307+
node: friend.otherPerson,
308+
createdAt: friend.createdAt,
309+
})),
293310
});
294311
}
295312
}
296313
```
297314

298-
Alternatively, you could build the connection result yourself by replacing the `connectionBuilder.build(...)` statement
299-
with something like the following:
315+
Alternatively, you can override the `createEdge()` or `createConnection()` methods when calling `build()`.
316+
317+
```ts
318+
return connectionBuilder.build({
319+
totalEdges,
320+
nodes: friends.map(friend => friend.otherPerson),
321+
createConnection({ edges, pageInfo }) {
322+
return new PersonFriendConnection({ edges, pageInfo, customField: 'hello-world' });
323+
},
324+
createEdge: ({ node, cursor }) => {
325+
const friend = friends.find(friend => friend.otherPerson.id === node.id);
326+
327+
return new PersonFriendEdge({ node, cursor, createdAt: friend.createdAt });
328+
},
329+
});
330+
```
331+
332+
Finally, if the above methods don't meet your needs you can always build the connection result yourself by replacing
333+
`connectionBuilder.build(...)` with something like the following:
300334

301335
```ts
302336
// Resolve edges with cursor, node, and additional metadata

src/builder/ConnectionBuilder.spec.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
11
import Joi from 'joi';
22
import { Cursor } from '../cursor/Cursor';
33
import { validateParamsUsingSchema } from '../cursor/validateParamsUsingSchema';
4-
import { ConnectionArgs, createConnectionType, createEdgeType, PageInfo } from '../type';
5-
import { ConnectionBuilder } from './ConnectionBuilder';
4+
import {
5+
ConnectionArgs,
6+
ConnectionInterface,
7+
createConnectionType,
8+
createEdgeType,
9+
EdgeInterface,
10+
PageInfo,
11+
} from '../type';
12+
import { ConnectionBuilder, EdgeInputWithCursor } from './ConnectionBuilder';
613

714
class TestNode {
815
id: string;
916
name: string;
1017
}
1118

12-
class TestEdge extends createEdgeType<{ customEdgeField?: number }>(TestNode) {
19+
interface TestEdgeInterface extends EdgeInterface<TestNode> {
20+
customEdgeField?: number;
21+
}
22+
23+
class TestEdge extends createEdgeType<TestEdgeInterface>(TestNode) implements TestEdgeInterface {
1324
public customEdgeField?: number;
1425
}
1526

16-
class TestConnection extends createConnectionType<{ customConnectionField?: number }>(TestEdge) {
27+
interface TestConnectionInterface extends ConnectionInterface<TestEdge> {
28+
customConnectionField?: number;
29+
}
30+
31+
class TestConnection
32+
extends createConnectionType<TestConnectionInterface>(TestEdge)
33+
implements TestConnectionInterface
34+
{
1735
public customConnectionField?: number;
1836
}
1937

@@ -36,7 +54,7 @@ class TestConnectionBuilder extends ConnectionBuilder<
3654
return new TestConnection(fields);
3755
}
3856

39-
public createEdge(fields: { node: TestNode; cursor: string }): TestEdge {
57+
public createEdge(fields: EdgeInputWithCursor<TestEdge>): TestEdge {
4058
return new TestEdge(fields);
4159
}
4260

@@ -271,4 +289,37 @@ describe('ConnectionBuilder', () => {
271289
],
272290
});
273291
});
292+
293+
test('Can build Connection using partial Edges', () => {
294+
const builder = new TestConnectionBuilder({
295+
first: 5,
296+
});
297+
const connection = builder.build({
298+
totalEdges: 12,
299+
edges: [
300+
{ node: { id: 'node1', name: 'A' }, customEdgeField: 1 },
301+
{ node: { id: 'node2', name: 'B' }, customEdgeField: 2 },
302+
{ node: { id: 'node3', name: 'C' }, customEdgeField: 3 },
303+
{ node: { id: 'node4', name: 'D' }, customEdgeField: 4 },
304+
{ node: { id: 'node5', name: 'E' }, customEdgeField: 5 },
305+
],
306+
});
307+
308+
expect(connection).toMatchObject({
309+
pageInfo: {
310+
totalEdges: 12,
311+
hasNextPage: true,
312+
hasPreviousPage: false,
313+
startCursor: new Cursor({ id: 'node1' }).encode(),
314+
endCursor: new Cursor({ id: 'node5' }).encode(),
315+
},
316+
edges: [
317+
{ node: { id: 'node1', name: 'A' }, cursor: new Cursor({ id: 'node1' }).encode(), customEdgeField: 1 },
318+
{ node: { id: 'node2', name: 'B' }, cursor: new Cursor({ id: 'node2' }).encode(), customEdgeField: 2 },
319+
{ node: { id: 'node3', name: 'C' }, cursor: new Cursor({ id: 'node3' }).encode(), customEdgeField: 3 },
320+
{ node: { id: 'node4', name: 'D' }, cursor: new Cursor({ id: 'node4' }).encode(), customEdgeField: 4 },
321+
{ node: { id: 'node5', name: 'E' }, cursor: new Cursor({ id: 'node5' }).encode(), customEdgeField: 5 },
322+
],
323+
});
324+
});
274325
});

src/builder/ConnectionBuilder.ts

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,52 @@ export interface ConnectionBuilderOptions {
88
allowReverseOrder?: boolean;
99
}
1010

11+
export type EdgeExtraFields<TEdge extends EdgeInterface<TNode>, TNode = any> = Partial<Omit<TEdge, 'node' | 'cursor'>>;
12+
13+
export type EdgeInput<TEdge extends EdgeInterface<TNode>, TNode = any> = {
14+
node: TNode;
15+
cursor?: string;
16+
} & EdgeExtraFields<TEdge>;
17+
18+
export type EdgeInputWithCursor<TEdge extends EdgeInterface<TNode>, TNode = any> = {
19+
node: TNode;
20+
cursor: string;
21+
} & EdgeExtraFields<TEdge>;
22+
23+
interface CommonBuildParams<
24+
TNode,
25+
TEdge extends EdgeInterface<TNode>,
26+
TConnection extends ConnectionInterface<TEdge>,
27+
TCursor extends Cursor = Cursor,
28+
> {
29+
totalEdges?: number;
30+
hasNextPage?: boolean;
31+
hasPreviousPage?: boolean;
32+
createConnection?: (fields: { edges: TEdge[]; pageInfo: PageInfo }) => TConnection;
33+
createEdge?: (fields: EdgeInputWithCursor<TEdge>) => TEdge;
34+
createCursor?: (node: TNode, index: number) => TCursor;
35+
}
36+
37+
type BuildFromNodesParams<
38+
TNode,
39+
TEdge extends EdgeInterface<TNode>,
40+
TConnection extends ConnectionInterface<TEdge>,
41+
TCursor extends Cursor = Cursor,
42+
> = {
43+
nodes?: TNode[];
44+
edges?: never;
45+
} & CommonBuildParams<TNode, TEdge, TConnection, TCursor>;
46+
47+
type BuildFromEdgesParams<
48+
TNode,
49+
TEdge extends EdgeInterface<TNode>,
50+
TConnection extends ConnectionInterface<TEdge>,
51+
TCursor extends Cursor = Cursor,
52+
> = {
53+
nodes?: never;
54+
edges?: EdgeInput<TEdge>[];
55+
} & CommonBuildParams<TNode, TEdge, TConnection, TCursor>;
56+
1157
export abstract class ConnectionBuilder<
1258
TConnection extends ConnectionInterface<TEdge>,
1359
TConnectionArgs extends ConnectionArgs,
@@ -28,7 +74,7 @@ export abstract class ConnectionBuilder<
2874

2975
public abstract createConnection(fields: { edges: TEdge[]; pageInfo: PageInfo }): TConnection;
3076

31-
public abstract createEdge(fields: { node: TNode; cursor: string }): TEdge;
77+
public abstract createEdge(fields: EdgeInputWithCursor<TEdge>): TEdge;
3278

3379
public abstract createCursor(node: TNode, index: number): TCursor;
3480

@@ -58,34 +104,37 @@ export abstract class ConnectionBuilder<
58104

59105
public build({
60106
nodes,
107+
edges,
61108
totalEdges,
62109
hasNextPage,
63110
hasPreviousPage,
64111
createConnection = this.createConnection,
65112
createEdge = this.createEdge,
66113
createCursor = this.createCursor,
67-
}: {
68-
nodes: TNode[];
69-
totalEdges?: number;
70-
hasNextPage?: boolean;
71-
hasPreviousPage?: boolean;
72-
createConnection?: (fields: { edges: TEdge[]; pageInfo: PageInfo }) => TConnection;
73-
createEdge?: (fields: { node: TNode; cursor: string }) => TEdge;
74-
createCursor?: (node: TNode, index: number) => TCursor;
75-
}): TConnection {
76-
const edges = nodes.map((node, index) =>
77-
createEdge.bind(this)({
78-
node,
79-
cursor: createCursor.bind(this)(node, index).encode(),
80-
}),
81-
);
114+
}:
115+
| BuildFromNodesParams<TNode, TEdge, TConnection, TCursor>
116+
| BuildFromEdgesParams<TNode, TEdge, TConnection, TCursor>): TConnection {
117+
const resolvedEdges: TEdge[] =
118+
edges != null
119+
? edges.map((edge, index) =>
120+
createEdge.bind(this)({
121+
cursor: edge.cursor ?? createCursor.bind(this)(edge.node, index).encode(),
122+
...edge,
123+
}),
124+
)
125+
: nodes!.map((node, index) =>
126+
createEdge.bind(this)({
127+
node,
128+
cursor: createCursor.bind(this)(node, index).encode(),
129+
}),
130+
);
82131

83132
return createConnection.bind(this)({
84-
edges,
133+
edges: resolvedEdges,
85134
pageInfo: this.createPageInfo({
86-
edges,
135+
edges: resolvedEdges,
87136
totalEdges,
88-
hasNextPage: hasNextPage ?? (totalEdges != null && totalEdges > edges.length),
137+
hasNextPage: hasNextPage ?? (totalEdges != null && totalEdges > resolvedEdges.length),
89138
hasPreviousPage: hasPreviousPage ?? (this.afterCursor != null || this.beforeCursor != null),
90139
}),
91140
});

0 commit comments

Comments
 (0)