|
| 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 | +} |
0 commit comments