grafast
Version:
Cutting edge GraphQL planning and execution engine
1,025 lines • 39.5 kB
JavaScript
"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