UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

1,025 lines 39.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionItemsStep = exports.ConnectionParamsStep = exports.EdgeStep = exports.ConnectionStep = void 0; exports.connection = connection; exports.itemsOrStep = itemsOrStep; const tslib_1 = require("tslib"); const iterall_1 = require("iterall"); const assert = tslib_1.__importStar(require("../assert.js")); const deferred_js_1 = require("../deferred.js"); const withGlobalLayerPlan_js_1 = require("../engine/lib/withGlobalLayerPlan.js"); const step_js_1 = require("../step.js"); const utils_js_1 = require("../utils.js"); const access_js_1 = require("./access.js"); const constant_js_1 = require("./constant.js"); const each_js_1 = require("./each.js"); const first_js_1 = require("./first.js"); const lambda_js_1 = require("./lambda.js"); const last_js_1 = require("./last.js"); const EMPTY_OBJECT = Object.freeze(Object.create(null)); const EMPTY_CONNECTION_RESULT = Object.freeze({ items: Object.freeze([]), hasNextPage: false, hasPreviousPage: false, }); function indexedItem(item, index) { return { index, item }; } /** * Handles GraphQL cursor pagination in a standard and consistent way * indepdenent of data source. */ class ConnectionStep extends step_js_1.Step { static { this.$$export = { moduleName: "grafast", exportName: "ConnectionStep", }; } constructor(subplan, params = {}) { super(); this.isSyncAndSafe = false; this.neededCollection = false; /** * null = unknown */ this._mightStream = null; // Pagination stuff this._firstDepId = null; this._lastDepId = null; this._offsetDepId = null; this._beforeDepId = undefined; this._afterDepId = undefined; /** If the user asks for details of `hasNextPage`/`hasPreviousPage`, then fetch one extra */ this.needsHasMore = false; this.needsCursor = false; this.nodePlan = ($rawItem) => { const $item = this.getUnindexedItem($rawItem); const subplan = this.setupSubplanWithPagination(); if (typeof subplan.nodeForItem === "function") { return subplan.nodeForItem($item); } else if (typeof subplan.listItem === "function") { return subplan.listItem($item); } else { return $item; } }; this.edgePlan = ($rawItem) => { const subplan = this.setupSubplanWithPagination(); if (typeof subplan.edgeForItem === "function") { return subplan.edgeForItem($rawItem); } else { return new EdgeStep(this, $rawItem); } }; if (params.edgeDataPlan) { this.edgeDataPlan = params.edgeDataPlan; } else { this.edgeDataPlan = (defaultEdgeDataPlan); } if ("paginationSupport" in subplan && subplan.paginationSupport != null && (subplan.paginationSupport.full || "applyPagination" in subplan)) { this.collectionPaginationSupport = subplan.paginationSupport; if (this.collectionPaginationSupport.full) { this.paramsDepId = null; } else { const $params = new ConnectionParamsStep(this.collectionPaginationSupport); this.paramsDepId = this.addUnaryDependency($params); subplan.applyPagination($params); } this.collectionDepId = this.addDependency(subplan); } else { const $params = new ConnectionParamsStep(null); this.paramsDepId = this.addUnaryDependency($params); this.collectionPaginationSupport = null; // It's pure, don't change it! this.collectionDepId = this.addDependency(subplan); } } mightStream() { return this._mightStream === true; } getSubplan() { return this.getDepOptions(this.collectionDepId).step; } _getSubplan() { return this.getSubplan(); } /** * This represents a single page from the collection - not only have * conditions and ordering been applied but we've also applied the pagination * constraints (before, after, first, last, offset). It's useful for * returning the actual edges and nodes of the connection. * * This cannot be called before the arguments have been finalized. */ setupSubplanWithPagination() { this.neededCollection = true; return this.getDepOptions(this.collectionDepId).step; } getHandler() { if (this.collectionPaginationSupport?.full) { return this.setupSubplanWithPagination(); } else { return this.paginationParams(); } } toStringMeta() { return String(this.getDepOptions(this.collectionDepId).step.id); } setNeedsHasMore() { this.needsHasMore = true; this.setupSubplanWithPagination(); } getFirst() { return this.maybeGetDep(this._firstDepId); } setFirst(first) { if (this._firstDepId != null) { throw new Error(`${this}->setFirst already called`); } const $first = typeof first === "number" ? (0, constant_js_1.constant)(first) : first; this._firstDepId = this.addUnaryDependency({ step: $first, nonUnaryMessage: () => `${this}.setFirst(...) must be passed a _unary_ step, but ${$first} is not unary. See: https://err.red/gud#connection`, }); this.getHandler().setFirst($first); } getLast() { return this.maybeGetDep(this._lastDepId); } setLast(last) { if (this._lastDepId != null) { throw new Error(`${this}->setLast already called`); } const $last = typeof last === "number" ? (0, constant_js_1.constant)(last) : last; this._lastDepId = this.addUnaryDependency({ step: $last, nonUnaryMessage: () => `${this}.setLast(...) must be passed a _unary_ step, but ${$last} is not unary. See: https://err.red/gud#connection`, }); this.getHandler().setLast($last); } getOffset() { return this.maybeGetDep(this._offsetDepId); } setOffset(offset) { if (this._offsetDepId != null) { throw new Error(`${this}->setOffset already called`); } const $offset = typeof offset === "number" ? (0, constant_js_1.constant)(offset) : offset; this._offsetDepId = this.addUnaryDependency({ step: $offset, nonUnaryMessage: () => `${this}.setOffset(...) must be passed a _unary_ step, but ${$offset} is not unary. See: https://err.red/gud#connection`, }); this.getHandler().setOffset($offset); } getBefore() { return this.maybeGetDep(this._beforeDepId, true); } setBefore($beforePlan) { if ($beforePlan instanceof constant_js_1.ConstantStep && $beforePlan.data == null) { return; } if (this._beforeDepId !== undefined) { throw new Error(`${this}->setBefore already called`); } const $parsedBeforePlan = this._getSubplan().parseCursor?.($beforePlan) ?? $beforePlan; this._beforeDepId = this.addUnaryDependency({ step: $parsedBeforePlan, nonUnaryMessage: () => `${this}.setBefore(...) must be passed a _unary_ step, but ${$parsedBeforePlan} (and presumably ${$beforePlan}) is not unary. See: https://err.red/gud#connection`, }); this.getHandler().setBefore($parsedBeforePlan); } getAfter() { // TODO: Move all of these to params, get rid of our dep here. return this.maybeGetDep(this._afterDepId, true); } setAfter($afterPlan) { if ($afterPlan instanceof constant_js_1.ConstantStep && $afterPlan.data == null) { return; } if (this._afterDepId !== undefined) { throw new Error(`${this}->setAfter already called`); } const $parsedAfterPlan = this._getSubplan().parseCursor?.($afterPlan) ?? $afterPlan; this._afterDepId = this.addUnaryDependency({ step: $parsedAfterPlan, nonUnaryMessage: () => `${this}.setAfter(...) must be passed a _unary_ step, but ${$parsedAfterPlan} (and presumably ${$afterPlan}) is not unary. See: https://err.red/gud#connection`, }); this.getHandler().setAfter($parsedAfterPlan); } /** * This represents the entire collection with conditions and ordering * applied, but without any pagination constraints (before, after, first, * last, offset) applied. It's useful for the following: * * - performing aggregates e.g. totalCount across the entire collection * - determining fields for pageInfo, e.g. is there a next/previous page * * This cannot be called before the arguments have been finalized. */ cloneSubplanWithoutPagination(...args) { if (!this.isArgumentsFinalized) { throw new Error("Forbidden to call ConnectionStep.nodes before arguments finalize"); } const plan = this._getSubplan(); if (typeof plan.connectionClone !== "function") { throw new Error(`${plan} does not support cloning the subplan`); } const clonedPlan = plan.connectionClone(...args); return clonedPlan; } paginationParams() { if (this.collectionPaginationSupport?.full) { throw new Error(`Full pagination support does not generate params`); } return this.getDepOptions(this.paramsDepId) .step; } /** * Subplans may call this from their `setBefore`/`setAfter`/etc plans in order * to add a dependency to us, which is typically useful for adding validation * functions so that they are thrown "earlier", avoiding error bubbling. */ addValidation(callback) { this.withMyLayerPlan(() => { this.addDependency(callback()); }); } get(fieldName) { switch (fieldName) { case "edges": return this.edges(); case "nodes": return this.nodes(); case "pageInfo": return this.pageInfo(); default: return (0, constant_js_1.constant)(undefined); } } listItem($rawItem) { return this.nodePlan($rawItem); } getUnindexedItem($rawItem) { const $item = this.collectionPaginationSupport?.cursor ? $rawItem : (0, access_js_1.access)($rawItem, "item"); return $item; } captureStream() { const $streamDetails = (0, withGlobalLayerPlan_js_1.currentFieldStreamDetails)(); if ($streamDetails === null || $streamDetails === true) { this.getHandler().addStreamDetails?.(null); this._mightStream = false; } else { this.getHandler().addStreamDetails?.($streamDetails); if (this._mightStream === null) { // Only override if it was unknown this._mightStream = true; } } } edges() { this.captureStream(); this.setupSubplanWithPagination(); return (0, each_js_1.each)(this._items(), this.edgePlan); } nodes() { this.captureStream(); this.setupSubplanWithPagination(); return (0, each_js_1.each)(this._items(), this.nodePlan); } items() { return this.nodes(); } /** @internal */ _items() { return this.withMyLayerPlan(() => this.operationPlan.cacheStep(this, "_items", null, () => new ConnectionItemsStep(this))); } cursorPlan($rawItem) { this.needsCursor = true; const subplan = this._getSubplan(); if (this.collectionPaginationSupport?.cursor || this.collectionPaginationSupport?.full) { const $item = $rawItem; return subplan.cursorForItem($item); } else { const $indexed = $rawItem; // We're doing cursors, which also means we're NOT doing reverse. // NOTE: there won't be a needsHasMore causing an extra row fetch when // doing numeric pagination backwards - we know there's another record if // we start > 0 const $leftPad = (0, access_js_1.access)(this.paginationParams(), "__skipOver"); const $offset = (0, access_js_1.access)(this.paginationParams(), "offset", 0); const $index = (0, access_js_1.access)($indexed, "index"); return (0, lambda_js_1.lambda)([$leftPad, $offset, $index], encodeNumericCursor); } } pageInfo() { return new PageInfoStep(this); } optimize() { if (!this.neededCollection) { return (0, constant_js_1.constant)(EMPTY_CONNECTION_RESULT); } if (this.needsHasMore) { this.getHandler().setNeedsHasMore(); } /* * **IMPORTANT**: no matter the arguments, we cannot optimize ourself away * by replacing ourself with a constant because otherwise errors in * cursors/etc will be pushed down a level. */ return this; } deduplicatedWith(replacement) { if (this.neededCollection) { replacement.neededCollection = true; } if (this.needsCursor) { replacement.needsCursor = true; } if (this.needsHasMore) { replacement.needsHasMore = true; } } execute({ values, indexMap, }) { if (!this.neededCollection) { // The main collection is not actually fetched, so we don't need to do // any pagination stuff. Could be they just wanted `totalCount` for // example. return indexMap(() => EMPTY_CONNECTION_RESULT); } const collectionDep = values[this.collectionDepId]; const params = this.paramsDepId != null ? values[this.paramsDepId].unaryValue() : null; return indexMap((i) => { const collectionValue = collectionDep.at(i); if (params === null) { // collection handles everything const result = collectionValue; if (!this._mightStream && result.items != null && !Array.isArray(result.items)) { // Except it might return a stream which we should turn into an array. return (async () => ({ ...result, items: await iterableToArray(result.items), }))(); } else { return result; } } // The value is either a list of items, or an object that contains the // `items` key. let mode = MODE_NULL; let collection = null; if (collectionValue == null) { mode = MODE_NULL; } else if (Array.isArray(collectionValue)) { collection = collectionValue; mode = MODE_ARRAY; } else if ((0, iterall_1.isIterable)(collectionValue)) { collection = collectionValue; // [Symbol.iterator](); mode = MODE_ITERABLE; } else if ((0, iterall_1.isAsyncIterable)(collectionValue)) { collection = collectionValue; //[Symbol.asyncIterator](); mode = MODE_ASYNC_ITERABLE; } else if (typeof collectionValue === "object" && "items" in collectionValue) { const items = collectionValue.items; if (items == null) { mode = MODE_NULL; } else if (Array.isArray(items)) { collection = items; mode = MODE_ARRAY; } else if ((0, iterall_1.isIterable)(items)) { collection = items; // [Symbol.iterator](); mode = MODE_ITERABLE; } else if ((0, iterall_1.isAsyncIterable)(items)) { collection = items; // [Symbol.asyncIterator](); mode = MODE_ASYNC_ITERABLE; } else { // WTF? mode = MODE_NULL; } } else { // WTF? mode = MODE_NULL; } if (mode === MODE_NULL || collection === null) { return null; } if (this._mightStream !== true && (mode === MODE_ITERABLE || mode === MODE_ASYNC_ITERABLE)) { // Convert to array, then process return iterableToArray(collection).then((array) => makeProcessedCollection(MODE_ARRAY, array, params, !this.collectionPaginationSupport?.cursor)); } else { return makeProcessedCollection(mode, collection, params, !this.collectionPaginationSupport?.cursor); } }); } } exports.ConnectionStep = ConnectionStep; const MODE_NULL = 0; const MODE_ARRAY = 1; const MODE_ITERABLE = 2; const MODE_ASYNC_ITERABLE = 3; function makeProcessedCollection(mode, collection, params, wrapIndicies) { const { __isForwardPagination, __hasMore, __skipOver, __limit } = params; // If we don't need to do anything, return the underlying collection directly if (__skipOver === 0 && __limit === null && typeof __hasMore === "boolean" && (!wrapIndicies || Array.isArray(collection))) { const hasNextPage = __isForwardPagination ? __hasMore : false; const hasPreviousPage = __isForwardPagination ? __hasMore : false; const items = wrapIndicies ? collection.map(indexedItem) : collection; return { items, hasNextPage, hasPreviousPage }; } let hasNext; let items; if (mode === MODE_ARRAY) { let array = collection; if (__skipOver > 0) { array = array.slice(__skipOver); } if (__limit != null) { array = array.slice(0, __limit); } if (typeof __hasMore === "boolean") { hasNext = __hasMore; } else { const [side, limit] = __hasMore; if (side === "l") { if (array.length > limit) { hasNext = true; array = array.slice(array.length - limit); } else { hasNext = false; } } else { if (array.length > limit) { hasNext = true; array = array.slice(0, limit); } else { hasNext = false; } } } items = wrapIndicies ? array.map(indexedItem) : array; } else { const deferredHasNext = typeof __hasMore === "boolean" ? null : (0, deferred_js_1.defer)(); hasNext = deferredHasNext ?? __hasMore; let didHaveNext = false; let started = false; items = (0, utils_js_1.asyncIteratorWithCleanup)((async function* () { let skip = __skipOver; let index = 0; const accumulator = typeof __hasMore !== "boolean" && __hasMore[0] === "l" ? [] : null; started = true; for await (const item of collection) { if (skip > 0) { --skip; } else if (__limit === null || index < __limit) { if (typeof __hasMore === "boolean") { yield wrapIndicies ? { index, item } : item; } else { const limit = __hasMore[1]; if (accumulator /* same as __hasMore[0] === "l" */) { // DO NOT USE `index` IN THIS BRANCH // eslint-disable-next-line @typescript-eslint/no-unused-vars const index = null; if (accumulator.length < limit) { // All good } else { // Drop one! accumulator.shift(); didHaveNext = true; } accumulator.push(item); } else { if (index < limit) { yield wrapIndicies ? { index, item } : item; } else { didHaveNext = true; break; } } } index++; } else { break; } } if (accumulator) { index = 0; for (const item of accumulator) { yield wrapIndicies ? { index, item } : item; ++index; } } })(), () => { if (deferredHasNext) { deferredHasNext.resolve(didHaveNext); } if (!started) { (0, utils_js_1.terminateIterable)(collection); } }); } const hasNextPage = __isForwardPagination ? hasNext : false; const hasPreviousPage = __isForwardPagination ? false : hasNext; return { hasNextPage, hasPreviousPage, items, }; } class EdgeStep extends step_js_1.UnbatchedStep { static { this.$$export = { moduleName: "grafast", exportName: "EdgeStep", }; } constructor($connection, $rawItem) { super(); this.isSyncAndSafe = true; this.cursorDepId = null; const itemDepId = this.addDependency($rawItem); assert.strictEqual(itemDepId, 0, "GrafastInternalError<89cc75cd-ccaf-4b7e-873f-a629c36d55f7>: item must be first dependency"); this.connectionRefId = this.addRef($connection); } get(fieldName) { switch (fieldName) { case "node": return this.node(); case "cursor": return this.cursor(); case "data": return this.data(); default: return (0, constant_js_1.constant)(undefined); } } getConnectionStep() { return this.getRef(this.connectionRefId); } getRawItemStep() { // We know we're not using flags return this.getDepOptions(0).step; } getItemStep() { const $rawItem = this.getRawItemStep(); return this.getConnectionStep().getUnindexedItem($rawItem); } data() { const $connection = this.getConnectionStep(); const $item = this.getItemStep(); return $connection.edgeDataPlan($item); } node() { const $rawItem = this.getRawItemStep(); return this.getConnectionStep().nodePlan($rawItem); } cursor() { if (this.cursorDepId == null) { const $connection = this.getConnectionStep(); const $rawItem = this.getRawItemStep(); const $cursor = this.withMyLayerPlan(() => $connection.cursorPlan($rawItem)); this.cursorDepId = this.addDependency($cursor); if (this.cursorDepId !== 1) { throw new Error(`Expected cursor to have depId = 1, but instead it got depId ${this.cursorDepId}`); } return $cursor; } else { return this.getDepOptions(this.cursorDepId).step; } } deduplicate(_peers) { return _peers; } unbatchedExecute(_extra, record, cursor) { // Handle nulls; everything else comes from the child plans return record == null || (this.cursorDepId === 1 && cursor == null) ? null : EMPTY_OBJECT; } } exports.EdgeStep = EdgeStep; /** * Wraps a collection fetch to provide the utilities for working with GraphQL * cursor connections. */ function connection(step, params) { if (typeof params === "function" || params?.nodePlan || params?.cursorPlan) { throw new Error(`connection() was completely overhauled during the beta; this usage is no longer supported. Usage is much more straightforward now.`); } const $connection = new ConnectionStep(step, params); const fieldArgs = params?.fieldArgs; if (fieldArgs) { const { $first, $last, $before, $after, $offset } = fieldArgs; // Connections may have a mixture of these arguments, so we must check each exists if ($first) $connection.setFirst($first); if ($last) $connection.setLast($last); if ($before) $connection.setBefore($before); if ($after) $connection.setAfter($after); if ($offset) $connection.setOffset($offset); } return $connection; } function itemsOrStep($step) { return "items" in $step && typeof $step.items === "function" ? $step.items() : $step; } function encodeNumericCursor(index) { const cursor = typeof index === "number" ? index : index.reduce((memo, n) => memo + n, 0); return Buffer.from(String(cursor), "utf8").toString("base64"); } function decodeNumericCursor(cursor) { const i = parseInt(Buffer.from(cursor, "base64").toString("utf8"), 10); if (!Number.isSafeInteger(i) || i < 0) { throw new Error(`Invalid cursor`); } return i; } class ConnectionParamsStep extends step_js_1.UnbatchedStep { static { this.$$export = { moduleName: "grafast", exportName: "ConnectionParamsStep", }; } constructor(paginationSupport) { super(); this.paginationSupport = paginationSupport; /** sync and safe because it's unary; an error thrown for one is thrown for all */ this.isSyncAndSafe = true; this.needsHasMore = false; // Pagination stuff this.firstDepId = null; this.lastDepId = null; this.offsetDepId = null; this.beforeDepId = null; this.afterDepId = null; this.streamDetailsDepIds = []; } setFirst($first) { if (this.firstDepId != null) throw new Error(`first already set`); this.firstDepId = this.addUnaryDependency($first); } setLast($last) { if (this.lastDepId != null) throw new Error(`last already set`); this.lastDepId = this.addUnaryDependency($last); } setOffset($offset) { if (this.offsetDepId != null) throw new Error(`offset already set`); this.offsetDepId = this.addUnaryDependency($offset); } setBefore($before) { if (this.beforeDepId != null) throw new Error(`before already set`); this.beforeDepId = this.addUnaryDependency($before); } setAfter($after) { if (this.afterDepId != null) throw new Error(`after already set`); this.afterDepId = this.addUnaryDependency($after); } setNeedsHasMore() { this.needsHasMore = true; } addStreamDetails($details) { if ($details) { this.streamDetailsDepIds?.push(this.addUnaryDependency($details)); } else { // Explicitly disable streaming this.streamDetailsDepIds = null; } } /** * True if it's possible this'll stream, false if we've not * been told anything about streaming. */ mightStream() { return (this.streamDetailsDepIds != null && this.streamDetailsDepIds.length > 0); } deduplicate(peers) { return peers.filter((p) => p.paginationSupport === this.paginationSupport && p.firstDepId === this.firstDepId && p.afterDepId === this.afterDepId && p.offsetDepId === this.offsetDepId && p.beforeDepId === this.beforeDepId && p.afterDepId === this.afterDepId && (0, utils_js_1.maybeArraysMatch)(p.streamDetailsDepIds, this.streamDetailsDepIds)); } deduplicatedWith(replacement) { if (this.needsHasMore) { replacement.needsHasMore = true; } } unbatchedExecute(extra, ...values) { /** If we should stream, set this (to 0 or more), otherwise leave it null */ let initialCount = null; if (this.streamDetailsDepIds != null) { for (const depId of this.streamDetailsDepIds) { const v = values[depId]; if (v != null) { const c = v.initialCount ?? 0; if (initialCount === null || c > initialCount) { initialCount = c; } } } } const stream = initialCount != null ? { initialCount } : null; const first = this.firstDepId != null ? values[this.firstDepId] : null; const last = this.lastDepId != null ? values[this.lastDepId] : null; const offset = this.offsetDepId != null ? values[this.offsetDepId] : null; const before = this.beforeDepId != null ? values[this.beforeDepId] : null; const after = this.afterDepId != null ? values[this.afterDepId] : null; const { needsHasMore } = this; const { reverse: supportsReverse, offset: supportsOffset, cursor: supportsCursor, } = this.paginationSupport ?? {}; const supportsLimit = this.paginationSupport != null; /** How many records do we need to skip over in addition to a limit implied by `first`/`last`? */ const params = { __skipOver: 0, __isForwardPagination: true, __hasMore: false, __limit: null, limit: null, offset: null, after: null, reverse: false, stream, }; const isForwardPagination = first != null || after != null || (before == null && last == null); if (first != null && (!Number.isSafeInteger(first) || first < 0)) { throw new Error("Invalid 'first'"); } if (last != null && (!Number.isSafeInteger(last) || last < 0)) { throw new Error("Invalid 'last'"); } // Setting boht first and last makes queries nonsensical. // https://relay.dev/graphql/connections.htm#note-95f8a if (first != null && before != null) { throw new Error("You must not set both `first` and `before`; specify `first` and optionally `after` to paginate forwards, or `last` and optionally `before` to paginate backwards."); } if (last != null && after != null) { throw new Error("You must not set `last` and `after`; specify `first` and optionally `after` to paginate forwards, or `last` and optionally `before` to paginate backwards; "); } if (first != null && last != null) { throw new Error("It is not permitted to set both 'first' and 'last' in the same field."); } if (!isForwardPagination && offset != null) { throw new Error(`May not combine 'offset' with backward pagination.`); } params.__isForwardPagination = isForwardPagination; if (isForwardPagination) { // Forwards: after, offset, first if (supportsCursor) { // Limit definitely supported if (after != null) { params.after = after; } if (offset != null) { if (supportsOffset) { params.offset = (params.offset ?? 0) + offset; } else { params.__skipOver += offset; } } if (first != null) { params.limit = params.__skipOver + first + (needsHasMore ? 1 : 0); params.__hasMore = needsHasMore ? ["r", first] : false; } else { // Just fetch the lot params.__hasMore = false; } } else { // Collection doesn't support cursors; we must be using numeric cursors let initialIndex = 0; if (after != null) { initialIndex = decodeNumericCursor(after) + 1; } if (offset != null) { initialIndex += offset; } let beforeIndex = Infinity; if (first != null) { beforeIndex = initialIndex + first; } if (supportsOffset) { params.offset = initialIndex; } else { params.__skipOver = initialIndex; } if (Number.isFinite(beforeIndex)) { const max = beforeIndex - initialIndex; if (supportsLimit) { params.limit = params.__skipOver + max + (needsHasMore ? 1 : 0); params.__hasMore = needsHasMore ? ["r", max] : false; } else { // We're using __hasMore as a hack to apply our limit params.__hasMore = ["r", max]; } } else { // There is no limit params.__hasMore = false; } } } else { // Backwards: before, last if (supportsReverse) { if (!supportsCursor) { throw new Error("If reverse pagination is supported, cursor pagination must also be supported."); } params.reverse = true; if (before != null) { params.after = before; } if (last != null) { params.limit = last + (needsHasMore ? 1 : 0); params.__hasMore = needsHasMore ? ["l", last] : false; } else { // Just fetch the lot params.__hasMore = false; } } else { if (supportsCursor) { throw new Error("This cursor-supporting collection does not support reverse pagination."); } // Implement numeric cursor pagination if (before != null) { const beforeIndex = decodeNumericCursor(before); let initialIndex = 0; if (last != null) { initialIndex = Math.max(0, beforeIndex - last); } if (supportsOffset) { params.offset = initialIndex; } else { params.__skipOver = initialIndex; } const max = beforeIndex - initialIndex; if (supportsLimit) { params.limit = params.__skipOver + max; } else { params.__limit = max; } // In this special case, hasNext is simply whether we started // fetching > 0 or not params.__hasMore = initialIndex > 0; } else if (last != null) { //params.__retain = last + (needsHasMore ? 1 : 0); //params.__hasMore = needsHasMore ? ["l", last] : false; // Hackily use the __hasMore retain mechanism to trim the collection params.__hasMore = ["l", last]; } else { throw new Error(`GrafastInternalError<62c60e68-2925-41a7-837c-674234acab1b>: This code should be unreachable`); } } } return params; } } exports.ConnectionParamsStep = ConnectionParamsStep; class PageInfoStep extends step_js_1.UnbatchedStep { static { this.$$export = { moduleName: "grafast", exportName: "PageInfoStep", }; } constructor($connection) { super(); this.isSyncAndSafe = true; this.addDependency($connection); } get(key) { const $connection = this.getDepOptions(0).step; switch (key) { case "hasNextPage": { $connection.setNeedsHasMore(); return (0, access_js_1.access)($connection, "hasNextPage"); } case "hasPreviousPage": { $connection.setNeedsHasMore(); return (0, access_js_1.access)($connection, "hasPreviousPage"); } case "startCursor": { // Get first node, get cursor for it const isArray = !$connection.mightStream(); const $first = (0, first_js_1.first)($connection._items(), isArray); return $connection.cursorPlan($first); } case "endCursor": { // Get last node, get cursor for it const isArray = !$connection.mightStream(); const $last = (0, last_js_1.last)($connection._items(), isArray); return $connection.cursorPlan($last); } default: { // TODO: allow expansion return (0, constant_js_1.constant)(undefined); } } } unbatchedExecute(_extra, _connection) { return _connection; } } class ConnectionItemsStep extends step_js_1.Step { static { this.$$export = { moduleName: "grafast", exportName: "ConnectionItemsStep", }; } constructor($connection) { super(); this.isSyncAndSafe = false; this.cloneStreams = true; this.addStrongDependency($connection); } getConnection() { return this.getDepOptions(0).step; } optimize(_options) { // If false, connection guarantees an array this.cloneStreams = this.getConnection().mightStream(); return this; } deduplicate(_peers) { return _peers; } execute(executionDetails) { const connection = executionDetails.values[0]; return executionDetails.indexMap((i) => connection.at(i)?.items); } } exports.ConnectionItemsStep = ConnectionItemsStep; async function iterableToArray(input) { const items = []; for await (const item of input) { items.push(item); } return items; } function defaultEdgeDataPlan(i) { return i; } //# sourceMappingURL=connection.js.map