Skip to content

Commit 99b0a7c

Browse files
committed
Work on Resolver creation.
1 parent 59476e2 commit 99b0a7c

File tree

12 files changed

+483
-64
lines changed

12 files changed

+483
-64
lines changed

.eslintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"extends": "airbnb",
33
"parser": "babel-eslint",
4+
"plugins": [
5+
"flowtype"
6+
],
47
"rules": {
58
"no-underscore-dangle": 0,
69
"no-unused-expressions": 0,

src/composeWithConnection.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
/* @flow */
2-
/* eslint-disable no-use-before-define */
32

43
import { TypeComposer } from 'graphql-compose';
5-
4+
import type {
5+
composeWithConnectionOpts,
6+
} from './definition.js';
7+
import { prepareConnectionResolver } from './resolvers/connectionResolver';
68

79
export function composeWithConnection(
8-
typeComposer: TypeComposer
10+
typeComposer: TypeComposer,
11+
opts: composeWithConnectionOpts
912
): TypeComposer {
1013
if (!(typeComposer instanceof TypeComposer)) {
1114
throw new Error('You should provide TypeComposer instance to composeWithRelay method');
1215
}
1316

14-
const findById = typeComposer.getResolver('findById');
15-
if (!findById) {
16-
throw new Error(`TypeComposer(${typeComposer.getTypeName()}) provided to composeWithRelay `
17-
+ 'should have findById resolver.');
17+
if (!opts) {
18+
throw new Error('You provide empty options to composeWithConnection');
19+
}
20+
21+
if (typeComposer.hasResolver('connection')) {
22+
return typeComposer;
1823
}
1924

25+
const resolver = prepareConnectionResolver(
26+
typeComposer,
27+
opts,
28+
);
29+
30+
typeComposer.addResolver(resolver);
2031
return typeComposer;
21-
}

src/cursorId.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* @flow */
2+
3+
import type {
4+
Base64String,
5+
} from './definition.js';
6+
7+
export function base64(i: string): Base64String {
8+
return ((new Buffer(i, 'ascii')).toString('base64'));
9+
}
10+
11+
export function unbase64(i: Base64String): string {
12+
return ((new Buffer(i, 'base64')).toString('ascii'));
13+
}
14+
15+
/**
16+
* Takes a type name and an ID specific to that type name, and returns a
17+
* "cursor ID" that is unique among all types.
18+
*/
19+
export function toCursorId(prefix: string, filter: string): string {
20+
return base64([prefix, id].join(':'));
21+
}
22+
23+
/**
24+
* Takes the "cursor ID" created by toCursorId, and returns the type name and ID
25+
* used to create it.
26+
*/
27+
export function fromCursorId(cursorId: string): ResolvedGlobalId {
28+
const unbasedCursorId = unbase64(cursorId);
29+
const delimiterPos = unbasedCursorId.indexOf(':');
30+
return {
31+
prefix: unbasedCursorId.substring(0, delimiterPos),
32+
id: unbasedCursorId.substring(delimiterPos + 1),
33+
};
34+
}

src/definition.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* @flow */
2+
/* eslint-disable */
3+
4+
export type CursorData = {
5+
[fieldName: string]: mixed,
6+
};
7+
8+
export type connectionSortOpts = {
9+
resolver: string,
10+
uniqueFields: string[],
11+
sortValue: mixed,
12+
cursorToFilter: (<T>(cursorData: CursorData, filterArg: T) => T),
13+
};
14+
15+
export type connectionSortMapOpts = {
16+
[sortName: string]: connectionSortOpts,
17+
};
18+
19+
export type composeWithConnectionOpts = {
20+
sort: connectionSortMapOpts,
21+
};

src/resolvers/connectionResolver.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/* @flow */
2+
/* eslint-disable no-param-reassign, no-use-before-define */
3+
4+
import type {
5+
ExtendedResolveParams,
6+
composeWithConnectionOpts,
7+
connectionSortOpts,
8+
} from '../definition';
9+
import { Resolver, TypeComposer } from 'graphql-compose';
10+
import { GraphQLInt } from 'graphql';
11+
import { prepareConnectionType } from '../types/connectionType';
12+
import { prepareSortType } from '../types/sortInputType';
13+
import Cursor from '../types/cursorType';
14+
15+
export function prepareConnectionResolver(
16+
typeComposer: TypeComposer,
17+
opts: composeWithConnectionOpts
18+
): Resolver {
19+
if (!(typeComposer instanceof TypeComposer)) {
20+
throw new Error('First arg for Resolver connection() should be instance of TypeComposer');
21+
}
22+
23+
if (!typeComposer.hasRecordIdFn()) {
24+
throw new Error(`TypeComposer(${typeComposer.getTypeName()}) should have recordIdFn. `
25+
+ 'This function returns ID from provided object.');
26+
}
27+
28+
const countResolver = typeComposer.getResolver('count');
29+
if (!countResolver) {
30+
throw new Error(`TypeComposer(${typeComposer.getTypeName()}) should have 'count' resolver`);
31+
}
32+
33+
const findManyResolver = typeComposer.getResolver('findMany');
34+
if (!findManyResolver) {
35+
throw new Error(`TypeComposer(${typeComposer.getTypeName()}) should have 'findMany' resolver`);
36+
}
37+
38+
const additionalArgs = {};
39+
if (findManyResolver.hasArg('filter')) {
40+
additionalArgs.filter = findManyResolver.getArg('filter');
41+
}
42+
43+
const sortEnumType = prepareSortType(typeComposer, opts);
44+
45+
return new Resolver(typeComposer, {
46+
outputType: prepareConnectionType(typeComposer),
47+
name: 'connection',
48+
kind: 'query',
49+
args: {
50+
first: {
51+
type: GraphQLInt,
52+
description: 'Forward pagination argument for returning at most first edges',
53+
},
54+
after: {
55+
type: Cursor,
56+
description: 'Forward pagination argument for returning at most first edges',
57+
},
58+
last: {
59+
type: GraphQLInt,
60+
description: 'Backward pagination argument for returning at most last edges',
61+
},
62+
before: {
63+
type: Cursor,
64+
description: 'Backward pagination argument for returning at most last edges',
65+
},
66+
...additionalArgs,
67+
sort: {
68+
type: sortEnumType,
69+
defaultValue: sortEnumType.getValues()[0].name, // first enum used by default
70+
description: 'Sort argument for data ordering',
71+
},
72+
},
73+
resolve: (resolveParams: ExtendedResolveParams) => {
74+
const { projection = {}, args = {} } = resolveParams;
75+
const findManyParams = Object.assign({}, resolveParams, {
76+
args: {},
77+
projection: {},
78+
});
79+
const connSortOpts: connectionSortOpts = resolveParams.args.sort;
80+
81+
const first = args.first;
82+
const last = args.last;
83+
84+
const limit = last || first;
85+
const skip = (first - last) || 0;
86+
87+
findManyParams.args.limit = limit + 1; // +1 document, to check next page presence
88+
if (skip > 0) {
89+
findManyParams.args.skip = skip;
90+
}
91+
92+
let filter = findManyParams.args.filter;
93+
const beginCursorData = cursorToData(args.after);
94+
if (beginCursorData) {
95+
filter = connSortOpts.cursorToFilter(beginCursorData, filter);
96+
}
97+
const endCursorData = cursorToData(args.before);
98+
if (endCursorData) {
99+
filter = connSortOpts.cursorToFilter(endCursorData, filter);
100+
}
101+
findManyParams.args.filter = filter;
102+
103+
findManyParams.args.skip = skip;
104+
// findManyParams.args.sort // TODO
105+
// findManyParams.projection // TODO
106+
107+
let countPromise;
108+
if (projection.count) {
109+
countPromise = countResolver(resolveParams);
110+
}
111+
const findManyPromise = findManyResolver(findManyParams);
112+
const hasPreviousPage = skip > 0;
113+
let hasNextPage = false; // will be requested +1 document, to check next page presence
114+
115+
return findManyPromise
116+
.then(recordList => {
117+
const edges = [];
118+
// if returned more than `limit` records, strip array and mark that exists next page
119+
if (recordList.length > limit) {
120+
hasNextPage = true;
121+
recordList = recordList.slice(0, limit - 1);
122+
}
123+
// transform record to object { cursor, node }
124+
recordList.forEach(record => {
125+
const id = typeComposer.getRecordId(record);
126+
edges.push({
127+
cursor: idToCursor(id),
128+
node: record,
129+
});
130+
});
131+
return edges;
132+
})
133+
.then(async (edges) => {
134+
const result = emptyConnection();
135+
136+
// pass `edge` data
137+
result.edges = edges;
138+
139+
// if exists countPromise, await it's data
140+
if (countPromise) {
141+
result.count = await countPromise;
142+
}
143+
144+
// pageInfo may be extended, so set data gradually
145+
if (edges.length > 0) {
146+
result.pageInfo.startCursor = edges[0].cursor;
147+
result.pageInfo.endCursor = edges[edges.length - 1].cursor;
148+
result.pageInfo.hasPreviousPage = hasPreviousPage;
149+
result.pageInfo.hasNextPage = hasNextPage;
150+
}
151+
152+
return result;
153+
});
154+
},
155+
});
156+
}
157+
158+
export function emptyConnection() {
159+
return {
160+
count: 0,
161+
edges: [],
162+
pageInfo: {
163+
startCursor: null,
164+
endCursor: null,
165+
hasPreviousPage: false,
166+
hasNextPage: false,
167+
},
168+
};
169+
}
170+
171+
export function idToCursor(id: string) {
172+
return id;
173+
}
174+
175+
export function cursorToId(cursor: string) {
176+
return cursor;
177+
}
178+
179+
180+
181+
var PREFIX = 'arrayconnection:';
182+
183+
/**
184+
* Creates the cursor string from an offset.
185+
*/
186+
export function offsetToCursor(offset: number): ConnectionCursor {
187+
return base64(PREFIX + offset);
188+
}
189+
190+
/**
191+
* Rederives the offset from the cursor string.
192+
*/
193+
export function cursorToOffset(cursor: ConnectionCursor): number {
194+
return parseInt(unbase64(cursor).substring(PREFIX.length), 10);
195+
}

src/type/connection.js

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)