grafast
Version:
Cutting edge GraphQL planning and execution engine
372 lines (370 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EdgeStep = exports.ConnectionStep = void 0;
exports.assertPageInfoCapableStep = assertPageInfoCapableStep;
exports.assertEdgeCapableStep = assertEdgeCapableStep;
exports.connection = connection;
exports.itemsOrStep = itemsOrStep;
const tslib_1 = require("tslib");
const assert = tslib_1.__importStar(require("../assert.js"));
const step_js_1 = require("../step.js");
const utils_js_1 = require("../utils.js");
const constant_js_1 = require("./constant.js");
const each_js_1 = require("./each.js");
function assertPageInfoCapableStep($step) {
const $typed = $step;
if (typeof $typed.hasNextPage !== "function" ||
typeof $typed.hasPreviousPage !== "function" ||
typeof $typed.startCursor !== "function" ||
typeof $typed.endCursor !== "function") {
throw new Error(`Expected a PageInfoCapableStep, but found '${$step}'`);
}
}
const EMPTY_OBJECT = Object.freeze(Object.create(null));
/**
* Handles GraphQL cursor pagination in a standard and consistent way
* indepdenent of data source.
*/
class ConnectionStep extends step_js_1.UnbatchedStep {
static { this.$$export = {
moduleName: "grafast",
exportName: "ConnectionStep",
}; }
// TYPES: if subplan is `ConnectionCapableStep<EdgeCapableStep<any>>` then `nodePlan`/`cursorPlan` aren't needed; otherwise `cursorPlan` is required.
constructor(subplan, config = {}) {
super();
this.isSyncAndSafe = true;
// Pagination stuff
this._firstDepId = null;
this._lastDepId = null;
this._offsetDepId = null;
this._beforeDepId = undefined;
this._afterDepId = undefined;
const { edgeDataPlan, nodePlan, cursorPlan } = config;
this.edgeDataPlan = edgeDataPlan;
this.itemPlan = nodePlan;
this.cursorPlan = cursorPlan;
if (!cursorPlan) {
// ENHANCE: Assert that the `nodePlan` has a `.cursor()` method.
}
// This is a _soft_ reference to the plan; we're not adding it as a
// dependency since we do not actually need it to execute; it's our
// children that need access to it.
this.subplanId = subplan.id;
}
toStringMeta() {
return String(this.subplanId);
}
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`,
});
}
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`,
});
}
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`,
});
}
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);
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`,
});
}
getAfter() {
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);
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 should not be called after the arguments have been finalized.
*/
getSubplan() {
if (this.isArgumentsFinalized) {
throw new Error("Forbidden to call ConnectionStep.getSubplan after arguments finalize");
}
const plan = this.getStep(this.subplanId);
return plan;
}
/**
* 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.getStep(this.subplanId);
const clonedPlan = plan.connectionClone(this, ...args);
return clonedPlan;
}
/**
* 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.
*/
cloneSubplanWithPagination(
// TYPES: ugh. The `|[]` shouldn't be needed.
...args) {
const clonedPlan = this.cloneSubplanWithoutPagination(...args);
{
const plan = this.getBefore();
if (plan !== null) {
clonedPlan.setBefore(plan);
}
}
{
const plan = this.getAfter();
if (plan !== null) {
clonedPlan.setAfter(plan);
}
}
{
const plan = this.getFirst();
if (plan !== null) {
clonedPlan.setFirst(plan);
}
}
{
const plan = this.getLast();
if (plan !== null) {
clonedPlan.setLast(plan);
}
}
{
const plan = this.getOffset();
if (plan !== null) {
clonedPlan.setOffset(plan);
}
}
return clonedPlan;
}
/**
* 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);
}
}
edges() {
if (this.cursorPlan || this.itemPlan || this.edgeDataPlan) {
return (0, each_js_1.each)(this.cloneSubplanWithPagination(), ($intermediate) => this.wrapEdge($intermediate));
}
else {
// Assuming the subplan is an EdgeCapableStep
return this.cloneSubplanWithPagination();
}
}
nodes() {
if (this.itemPlan !== undefined) {
return (0, each_js_1.each)(this.cloneSubplanWithPagination(), ($intermediate) => this.itemPlan($intermediate));
}
else {
return this.cloneSubplanWithPagination();
}
}
wrapEdge($edge) {
return new EdgeStep(this, $edge);
}
pageInfo() {
return this.cloneSubplanWithPagination().pageInfo(this);
}
/*
**IMPORTANT**: we cannot optimize this by replacing ourself with a constant
because otherwise errors in cursors/etc will be pushed down a level.
public optimize() {
return constant(EMPTY_OBJECT, false);
}
*/
execute({ count, }) {
// Fake execution; data actually comes from the child plans
return (0, utils_js_1.arrayOfLength)(count, EMPTY_OBJECT);
}
unbatchedExecute() {
return EMPTY_OBJECT;
}
}
exports.ConnectionStep = ConnectionStep;
function assertEdgeCapableStep($step) {
const $typed = $step;
if (typeof $typed.node !== "function" ||
typeof $typed.cursor !== "function") {
throw new Error(`Expected a EdgeCapableStep, but found '${$step}'`);
}
}
class EdgeStep extends step_js_1.UnbatchedStep {
static { this.$$export = {
moduleName: "grafast",
exportName: "EdgeStep",
}; }
constructor($connection, $item, skipCursor = false) {
super();
this.skipCursor = skipCursor;
this.isSyncAndSafe = true;
this.needCursor = false;
const itemDepId = this.addDependency($item);
assert.strictEqual(itemDepId, 0, "GrafastInternalError<89cc75cd-ccaf-4b7e-873f-a629c36d55f7>: item must be first dependency");
if (!skipCursor) {
const $cursor = $connection.cursorPlan?.($item) ??
$item.cursor?.();
if (!$cursor) {
throw new Error(`No cursor plan known for '${$item}'`);
}
this.cursorDepId = this.addDependency($cursor);
assert.strictEqual(this.cursorDepId, 1, "GrafastInternalError<46e4b5ca-0c11-4737-973d-0edd0be060c9>: cursor must be second dependency");
}
else {
this.cursorDepId = null;
}
this.connectionDepId = this.addDependency($connection);
}
get(fieldName) {
switch (fieldName) {
case "node":
return this.node();
case "cursor":
return this.cursor();
default:
return (0, constant_js_1.constant)(undefined);
}
}
getConnectionStep() {
return this.getDep(this.connectionDepId);
}
getItemStep() {
return this.getDep(0);
}
data() {
const $item = this.getItemStep();
return this.getConnectionStep().edgeDataPlan?.($item) ?? $item;
}
node() {
const $item = this.getItemStep();
return this.getConnectionStep().itemPlan?.($item) ?? $item;
}
cursor() {
this.needCursor = true;
return this.getDep(this.cursorDepId);
}
optimize() {
if (!this.needCursor && this.cursorDepId !== null) {
return new EdgeStep(this.getConnectionStep(), this.getItemStep(), true);
}
return this;
}
deduplicate(_peers) {
return _peers;
}
deduplicatedWith(replacement) {
if (this.needCursor) {
replacement.needCursor = true;
}
}
unbatchedExecute(_extra, record, cursor) {
// Handle nulls; everything else comes from the child plans
return record == null && (this.cursorDepId == null || cursor == null)
? null
: EMPTY_OBJECT;
}
}
exports.EdgeStep = EdgeStep;
let warned = false;
/**
* Wraps a collection fetch to provide the utilities for working with GraphQL
* cursor connections.
*/
function connection(step, config) {
if (typeof config === "function") {
if (!warned) {
warned = true;
console.warn(`The call signature for connection() has changed, arguments after the first argument should be specified via a config object`);
}
return connection(step, {
// eslint-disable-next-line prefer-rest-params
nodePlan: arguments[1],
// eslint-disable-next-line prefer-rest-params
cursorPlan: arguments[2],
});
}
return new ConnectionStep(step, config);
}
function itemsOrStep($step) {
return "items" in $step && typeof $step.items === "function"
? $step.items()
: $step;
}
//# sourceMappingURL=connection.js.map