diff --git a/packages/supermassive/scripts/install-graphql-17.js b/packages/supermassive/scripts/install-graphql-17.js index 18c23ecbf..3ed9b9733 100644 --- a/packages/supermassive/scripts/install-graphql-17.js +++ b/packages/supermassive/scripts/install-graphql-17.js @@ -13,7 +13,7 @@ async function main() { const packageJsonData = await fs.readFile(packageJsonPath, "utf-8"); const { stderr } = await exec( - `yarn add graphql@17.0.0-alpha.2 --exact --dev --no-lockfile`, + `yarn add graphql@17.0.0-alpha.7 --exact --dev --no-lockfile`, ); console.log(stderr); diff --git a/packages/supermassive/src/IncrementalDataManager.ts b/packages/supermassive/src/IncrementalDataManager.ts new file mode 100644 index 000000000..254255783 --- /dev/null +++ b/packages/supermassive/src/IncrementalDataManager.ts @@ -0,0 +1,365 @@ +import { Path } from "graphql/jsutils/Path"; +import { DeferUsage, GroupedFieldSet } from "./collectFields"; +import { + PendingResult, + IncrementalResult, + IncrementalDeferResult, + IncrementalStreamResult, + CompletedResult, +} from "./types"; +import { pathToArray } from "./jsutils/Path"; +import { PromiseOrValue } from "./jsutils/PromiseOrValue"; +import { GraphQLError } from "graphql"; +import { ExecutionContext } from "./executeWithoutSchema"; +import { ObjMap } from "./jsutils/ObjMap"; + +export class DeferredExecutionManager { + private _lastId = 0; + private _records: Map = new Map(); + + public hasRecords(): boolean { + return this._records.size > 0; + } + + public addDeferRecord({ + label, + path, + parentChunkId, + returnTypeName, + parentResult, + groupedFieldSet, + nestedDefers, + }: { + label: string | null; + path: Path | undefined; + parentChunkId: string | null; + returnTypeName: string; + parentResult: unknown; + groupedFieldSet: GroupedFieldSet; + nestedDefers: ReadonlyArray; + }) { + const id = this._lastId.toString(); + this._lastId++; + this._records.set(id, { + id, + label, + status: "pending", + type: "defer", + parentChunkId, + path, + returnTypeName, + parentResult, + groupedFieldSet, + nestedDefers, + errors: [], + }); + } + + // public addAsyncIteratorStreamRecord({ + // label, + // path, + // parentChunkId, + // asyncIterator, + // }: { + // label: string | null; + // path: Path | undefined; + // parentChunkId: string | null; + // asyncIterator: AsyncIterator>; + // }) { + // const id = this._lastId.toString(); + // this._lastId++; + // this._records.set(id, { + // id, + // label, + // status: "promised", + // type: "asyncStream", + // parentChunkId, + // path, + // asyncIterator, + // errors: [], + // items: [], + // }); + // } + + // public addItemsStreamRecord({ + // label, + // path, + // parentChunkId, + // promise, + // }: { + // label: string | null; + // path: Path | undefined; + // parentChunkId: string | null; + // promise: Promise>; + // }) { + // const id = this._lastId.toString(); + // this._lastId++; + // this._records.set(id, { + // id, + // label, + // status: "pending", + // type: "stream", + // parentChunkId, + // path, + // errors: [], + // promise: Promise.resolve(promise), + // items: [], + // }); + // } + + public hasNext(): boolean { + for (const record of this._records.values()) { + if (record.status !== "complete") { + return true; + } + } + return false; + } + + private pendingRecords(): Array { + return Array.from(this._records.values()).filter( + (chunk) => chunk.status === "pending", + // || (chunk.type === "asyncStream" && chunk.status !== "complete"), + ); + } + + public getPending(): ReadonlyArray { + return this.pendingRecords().map(({ id, path, label }) => { + const pending: PendingResult = { id, path: pathToArray(path) }; + if (label != null) { + pending.label = label; + } + return pending; + }); + } + + public executePending( + exeContext: ExecutionContext, + executor: ( + exeContext: ExecutionContext, + parentTypeName: string, + sourceValue: unknown, + path: Path | undefined, + groupedFieldSet: GroupedFieldSet, + deferredChunkId: string | null, + ) => PromiseOrValue>, + ) { + this.pendingRecords().map((record) => { + if (record.type == "defer") { + for (const { + label, + groupedFieldSet, + nestedDefers, + } of record.nestedDefers) { + this.addDeferRecord({ + label: label || null, + path: record.path, + parentChunkId: record.id, + returnTypeName: record.returnTypeName, + parentResult: record.parentResult, + groupedFieldSet, + nestedDefers, + }); + } + record.status = "promised"; + record.promise = Promise.resolve( + executor( + exeContext, + record.returnTypeName, + record.parentResult, + record.path, + record.groupedFieldSet, + record.id, + ), + ).then( + (result) => { + record.status = "complete"; + this.addResult(record.id, result); + return result as unknown; + }, + (error) => { + record.status = "complete"; + this.addError(record.id, error); + return null; + }, + ); + return record.promise; + } + // else if (record.type === "stream") { + // record.status = "promised"; + // record.promise.then( + // (result) => { + // record.status = "complete"; + // record.items = result; + // return result as unknown; + // }, + // (error) => { + // record.status = "complete"; + // this.addError(record.id, error); + // return null; + // }, + // ); + // } else { + // return record.asyncIterator.next(); + // } + }); + } + + public async waitForNext(): Promise { + return Promise.race( + Array.from(this._records.values()) + .filter(({ status }) => status === "promised") + .map((chunk) => { + if ( + (chunk.type === "defer" || chunk.type === "stream") && + chunk.promise + ) { + return chunk.promise; + } else { + return Promise.resolve(); + } + }), + ); + } + + public drainCompleted(): [ + Array, ObjMap>>, + Array, + ] { + const results: Array, ObjMap>> = + []; + const completeds: Array = []; + for (const chunk of this._records.values()) { + if (chunk.type === "defer" && chunk.status === "complete") { + this._records.delete(chunk.id); + const completed: CompletedResult = { + id: chunk.id, + }; + if (chunk.result != null) { + const result: IncrementalDeferResult = { + id: chunk.id, + data: chunk.result, + // Subpath only needed for parital deliveries + // subPath: pathToArray(chunk.path), + }; + + if (chunk.errors.length > 0) { + result.errors = chunk.errors; + } + results.push(result); + } else { + completed.errors = chunk.errors; + } + completeds.push(completed); + } + // else if (chunk.type === "stream") { + // if (chunk.status === "complete") { + // const completed: CompletedResult = { + // id: chunk.id, + // }; + // if (chunk.errors && chunk.items.length === 0) { + // completed.errors = chunk.errors; + // } + // completeds.push(completed); + // } + // if (chunk.items.length > 0) { + // const result: IncrementalStreamResult> = { + // id: chunk.id, + // items: chunk.items as Array>, + // // Subpath only needed for parital deliveries + // // subPath: pathToArray(chunk.path), + // }; + // if (chunk.errors.length > 0) { + // result.errors = chunk.errors; + // } + // results.push(result); + // } + // } + } + return [results, completeds]; + } + + private addResult(id: string, result: ObjMap) { + const record = this._records.get(id); + if (record) { + if (record.type === "defer") { + record.result = result; + } + // } else if (record.type === "stream") { + // record.items.push(result); + // } + } + } + + public addError(id: string, error: GraphQLError) { + const record = this._records.get(id); + if (record) { + record.errors.push(error); + } + } + + public removeSubsequentPayloads( + path: Path, + failingChunkId: string | null, + ): void { + const pathArray = pathToArray(path); + for (const record of this._records.values()) { + if (record.id === failingChunkId) { + continue; + } + const recordPath = pathToArray(record.path); + for (let i = 0; i < pathArray.length; i++) { + if (recordPath[i] !== pathArray[i]) { + return; + } + + // if (record.type === "asyncStream") { + // record.asyncIterator.return?.().catch(() => {}); + // } + this._records.delete(record.id); + } + } + } +} + +export type DeferredChunkType = + // "asyncStream" | "stream" + "defer"; +export type DeferredChunkStatus = "pending" | "promised" | "complete"; + +export type DeferredChunk = DeferredDeferChunk; +// | DeferredStreamChunk +// | DeferredAsyncStreamChunk; + +export interface DeferredChunkBase { + type: DeferredChunkType; + status: DeferredChunkStatus; + id: string; + label: string | null; + path: Path | undefined; + parentChunkId: string | null; + + errors: Array; +} + +export interface DeferredDeferChunk extends DeferredChunkBase { + type: "defer"; + promise?: Promise; + result?: ObjMap; + parentResult: unknown; + returnTypeName: string; + groupedFieldSet: GroupedFieldSet; + nestedDefers: ReadonlyArray; +} + +// export interface DeferredStreamChunk extends DeferredChunkBase { +// type: "stream"; +// items: Array; +// promise: Promise>; +// } + +// export interface DeferredAsyncStreamChunk extends DeferredChunkBase { +// type: "asyncStream"; +// asyncIterator: AsyncIterator; +// items: Array; +// } diff --git a/packages/supermassive/src/__tests__/__snapshots__/executeIncrementally.graphql17.test.ts.snap b/packages/supermassive/src/__tests__/__snapshots__/executeIncrementally.graphql17.test.ts.snap index 894bd9699..80b403792 100644 --- a/packages/supermassive/src/__tests__/__snapshots__/executeIncrementally.graphql17.test.ts.snap +++ b/packages/supermassive/src/__tests__/__snapshots__/executeIncrementally.graphql17.test.ts.snap @@ -2,30 +2,12 @@ exports[`graphql-js snapshot check to ensure test stability @defer crossselection 1`] = ` { - "initialResult": { - "data": { - "person": { - "gender": "male", - "name": "Luke Skywalker", - }, + "data": { + "person": { + "gender": "male", + "name": "Luke Skywalker", }, - "hasNext": true, }, - "subsequentResults": [ - { - "hasNext": false, - "incremental": [ - { - "data": { - "gender": "male", - }, - "path": [ - "person", - ], - }, - ], - }, - ], } `; @@ -39,18 +21,29 @@ exports[`graphql-js snapshot check to ensure test stability @defer if 1`] = ` }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, "incremental": [ { "data": { "gender": "male", }, - "path": [ - "person", - ], + "id": "0", }, ], }, @@ -67,27 +60,45 @@ exports[`graphql-js snapshot check to ensure test stability @defer label simple }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "label": "INLINE", + "path": [ + "person", + ], + }, + { + "id": "1", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + { + "id": "1", + }, + ], "hasNext": false, "incremental": [ { "data": { "gender": "male", }, - "label": "INLINE", - "path": [ - "person", - ], + "id": "0", }, { "data": { "birth_year": "19BBY", }, - "path": [ - "person", - ], + "id": "1", }, ], }, @@ -104,26 +115,44 @@ exports[`graphql-js snapshot check to ensure test stability @defer multiple frag }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + { + "id": "1", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + { + "id": "1", + }, + ], "hasNext": false, "incremental": [ { "data": { "gender": "male", }, - "path": [ - "person", - ], + "id": "0", }, { "data": { "birth_year": "19BBY", }, - "path": [ - "person", - ], + "id": "1", }, ], }, @@ -140,15 +169,34 @@ exports[`graphql-js snapshot check to ensure test stability @defer nested 1`] = }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": true, "incremental": [ { "data": { "birth_year": "19BBY", }, + "id": "0", + }, + ], + "pending": [ + { + "id": "1", "path": [ "person", ], @@ -156,6 +204,11 @@ exports[`graphql-js snapshot check to ensure test stability @defer nested 1`] = ], }, { + "completed": [ + { + "id": "1", + }, + ], "hasNext": false, "incremental": [ { @@ -175,9 +228,7 @@ exports[`graphql-js snapshot check to ensure test stability @defer nested 1`] = }, ], }, - "path": [ - "person", - ], + "id": "1", }, ], }, @@ -194,9 +245,22 @@ exports[`graphql-js snapshot check to ensure test stability @defer on fragment 1 }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, "incremental": [ { @@ -204,9 +268,7 @@ exports[`graphql-js snapshot check to ensure test stability @defer on fragment 1 "birth_year": "19BBY", "gender": "male", }, - "path": [ - "person", - ], + "id": "0", }, ], }, @@ -223,9 +285,22 @@ exports[`graphql-js snapshot check to ensure test stability @defer on inline fra }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, "incremental": [ { @@ -233,9 +308,7 @@ exports[`graphql-js snapshot check to ensure test stability @defer on inline fra "birth_year": "19BBY", "gender": "male", }, - "path": [ - "person", - ], + "id": "0", }, ], }, @@ -258,50 +331,82 @@ exports[`graphql-js snapshot check to ensure test stability @defer on list 1`] = }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + "films", + 0, + ], + }, + { + "id": "1", + "path": [ + "person", + "films", + 1, + ], + }, + { + "id": "2", + "path": [ + "person", + "films", + 2, + ], + }, + { + "id": "3", + "path": [ + "person", + "films", + 3, + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + { + "id": "1", + }, + { + "id": "2", + }, + { + "id": "3", + }, + ], "hasNext": false, "incremental": [ { "data": { "title": "A New Hope", }, - "path": [ - "person", - "films", - 0, - ], + "id": "0", }, { "data": { "title": "The Empire Strikes Back", }, - "path": [ - "person", - "films", - 1, - ], + "id": "1", }, { "data": { "title": "Return of the Jedi", }, - "path": [ - "person", - "films", - 2, - ], + "id": "2", }, { "data": { "title": "Revenge of the Sith", }, - "path": [ - "person", - "films", - 3, - ], + "id": "3", }, ], }, @@ -343,28 +448,46 @@ exports[`graphql-js snapshot check to ensure test stability @defer on query with ], }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "search", + 0, + ], + }, + { + "id": "1", + "path": [ + "search", + 1, + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + { + "id": "1", + }, + ], "hasNext": false, "incremental": [ { "data": { "name": "Luke Skywalker", }, - "path": [ - "search", - 0, - ], + "id": "0", }, { "data": { "name": "Luminara Unduli", }, - "path": [ - "search", - 1, - ], + "id": "1", }, ], }, @@ -381,27 +504,44 @@ exports[`graphql-js snapshot check to ensure test stability @defer overlapping f }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + { + "id": "1", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + { + "id": "1", + }, + ], "hasNext": false, "incremental": [ { "data": { "gender": "male", }, - "path": [ - "person", - ], + "id": "0", }, { "data": { "birth_year": "19BBY", - "gender": "male", }, - "path": [ - "person", - ], + "id": "1", }, ], }, @@ -411,33 +551,11 @@ exports[`graphql-js snapshot check to ensure test stability @defer overlapping f exports[`graphql-js snapshot check to ensure test stability @defer skip/include 1`] = ` { - "initialResult": { - "data": { - "person": { - "name": "Luke Skywalker", - }, + "data": { + "person": { + "name": "Luke Skywalker", }, - "hasNext": true, }, - "subsequentResults": [ - { - "hasNext": false, - "incremental": [ - { - "data": {}, - "path": [ - "person", - ], - }, - { - "data": {}, - "path": [ - "person", - ], - }, - ], - }, - ], } `; @@ -451,22 +569,27 @@ exports[`graphql-js snapshot check to ensure test stability @stream basic 1`] = }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + "films", + ], + }, + ], }, "subsequentResults": [ { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "A New Hope", }, ], - "path": [ - "person", - "films", - 0, - ], }, ], }, @@ -474,16 +597,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream basic 1`] = "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "The Empire Strikes Back", }, ], - "path": [ - "person", - "films", - 1, - ], }, ], }, @@ -491,16 +610,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream basic 1`] = "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Return of the Jedi", }, ], - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -508,20 +623,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream basic 1`] = "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Revenge of the Sith", }, ], - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, }, ], @@ -546,34 +662,40 @@ exports[`graphql-js snapshot check to ensure test stability @stream if 1`] = ` }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + "films", + ], + }, + ], }, "subsequentResults": [ { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "A New Hope", }, ], - "path": [ - "person", - "films", - 0, - ], }, + ], + }, + { + "hasNext": true, + "incremental": [ { + "id": "0", "items": [ { "title": "The Empire Strikes Back", }, ], - "path": [ - "person", - "films", - 1, - ], }, ], }, @@ -581,16 +703,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream if 1`] = ` "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Return of the Jedi", }, ], - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -598,20 +716,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream if 1`] = ` "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Revenge of the Sith", }, ], - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, }, ], @@ -635,23 +754,28 @@ exports[`graphql-js snapshot check to ensure test stability @stream initialCount }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "label": "FILMS", + "path": [ + "person", + "films", + ], + }, + ], }, "subsequentResults": [ { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Return of the Jedi", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -659,21 +783,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream initialCount "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Revenge of the Sith", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, }, ], @@ -689,15 +813,34 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": true, "incremental": [ { "data": { "birth_year": "19BBY", }, + "id": "0", + }, + ], + "pending": [ + { + "id": "1", "path": [ "person", ], @@ -705,14 +848,26 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe ], }, { + "completed": [ + { + "id": "1", + }, + ], "hasNext": true, "incremental": [ { "data": { "films": [], }, + "id": "1", + }, + ], + "pending": [ + { + "id": "2", "path": [ "person", + "films", ], }, ], @@ -721,16 +876,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe "hasNext": true, "incremental": [ { + "id": "2", "items": [ { "title": "A New Hope", }, ], - "path": [ - "person", - "films", - 0, - ], }, ], }, @@ -738,16 +889,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe "hasNext": true, "incremental": [ { + "id": "2", "items": [ { "title": "The Empire Strikes Back", }, ], - "path": [ - "person", - "films", - 1, - ], }, ], }, @@ -755,16 +902,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe "hasNext": true, "incremental": [ { + "id": "2", "items": [ { "title": "Return of the Jedi", }, ], - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -772,20 +915,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream inside @defe "hasNext": true, "incremental": [ { + "id": "2", "items": [ { "title": "Revenge of the Sith", }, ], - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "2", + }, + ], "hasNext": false, }, ], @@ -803,23 +947,36 @@ exports[`graphql-js snapshot check to ensure test stability @stream label 1`] = }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "label": "FILMS", + "path": [ + "person", + "films", + ], + }, + { + "id": "1", + "label": "STARSHIPS", + "path": [ + "person", + "starships", + ], + }, + ], }, "subsequentResults": [ { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "A New Hope", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 0, - ], }, ], }, @@ -827,17 +984,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream label 1`] = "hasNext": true, "incremental": [ { + "id": "1", "items": [ { "name": "X-wing", }, ], - "label": "STARSHIPS", - "path": [ - "person", - "starships", - 0, - ], }, ], }, @@ -845,17 +997,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream label 1`] = "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "The Empire Strikes Back", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 1, - ], }, ], }, @@ -863,35 +1010,33 @@ exports[`graphql-js snapshot check to ensure test stability @stream label 1`] = "hasNext": true, "incremental": [ { + "id": "1", "items": [ { "name": "Imperial shuttle", }, ], - "label": "STARSHIPS", - "path": [ - "person", - "starships", - 1, - ], }, ], }, + { + "completed": [ + { + "id": "1", + }, + ], + "hasNext": true, + }, { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Return of the Jedi", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -899,21 +1044,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream label 1`] = "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Revenge of the Sith", }, ], - "label": "FILMS", - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, }, ], @@ -931,22 +1076,34 @@ exports[`graphql-js snapshot check to ensure test stability @stream multiple 1`] }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + "films", + ], + }, + { + "id": "1", + "path": [ + "person", + "starships", + ], + }, + ], }, "subsequentResults": [ { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "A New Hope", }, ], - "path": [ - "person", - "films", - 0, - ], }, ], }, @@ -954,16 +1111,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream multiple 1`] "hasNext": true, "incremental": [ { + "id": "1", "items": [ { "name": "X-wing", }, ], - "path": [ - "person", - "starships", - 0, - ], }, ], }, @@ -971,16 +1124,12 @@ exports[`graphql-js snapshot check to ensure test stability @stream multiple 1`] "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "The Empire Strikes Back", }, ], - "path": [ - "person", - "films", - 1, - ], }, ], }, @@ -988,33 +1137,33 @@ exports[`graphql-js snapshot check to ensure test stability @stream multiple 1`] "hasNext": true, "incremental": [ { + "id": "1", "items": [ { "name": "Imperial shuttle", }, ], - "path": [ - "person", - "starships", - 1, - ], }, ], }, + { + "completed": [ + { + "id": "1", + }, + ], + "hasNext": true, + }, { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Return of the Jedi", }, ], - "path": [ - "person", - "films", - 2, - ], }, ], }, @@ -1022,20 +1171,21 @@ exports[`graphql-js snapshot check to ensure test stability @stream multiple 1`] "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "title": "Revenge of the Sith", }, ], - "path": [ - "person", - "films", - 3, - ], }, ], }, { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, }, ], @@ -1052,469 +1202,189 @@ exports[`graphql-js snapshot check to ensure test stability @stream nested 1`] = }, }, "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + "films", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "1", + }, + ], "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "planets": [], "title": "A New Hope", }, ], - "path": [ - "person", - "films", - 0, - ], }, - ], - }, - { - "hasNext": true, - "incremental": [ { + "id": "1", "items": [ {}, - ], - "path": [ - "person", - "films", - 0, - "planets", - 0, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 0, - "planets", - 1, + {}, ], }, + ], + "pending": [ { - "items": [ - {}, - ], + "id": "1", "path": [ "person", "films", 0, "planets", - 2, ], }, ], }, { + "completed": [ + { + "id": "2", + }, + ], "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "planets": [], "title": "The Empire Strikes Back", }, ], - "path": [ - "person", - "films", - 1, - ], }, - ], - }, - { - "hasNext": true, - "incremental": [ { + "id": "2", "items": [ {}, - ], - "path": [ - "person", - "films", - 1, - "planets", - 0, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 1, - "planets", - 1, + {}, + {}, ], }, + ], + "pending": [ { - "items": [ - {}, - ], + "id": "2", "path": [ "person", "films", 1, "planets", - 2, ], }, ], }, { - "hasNext": true, - "incremental": [ + "completed": [ { - "items": [ - {}, - ], - "path": [ - "person", - "films", - 1, - "planets", - 3, - ], + "id": "3", }, ], - }, - { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "planets": [], "title": "Return of the Jedi", }, ], - "path": [ - "person", - "films", - 2, - ], }, - ], - }, - { - "hasNext": true, - "incremental": [ { + "id": "3", "items": [ {}, - ], - "path": [ - "person", - "films", - 2, - "planets", - 0, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 2, - "planets", - 1, - ], - }, - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 2, - "planets", - 2, + {}, + {}, ], }, ], - }, - { - "hasNext": true, - "incremental": [ + "pending": [ { - "items": [ - {}, - ], + "id": "3", "path": [ "person", "films", 2, "planets", - 3, ], }, ], }, { - "hasNext": true, - "incremental": [ + "completed": [ { - "items": [ - {}, - ], - "path": [ - "person", - "films", - 2, - "planets", - 4, - ], + "id": "4", }, ], - }, - { "hasNext": true, "incremental": [ { + "id": "0", "items": [ { "planets": [], "title": "Revenge of the Sith", }, ], - "path": [ - "person", - "films", - 3, - ], }, - ], - }, - { - "hasNext": true, - "incremental": [ { + "id": "4", "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 0, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 1, - ], - }, - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 2, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 3, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 4, - ], - }, - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 5, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 6, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 7, - ], - }, - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 8, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 9, - ], - }, - ], - }, - { - "hasNext": true, - "incremental": [ - { - "items": [ {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 10, + {}, + {}, ], }, + ], + "pending": [ { - "items": [ - {}, - ], + "id": "4", "path": [ "person", "films", 3, "planets", - 11, ], }, ], }, { - "hasNext": false, - "incremental": [ + "completed": [ { - "items": [ - {}, - ], - "path": [ - "person", - "films", - 3, - "planets", - 12, - ], + "id": "0", }, ], + "hasNext": false, }, ], } @@ -1533,9 +1403,22 @@ exports[`graphql-js snapshot check to ensure test stability @stream/defer errors [GraphQLError: Bubbling in list!], ], "hasNext": true, + "pending": [ + { + "id": "0", + "path": [ + "person", + ], + }, + ], }, "subsequentResults": [ { + "completed": [ + { + "id": "0", + }, + ], "hasNext": false, "incremental": [ { @@ -1545,9 +1428,7 @@ exports[`graphql-js snapshot check to ensure test stability @stream/defer errors "errors": [ [GraphQLError: Bubbling!], ], - "path": [ - "person", - ], + "id": "0", }, ], }, diff --git a/packages/supermassive/src/__tests__/execute.test.ts b/packages/supermassive/src/__tests__/execute.test.ts index a18025cee..9f5d9e7a3 100644 --- a/packages/supermassive/src/__tests__/execute.test.ts +++ b/packages/supermassive/src/__tests__/execute.test.ts @@ -1,6 +1,6 @@ import { parse, - execute as graphQLExecute, + experimentalExecuteIncrementally as graphQLExecute, subscribe as graphQLSubscribe, GraphQLSchema, } from "graphql"; @@ -16,7 +16,7 @@ const { compareResultForExecuteWithoutSchemaWithMVSAnnotation, drainExecution, graphqlExecuteOrSubscribe, -} = createExecutionUtils(graphQLExecute, graphQLSubscribe); +} = createExecutionUtils(graphQLExecute as any, graphQLSubscribe); interface TestCase { name: string; diff --git a/packages/supermassive/src/__tests__/executeIncrementally.graphql17.test.ts b/packages/supermassive/src/__tests__/executeIncrementally.graphql17.test.ts index 53e95837c..dad79c09a 100644 --- a/packages/supermassive/src/__tests__/executeIncrementally.graphql17.test.ts +++ b/packages/supermassive/src/__tests__/executeIncrementally.graphql17.test.ts @@ -2,7 +2,6 @@ import { parse, GraphQLSchema, experimentalExecuteIncrementally as graphQLExecute, - experimentalSubscribeIncrementally as graphQLSubscribe, } from "graphql"; import { makeSchema } from "../benchmarks/swapi-schema"; import models from "../benchmarks/swapi-schema/models"; @@ -13,7 +12,7 @@ const { compareResultForExecuteWithoutSchemaWithMVSAnnotation, drainExecution, graphqlExecuteOrSubscribe, -} = createExecutionUtils(graphQLExecute, graphQLSubscribe); +} = createExecutionUtils(graphQLExecute as any, graphQLExecute as any); interface TestCase { name: string; diff --git a/packages/supermassive/src/collectFields.ts b/packages/supermassive/src/collectFields.ts index 2f93d2cbc..02a1323d0 100644 --- a/packages/supermassive/src/collectFields.ts +++ b/packages/supermassive/src/collectFields.ts @@ -20,201 +20,187 @@ import { ExecutionContext } from "./executeWithoutSchema"; import { isAbstractType, isSubType } from "./schema/definition"; import { SchemaFragment } from "./types"; -export type FieldGroup = ReadonlyArray; - -export type GroupedFieldSet = Map; - -export interface PatchFields { - label: string | undefined; +export interface CollectFieldsResult { groupedFieldSet: GroupedFieldSet; + deferredFieldSets: ReadonlyArray; } -export interface FieldsAndPatches { +export interface DeferUsage { + label: string | undefined; groupedFieldSet: GroupedFieldSet; - patches: Array; + nestedDefers: ReadonlyArray; } -/** - * Given a selectionSet, collects all of the fields and returns them. - * - * CollectFields requires the "runtime type" of an object. For a field that - * returns an Interface or Union type, the "runtime type" will be the actual - * object type returned by that field. - * - * @internal - */ +export type FieldGroup = ReadonlyArray; + +export type GroupedFieldSet = ReadonlyMap; + export function collectFields( exeContext: ExecutionContext, - runtimeTypeName: string, -): FieldsAndPatches { - const { operation } = exeContext; + returnTypeName: string, + selectionSet: SelectionSetNode, + visitedFragmentNames?: Set, +): CollectFieldsResult { const groupedFieldSet = new AccumulatorMap(); - const patches: Array = []; + const deferredFieldSets = new Array(); + if (!visitedFragmentNames) { + visitedFragmentNames = new Set(); + } collectFieldsImpl( exeContext, - runtimeTypeName, - operation.selectionSet, + returnTypeName, + selectionSet, groupedFieldSet, - patches, - new Set(), + deferredFieldSets, + visitedFragmentNames, ); - return { groupedFieldSet, patches }; + return { groupedFieldSet, deferredFieldSets: deferredFieldSets }; } -/** - * Given an array of field nodes, collects all of the subfields of the passed - * in fields, and returns them at the end. - * - * CollectSubFields requires the "return type" of an object. For a field that - * returns an Interface or Union type, the "return type" will be the actual - * object type returned by that field. - * - * @internal - */ -// eslint-disable-next-line max-params -export function collectSubfields( - exeContext: ExecutionContext, - returnTypeName: string, - fieldGroup: FieldGroup, -): FieldsAndPatches { - const subGroupedFieldSet = new AccumulatorMap(); - const visitedFragmentNames = new Set(); - - const subPatches: Array = []; - const subFieldsAndPatches = { - groupedFieldSet: subGroupedFieldSet, - patches: subPatches, - }; - - for (const node of fieldGroup) { - if (node.selectionSet) { - collectFieldsImpl( - exeContext, - returnTypeName, - node.selectionSet, - subGroupedFieldSet, - subPatches, - visitedFragmentNames, - ); - } - } - return subFieldsAndPatches; -} - -// eslint-disable-next-line max-params function collectFieldsImpl( exeContext: ExecutionContext, - runtimeTypeName: string, + returnTypeName: string, selectionSet: SelectionSetNode, groupedFieldSet: AccumulatorMap, - patches: Array, + deferredFieldSets: Array, visitedFragmentNames: Set, ): void { for (const selection of selectionSet.selections) { + if (!shouldIncludeNode(exeContext, selection)) { + continue; + } switch (selection.kind) { case Kind.FIELD: { - if (!shouldIncludeNode(exeContext, selection)) { - continue; - } groupedFieldSet.add(getFieldEntryKey(selection), selection); break; } - case Kind.INLINE_FRAGMENT: { + case Kind.FRAGMENT_SPREAD: { + const fragmentName = selection.name.value; + const fragment = exeContext.fragments[fragmentName]; + const deferUsage = getDeferValues(exeContext, selection); + if ( - !shouldIncludeNode(exeContext, selection) || + (visitedFragmentNames.has(fragmentName) && !deferUsage) || !doesFragmentConditionMatch( - selection, - runtimeTypeName, + fragment, + returnTypeName, exeContext.schemaFragment, ) ) { continue; } - const defer = getDeferValues(exeContext, selection); + if (!deferUsage) { + visitedFragmentNames.add(fragmentName); + } + + const fragmentSelectionSet = fragment.selectionSet; - if (defer) { - const patchFields = new AccumulatorMap(); - collectFieldsImpl( - exeContext, - runtimeTypeName, - selection.selectionSet, - patchFields, - patches, - visitedFragmentNames, - ); - patches.push({ - label: defer.label, - groupedFieldSet: patchFields, - }); + const { + groupedFieldSet: fragmentGroupedFieldSets, + deferredFieldSets: fragmentDeferredFieldGroups, + } = collectFields( + exeContext, + returnTypeName, + fragmentSelectionSet, + visitedFragmentNames, + ); + if (deferUsage) { + const deferredFieldSet = { + label: deferUsage.label, + groupedFieldSet: fragmentGroupedFieldSets, + nestedDefers: fragmentDeferredFieldGroups, + }; + if (fragmentGroupedFieldSets.size > 0) { + deferredFieldSets.push(deferredFieldSet); + } } else { - collectFieldsImpl( - exeContext, - runtimeTypeName, - selection.selectionSet, - groupedFieldSet, - patches, - visitedFragmentNames, - ); + for (const [responseKey, selections] of fragmentGroupedFieldSets) { + for (const selection of selections) { + groupedFieldSet.add(responseKey, selection); + } + } + + for (const fragmentDeferredFieldGroup of fragmentDeferredFieldGroups) { + console.log(fragmentDeferredFieldGroup); + if (fragmentDeferredFieldGroup.groupedFieldSet.size > 0) { + deferredFieldSets.push(fragmentDeferredFieldGroup); + } + } } break; } - case Kind.FRAGMENT_SPREAD: { - const fragName = selection.name.value; - - if (!shouldIncludeNode(exeContext, selection)) { - continue; - } - - const defer = getDeferValues(exeContext, selection); - if (visitedFragmentNames.has(fragName) && !defer) { - continue; - } - - const fragment = exeContext.fragments[fragName]; + case Kind.INLINE_FRAGMENT: { if ( - fragment == null || !doesFragmentConditionMatch( - fragment, - runtimeTypeName, + selection, + returnTypeName, exeContext.schemaFragment, ) ) { continue; } + const fragmentSelectionSet = selection.selectionSet; + const deferUsage = getDeferValues(exeContext, selection); + + const { + groupedFieldSet: fragmentGroupedFieldSets, + deferredFieldSets: fragmentDeferredFieldGroups, + } = collectFields( + exeContext, + returnTypeName, + fragmentSelectionSet, + visitedFragmentNames, + ); + if (deferUsage) { + const deferredFieldSet = { + label: deferUsage.label, + groupedFieldSet: fragmentGroupedFieldSets, + nestedDefers: fragmentDeferredFieldGroups, + }; + if (fragmentGroupedFieldSets.size > 0) { + deferredFieldSets.push(deferredFieldSet); + } + } else { + for (const [responseKey, selections] of fragmentGroupedFieldSets) { + for (const selection of selections) { + groupedFieldSet.add(responseKey, selection); + } + } - if (!defer) { - visitedFragmentNames.add(fragName); + for (const fragmentDeferredFieldGroup of fragmentDeferredFieldGroups) { + if (fragmentDeferredFieldGroup.groupedFieldSet.size > 0) { + deferredFieldSets.push(fragmentDeferredFieldGroup); + } + } } + } + } + } +} - if (defer) { - const patchFields = new AccumulatorMap(); - collectFieldsImpl( - exeContext, - runtimeTypeName, - fragment.selectionSet, - patchFields, - patches, - visitedFragmentNames, - ); - patches.push({ - label: defer.label, - groupedFieldSet: patchFields, - }); - } else { - collectFieldsImpl( - exeContext, - runtimeTypeName, - fragment.selectionSet, - groupedFieldSet, - patches, - visitedFragmentNames, - ); +export function collectSubfields( + exeContext: ExecutionContext, + returnTypeName: string, + fieldGroup: FieldGroup, +): CollectFieldsResult { + const groupedFieldSet = new AccumulatorMap(); + const deferredFieldGroups = new Array(); + for (const fieldNode of fieldGroup) { + if (fieldNode.selectionSet) { + const { + groupedFieldSet: subGroupedFieldSet, + deferredFieldSets: deferredSubGrouppedFieldSets, + } = collectFields(exeContext, returnTypeName, fieldNode.selectionSet); + deferredFieldGroups.push(...deferredSubGrouppedFieldSets); + for (const [responseKey, selections] of subGroupedFieldSet) { + for (const selection of selections) { + groupedFieldSet.add(responseKey, selection); } - break; } } } + return { groupedFieldSet, deferredFieldSets: deferredFieldGroups }; } /** @@ -281,7 +267,7 @@ function getFieldEntryKey(node: FieldNode): string { function getDeferValues( exeContext: ExecutionContext, node: FragmentSpreadNode | InlineFragmentNode, -): undefined | { label: string | undefined } { +): { label: string | undefined } | undefined { const defer = getDirectiveValues(exeContext, GraphQLDeferDirective, node); if (!defer) { diff --git a/packages/supermassive/src/executeWithoutSchema.ts b/packages/supermassive/src/executeWithoutSchema.ts index bba24b888..33112e3f7 100644 --- a/packages/supermassive/src/executeWithoutSchema.ts +++ b/packages/supermassive/src/executeWithoutSchema.ts @@ -35,9 +35,6 @@ import type { ExecutionResult, TotalExecutionResult, SubsequentIncrementalExecutionResult, - IncrementalDeferResult, - IncrementalResult, - IncrementalStreamResult, IncrementalExecutionResult, SchemaFragment, SchemaFragmentLoader, @@ -65,6 +62,7 @@ import type { TypeReference } from "./schema/reference"; import type { FieldDefinition } from "./schema/definition"; import * as Definitions from "./schema/definition"; import * as Resolvers from "./schema/resolvers"; +import { DeferredExecutionManager } from "./IncrementalDataManager"; /** * A memoized collection of relevant subfields with regard to the return @@ -120,7 +118,7 @@ export interface ExecutionContext { subscribeFieldResolver: FunctionFieldResolver; errors: Array; fieldExecutionHooks?: ExecutionHooks; - subsequentPayloads: Set; + deferredExecutionManager: DeferredExecutionManager; } /** @@ -258,7 +256,7 @@ function buildExecutionContext( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, errors: [], fieldExecutionHooks, - subsequentPayloads: new Set(), + deferredExecutionManager: new DeferredExecutionManager(), }; } @@ -272,7 +270,6 @@ function buildPerEventExecutionContext( ? exeContext.buildContextValue(exeContext.contextValue) : exeContext.contextValue, rootValue: payload, - subsequentPayloads: new Set(), errors: [], }; } @@ -307,16 +304,18 @@ function executeOperation( ): PromiseOrValue { try { const { operation, rootValue } = exeContext; + const { operation: operationType, selectionSet } = operation; const rootTypeName = getOperationRootTypeName(operation); - const { groupedFieldSet, patches } = collectFields( + const { groupedFieldSet, deferredFieldSets } = collectFields( exeContext, rootTypeName, + selectionSet, ); const path = undefined; let result; // Note: cannot use OperationTypeNode from graphql-js as it doesn't exist in 15.x - switch (operation.operation) { + switch (operationType) { case "query": result = executeFields( exeContext, @@ -324,7 +323,7 @@ function executeOperation( rootValue, path, groupedFieldSet, - undefined, + null, ); result = buildResponse(exeContext, result); break; @@ -356,16 +355,21 @@ function executeOperation( ); } - for (const patch of patches) { - const { label, groupedFieldSet: patchGroupedFieldSet } = patch; - executeDeferredFragment( - exeContext, - rootTypeName, - rootValue, - patchGroupedFieldSet, - label, + for (const { + label, + groupedFieldSet: deferredGroupFieldSet, + nestedDefers, + } of deferredFieldSets) { + console.log(label, groupedFieldSet); + exeContext.deferredExecutionManager.addDeferRecord({ + label: label || null, path, - ); + parentChunkId: null, + returnTypeName: rootTypeName, + parentResult: result, + groupedFieldSet: deferredGroupFieldSet, + nestedDefers, + }); } return result; @@ -399,28 +403,31 @@ function buildResponse( exeContext.errors.length === 0 ? { data } : { errors: exeContext.errors, data }; - if (exeContext.subsequentPayloads.size > 0) { - // TODO: define how to call hooks for incremental results + if (hooks?.afterBuildResponse) { + const hookResult = invokeAfterBuildResponseHook( + exeContext, + initialResult, + ); + if (exeContext.errors.length > (initialResult.errors?.length ?? 0)) { + initialResult.errors = exeContext.errors; + } + if (hookResult instanceof GraphQLError) { + return { errors: initialResult.errors }; + } + } + if ( + initialResult.data != null && + exeContext.deferredExecutionManager.hasRecords() + ) { return { initialResult: { ...initialResult, hasNext: true, + pending: exeContext.deferredExecutionManager.getPending(), }, subsequentResults: yieldSubsequentPayloads(exeContext), }; } else { - if (hooks?.afterBuildResponse) { - const hookResult = invokeAfterBuildResponseHook( - exeContext, - initialResult, - ); - if (exeContext.errors.length > (initialResult.errors?.length ?? 0)) { - initialResult.errors = exeContext.errors; - } - if (hookResult instanceof GraphQLError) { - return { errors: initialResult.errors }; - } - } return initialResult; } } catch (error) { @@ -450,7 +457,7 @@ function executeFieldsSerially( sourceValue, fieldGroup, fieldPath, - undefined, + null, ); if (result === undefined) { return results; @@ -478,7 +485,7 @@ function executeFields( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue> { const results = Object.create(null); let containsPromise = false; @@ -492,7 +499,7 @@ function executeFields( sourceValue, fieldGroup, fieldPath, - incrementalDataRecord, + deferredChunkId, ); if (result !== undefined) { @@ -525,7 +532,7 @@ function executeFields( /** * Implements the "Executing field" section of the spec - * In particular, this function figures out the value that the field returns by + * `In` particular, this function figures out the value that the field returns by * calling its resolve function, then calls completeValue to complete promises, * serialize scalars, or execute the sub-selection-set for objects. */ @@ -535,7 +542,7 @@ function executeField( source: unknown, fieldGroup: FieldGroup, path: Path, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue { const schemaFragment = exeContext.schemaFragment; const fieldName = fieldGroup[0].name.value; @@ -553,7 +560,7 @@ function executeField( fieldGroup, path, source, - incrementalDataRecord, + deferredChunkId, ); } @@ -579,7 +586,7 @@ function executeField( fieldGroup, path, source, - incrementalDataRecord, + deferredChunkId, ); } return undefined; @@ -680,7 +687,11 @@ function executeSubscriptionImpl( ): PromiseOrValue> { const { operation, rootValue, schemaFragment } = exeContext; const rootTypeName = getOperationRootTypeName(operation); - const { groupedFieldSet } = collectFields(exeContext, rootTypeName); + const { groupedFieldSet } = collectFields( + exeContext, + rootTypeName, + operation.selectionSet, + ); const firstRootField = groupedFieldSet.entries().next().value; if (firstRootField === undefined) { @@ -891,7 +902,7 @@ function mapResultOrEventStreamOrPromise( payload, path, groupedFieldSet, - undefined, + null, ); }) : executeFields( @@ -900,7 +911,7 @@ function mapResultOrEventStreamOrPromise( payload, path, groupedFieldSet, - undefined, + null, ); // This is typechecked in collect values return buildResponse(perEventContext, data) as TotalExecutionResult; @@ -931,6 +942,7 @@ export function buildResolveInfo( return { fieldName: fieldName, fieldNodes: fieldGroup, + fieldGroup: fieldGroup, returnTypeName, parentTypeName, path, @@ -947,7 +959,7 @@ function handleFieldError( returnTypeRef: TypeReference, fieldGroup: FieldGroup, path: Path, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): void { const error = locatedError(rawError, fieldGroup, pathToArray(path)); @@ -957,11 +969,13 @@ function handleFieldError( throw error; } - const errors = incrementalDataRecord?.errors ?? exeContext.errors; - // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. - errors.push(error); + if (deferredChunkId) { + exeContext.deferredExecutionManager.addError(deferredChunkId, error); + } else { + exeContext.errors.push(error); + } } function resolveAndCompleteField( @@ -971,7 +985,7 @@ function resolveAndCompleteField( fieldGroup: FieldGroup, path: Path, source: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue { const fieldName = fieldGroup[0].name.value; const returnTypeRef = Definitions.getFieldTypeReference(fieldDefinition); @@ -1057,7 +1071,7 @@ function resolveAndCompleteField( info, path, hookContext instanceof GraphQLError ? null : resolved, - incrementalDataRecord, + deferredChunkId, ); }, (rawError) => { @@ -1094,7 +1108,7 @@ function resolveAndCompleteField( info, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1134,7 +1148,7 @@ function resolveAndCompleteField( returnTypeRef, fieldGroup, path, - incrementalDataRecord, + deferredChunkId, ); return null; }, @@ -1189,7 +1203,7 @@ function resolveAndCompleteField( returnTypeRef, fieldGroup, path, - incrementalDataRecord, + deferredChunkId, ); return null; } @@ -1223,7 +1237,7 @@ function completeValue( info: ResolveInfo, path: Path, result: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue { // If result is an Error, throw a located error. if (result instanceof Error) { @@ -1240,7 +1254,7 @@ function completeValue( info, path, result, - incrementalDataRecord, + deferredChunkId, ); if (completed === null) { throw new Error( @@ -1264,7 +1278,7 @@ function completeValue( info, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1288,7 +1302,7 @@ function completeValue( info, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1301,7 +1315,7 @@ function completeValue( fieldGroup, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1320,7 +1334,7 @@ async function completePromisedValue( info: ResolveInfo, path: Path, result: Promise, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): Promise { try { const resolved = await result; @@ -1331,7 +1345,7 @@ async function completePromisedValue( info, path, resolved, - incrementalDataRecord, + deferredChunkId, ); if (isPromise(completed)) { completed = await completed; @@ -1344,9 +1358,12 @@ async function completePromisedValue( returnTypeRef, fieldGroup, path, - incrementalDataRecord, + deferredChunkId, + ); + exeContext.deferredExecutionManager.removeSubsequentPayloads( + path, + deferredChunkId, ); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); return null; } } @@ -1362,7 +1379,7 @@ function completeListValue( info: ResolveInfo, path: Path, result: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue> { const itemTypeRef = unwrap(returnTypeRef); if (isAsyncIterable(result)) { @@ -1375,7 +1392,7 @@ function completeListValue( info, path, asyncIterator, - incrementalDataRecord, + deferredChunkId, ); } @@ -1386,12 +1403,11 @@ function completeListValue( ); } - const stream = getStreamValues(exeContext, fieldGroup, path); + // const streamUsage = getStreamValues(exeContext, fieldGroup, path); // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; - let previousIncrementalDataRecord = incrementalDataRecord; const completedResults: Array = []; let index = 0; for (const item of result) { @@ -1399,25 +1415,13 @@ function completeListValue( // since from here on it is not ever accessed by resolver functions. const itemPath = addPath(path, index, undefined); - if ( - stream && - typeof stream.initialCount === "number" && - index >= stream.initialCount - ) { - previousIncrementalDataRecord = executeStreamField( - path, - itemPath, - item, - exeContext, - fieldGroup, - info, - itemTypeRef, - stream.label, - previousIncrementalDataRecord, - ); - index++; - continue; - } + // if ( + // streamUsage && + // typeof streamUsage.initialCount === "number" && + // index >= streamUsage.initialCount + // ) { + // break; + // } if ( completeListItemValue( @@ -1428,7 +1432,7 @@ function completeListValue( fieldGroup, info, itemPath, - incrementalDataRecord, + deferredChunkId, ) ) { containsPromise = true; @@ -1437,7 +1441,20 @@ function completeListValue( index++; } + // if (streamUsage && index <= completedResults.length - 1) { + // const sliced = Promise.all(completedResults.slice(index)); + // exeContext.deferredExecutionManager.addItemsStreamRecord({ + // label: streamUsage.label || null, + // path, + // parentChunkId: deferredChunkId, + // promise: sliced, + // }); + // return containsPromise + // ? Promise.all(completedResults.slice(0, index)) + // : completedResults.slice(0, index); + // } else { return containsPromise ? Promise.all(completedResults) : completedResults; + // } } /** @@ -1453,7 +1470,7 @@ function completeListItemValue( fieldGroup: FieldGroup, info: ResolveInfo, itemPath: Path, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): boolean { if (isPromise(item)) { completedResults.push( @@ -1464,7 +1481,7 @@ function completeListItemValue( info, itemPath, item, - incrementalDataRecord, + deferredChunkId, ), ); @@ -1479,7 +1496,7 @@ function completeListItemValue( info, itemPath, item, - incrementalDataRecord, + deferredChunkId, ); if (isPromise(completedItem)) { @@ -1493,9 +1510,12 @@ function completeListItemValue( itemTypeRef, fieldGroup, itemPath, - incrementalDataRecord, + deferredChunkId, + ); + exeContext.deferredExecutionManager.removeSubsequentPayloads( + itemPath, + deferredChunkId, ); - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); return null; }), ); @@ -1511,71 +1531,74 @@ function completeListItemValue( itemTypeRef, fieldGroup, itemPath, - incrementalDataRecord, + deferredChunkId, + ); + exeContext.deferredExecutionManager.removeSubsequentPayloads( + itemPath, + deferredChunkId, ); - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); completedResults.push(null); } return false; } -/** - * Returns an object containing the `@stream` arguments if a field should be - * streamed based on the experimental flag, stream directive present and - * not disabled by the "if" argument. - */ -function getStreamValues( - exeContext: ExecutionContext, - fieldGroup: FieldGroup, - path: Path, -): - | undefined - | { - initialCount: number | undefined; - label: string | undefined; - } { - // do not stream inner lists of multi-dimensional lists - if (typeof path.key === "number") { - return; - } - - // validation only allows equivalent streams on multiple fields, so it is - // safe to only check the first fieldNode for the stream directive - const stream = getDirectiveValues( - exeContext, - GraphQLStreamDirective, - fieldGroup[0], - ); - - if (!stream) { - return; - } - - if (stream.if === false) { - return; - } - - invariant( - typeof stream.initialCount === "number", - "initialCount must be a number", - ); - - invariant( - stream.initialCount >= 0, - "initialCount must be a positive integer", - ); - - invariant( - exeContext.operation.operation !== "subscription", - "`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.", - ); - - return { - initialCount: stream.initialCount, - label: typeof stream.label === "string" ? stream.label : undefined, - }; -} +// /** +// * Returns an object containing the `@stream` arguments if a field should be +// * streamed based on the experimental flag, stream directive present and +// * not disabled by the "if" argument. +// */ +// function getStreamValues( +// exeContext: ExecutionContext, +// fieldGroup: FieldGroup, +// path: Path, +// ): +// | undefined +// | { +// initialCount: number | undefined; +// label: string | undefined; +// } { +// // do not stream inner lists of multi-dimensional lists +// if (typeof path.key === "number") { +// return; +// } + +// // validation only allows equivalent streams on multiple fields, so it is +// // safe to only check the first fieldNode for the stream directive +// const stream = getDirectiveValues( +// exeContext, +// GraphQLStreamDirective, +// fieldGroup[0], +// ); + +// if (!stream) { +// return; +// } + +// if (stream.if === false) { +// return; +// } + +// invariant( +// typeof stream.initialCount === "number", +// "initialCount must be a number", +// ); + +// invariant( +// stream.initialCount >= 0, +// "initialCount must be a positive integer", +// ); + +// invariant( +// exeContext.operation.operation !== "subscription", +// "`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.", +// ); + +// return { +// initialCount: stream.initialCount, +// label: typeof stream.label === "string" ? stream.label : undefined, +// }; +// } /** * Complete a async iterator value by completing the result and calling @@ -1588,33 +1611,27 @@ async function completeAsyncIteratorValue( info: ResolveInfo, path: Path, asyncIterator: AsyncIterator, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): Promise> { - const stream = getStreamValues(exeContext, fieldGroup, path); + // const streamUsage = getStreamValues(exeContext, fieldGroup, path); let containsPromise = false; const completedResults: Array = []; let index = 0; // eslint-disable-next-line no-constant-condition while (true) { - if ( - stream && - typeof stream.initialCount === "number" && - index >= stream.initialCount - ) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - executeStreamAsyncIterator( - index, - asyncIterator, - exeContext, - fieldGroup, - info, - itemTypeRef, - path, - stream.label, - incrementalDataRecord, - ); - break; - } + // if ( + // streamUsage && + // typeof streamUsage.initialCount === "number" && + // index >= streamUsage.initialCount + // ) { + // exeContext.deferredExecutionManager.addAsyncIteratorStreamRecord({ + // label: streamUsage.label || null, + // path, + // parentChunkId: deferredChunkId, + // asyncIterator: asyncIterator as AsyncIterator>, + // }); + // break; + // } const itemPath = addPath(path, index, undefined); let iteration; @@ -1637,7 +1654,7 @@ async function completeAsyncIteratorValue( fieldGroup, info, itemPath, - incrementalDataRecord, + deferredChunkId, ) ) { containsPromise = true; @@ -1676,7 +1693,7 @@ function completeAbstractValue( info: ResolveInfo, path: Path, result: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue> { const { schemaFragment } = exeContext; const resolveTypeFn = @@ -1713,7 +1730,7 @@ function completeAbstractValue( fieldGroup, path, result, - incrementalDataRecord, + deferredChunkId, ), ); } @@ -1724,7 +1741,7 @@ function completeAbstractValue( fieldGroup, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1853,7 +1870,7 @@ function completeObjectValue( fieldGroup: FieldGroup, path: Path, result: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + deferredChunkId: string | null, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. return collectAndExecuteSubfields( @@ -1862,7 +1879,7 @@ function completeObjectValue( fieldGroup, path, result, - incrementalDataRecord, + deferredChunkId, ); } @@ -1872,11 +1889,14 @@ function collectAndExecuteSubfields( fieldGroup: FieldGroup, path: Path, result: unknown, - incrementalDataRecord: IncrementalDataRecord | undefined, + parentDeferredChunkId: string | null, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. - const { groupedFieldSet: subGroupedFieldSet, patches: subPatches } = - collectSubfields(exeContext, { name: returnTypeName }, fieldGroup); + const { + groupedFieldSet: subGroupedFieldSet, + deferredFieldSets: deferredSelections, + } = collectSubfields(exeContext, { name: returnTypeName }, fieldGroup); + // console.log(subGroupedFieldSet, deferredFieldSets); const subFields = executeFields( exeContext, @@ -1884,20 +1904,23 @@ function collectAndExecuteSubfields( result, path, subGroupedFieldSet, - incrementalDataRecord, + parentDeferredChunkId, ); - for (const subPatch of subPatches) { - const { label, groupedFieldSet: subPatchGroupedFieldSet } = subPatch; - executeDeferredFragment( - exeContext, - returnTypeName, - result, - subPatchGroupedFieldSet, - label, + for (const { + label, + groupedFieldSet: deferredGroupedFieldSet, + nestedDefers, + } of deferredSelections) { + exeContext.deferredExecutionManager.addDeferRecord({ + label: label || null, path, - incrementalDataRecord, - ); + parentChunkId: parentDeferredChunkId, + returnTypeName, + parentResult: result, + groupedFieldSet: deferredGroupedFieldSet, + nestedDefers, + }); } return subFields; @@ -2327,373 +2350,49 @@ export function getOperationRootTypeName( } } -function executeDeferredFragment( - exeContext: ExecutionContext, - parentTypeName: string, - sourceValue: unknown, - fields: GroupedFieldSet, - label?: string, - path?: Path, - parentContext?: IncrementalDataRecord, -): void { - const incrementalDataRecord = new DeferredFragmentRecord({ - label, - path, - parentContext, - exeContext, - }); - let promiseOrData; - try { - promiseOrData = executeFields( - exeContext, - parentTypeName, - sourceValue, - path, - fields, - incrementalDataRecord, - ); - - if (isPromise(promiseOrData)) { - promiseOrData = promiseOrData.then(null, (e) => { - incrementalDataRecord.errors.push(e); - return null; - }); - } - } catch (e) { - incrementalDataRecord.errors.push(e as GraphQLError); - promiseOrData = null; - } - incrementalDataRecord.addData(promiseOrData); -} - -function executeStreamField( - path: Path, - itemPath: Path, - item: PromiseOrValue, +function yieldSubsequentPayloads( exeContext: ExecutionContext, - fieldGroup: FieldGroup, - info: ResolveInfo, - itemTypeRef: TypeReference, - label?: string, - parentContext?: IncrementalDataRecord, -): IncrementalDataRecord { - const incrementalDataRecord = new StreamItemsRecord({ - label, - path: itemPath, - parentContext, - exeContext, - }); - if (isPromise(item)) { - const completedItems = completePromisedValue( - exeContext, - itemTypeRef, - fieldGroup, - info, - itemPath, - item, - incrementalDataRecord, - ).then( - (value) => [value], - (error) => { - incrementalDataRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); - return null; - }, - ); - - incrementalDataRecord.addItems(completedItems); - return incrementalDataRecord; - } - - let completedItem: PromiseOrValue; - try { - try { - completedItem = completeValue( - exeContext, - itemTypeRef, - fieldGroup, - info, - itemPath, - item, - incrementalDataRecord, - ); - } catch (rawError) { - handleFieldError( - rawError, - exeContext, - itemTypeRef, - fieldGroup, - itemPath, - incrementalDataRecord, - ); - completedItem = null; - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); - } - } catch (error) { - incrementalDataRecord.errors.push(error as GraphQLError); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); - incrementalDataRecord.addItems(null); - return incrementalDataRecord; - } - - if (isPromise(completedItem)) { - const completedItems = completedItem - .then(undefined, (rawError) => { - handleFieldError( - rawError, - exeContext, - itemTypeRef, - fieldGroup, - itemPath, - incrementalDataRecord, - ); - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); - return null; - }) - .then( - (value) => [value], - (error) => { - incrementalDataRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); - return null; - }, - ); - - incrementalDataRecord.addItems(completedItems); - return incrementalDataRecord; - } - - incrementalDataRecord.addItems([completedItem]); - return incrementalDataRecord; -} +): AsyncGenerator< + SubsequentIncrementalExecutionResult, ObjMap>, + void, + void +> { + let isDone = false; -async function executeStreamAsyncIteratorItem( - asyncIterator: AsyncIterator, - exeContext: ExecutionContext, - fieldGroup: FieldGroup, - info: ResolveInfo, - itemTypeRef: TypeReference, - incrementalDataRecord: StreamItemsRecord, - path: Path, - itemPath: Path, -): Promise> { - let item; - try { - const { value, done } = await asyncIterator.next(); - if (done) { - incrementalDataRecord.setIsCompletedAsyncIterator(); - return { done, value: undefined }; + async function next(): Promise< + IteratorResult< + SubsequentIncrementalExecutionResult, ObjMap>, + void + > + > { + if (isDone) { + return { value: undefined, done: true }; } - item = value; - } catch (rawError) { - throw locatedError(rawError, fieldGroup, pathToArray(path)); - } - let completedItem; - try { - completedItem = completeValue( - exeContext, - itemTypeRef, - fieldGroup, - info, - itemPath, - item, - incrementalDataRecord, - ); - if (isPromise(completedItem)) { - completedItem = completedItem.then(undefined, (rawError) => { - handleFieldError( - rawError, - exeContext, - itemTypeRef, - fieldGroup, - itemPath, - incrementalDataRecord, - ); - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); - return null; - }); - } - return { done: false, value: completedItem }; - } catch (rawError) { - handleFieldError( - rawError, + exeContext.deferredExecutionManager.executePending( exeContext, - itemTypeRef, - fieldGroup, - itemPath, - incrementalDataRecord, + executeFields, ); - filterSubsequentPayloads(exeContext, itemPath, incrementalDataRecord); - return { done: false, value: null }; - } -} -async function executeStreamAsyncIterator( - initialIndex: number, - asyncIterator: AsyncIterator, - exeContext: ExecutionContext, - fieldGroup: FieldGroup, - info: ResolveInfo, - itemTypeRef: TypeReference, - path: Path, - label?: string, - parentContext?: IncrementalDataRecord, -): Promise { - let index = initialIndex; - let previousIncrementalDataRecord = parentContext ?? undefined; - // eslint-disable-next-line no-constant-condition - while (true) { - const itemPath = addPath(path, index, undefined); - const incrementalDataRecord = new StreamItemsRecord({ - label, - path: itemPath, - parentContext: previousIncrementalDataRecord, - asyncIterator, - exeContext, - }); + await exeContext.deferredExecutionManager.waitForNext(); - let iteration; - try { - // eslint-disable-next-line no-await-in-loop - iteration = await executeStreamAsyncIteratorItem( - asyncIterator, - exeContext, - fieldGroup, - info, - itemTypeRef, - incrementalDataRecord, - path, - itemPath, - ); - } catch (error) { - incrementalDataRecord.errors.push(error as GraphQLError); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); - incrementalDataRecord.addItems(null); - // entire stream has errored and bubbled upwards - if (asyncIterator?.return) { - asyncIterator.return().catch(() => { - // ignore errors - }); - } - return; - } - - const { done, value: completedItem } = iteration; - - let completedItems: PromiseOrValue | null>; - if (isPromise(completedItem)) { - completedItems = completedItem.then( - (value) => [value], - (error) => { - incrementalDataRecord.errors.push(error); - filterSubsequentPayloads(exeContext, path, incrementalDataRecord); - return null; - }, - ); - } else { - completedItems = [completedItem]; - } - - incrementalDataRecord.addItems(completedItems); - - if (done) { - break; - } - previousIncrementalDataRecord = incrementalDataRecord; - index++; - } -} - -function filterSubsequentPayloads( - exeContext: ExecutionContext, - nullPath: Path, - currentIncrementalDataRecord: IncrementalDataRecord | undefined, -): void { - const nullPathArray = pathToArray(nullPath); - exeContext.subsequentPayloads.forEach((incrementalDataRecord) => { - if (incrementalDataRecord === currentIncrementalDataRecord) { - // don't remove payload from where error originates - return; - } - for (let i = 0; i < nullPathArray.length; i++) { - if (incrementalDataRecord.path[i] !== nullPathArray[i]) { - // incrementalDataRecord points to a path unaffected by this payload - return; - } - } - // incrementalDataRecord path points to nulled error field - if ( - isStreamItemsRecord(incrementalDataRecord) && - incrementalDataRecord.asyncIterator?.return - ) { - incrementalDataRecord.asyncIterator.return().catch(() => { - // ignore error - }); - } - exeContext.subsequentPayloads.delete(incrementalDataRecord); - }); -} - -function getCompletedIncrementalResults( - exeContext: ExecutionContext, -): Array { - const incrementalResults: Array = []; - for (const incrementalDataRecord of exeContext.subsequentPayloads) { - const incrementalResult: IncrementalResult = {}; - if (!incrementalDataRecord.isCompleted) { - continue; - } - exeContext.subsequentPayloads.delete(incrementalDataRecord); - if (isStreamItemsRecord(incrementalDataRecord)) { - const items = incrementalDataRecord.items; - if (incrementalDataRecord.isCompletedAsyncIterator) { - // async iterable resolver just finished but there may be pending payloads - continue; - } - (incrementalResult as IncrementalStreamResult).items = items; - } else { - const data = incrementalDataRecord.data; - (incrementalResult as IncrementalDeferResult).data = data ?? null; - } - - incrementalResult.path = incrementalDataRecord.path; - if (incrementalDataRecord.label != null) { - incrementalResult.label = incrementalDataRecord.label; - } - if (incrementalDataRecord.errors.length > 0) { - incrementalResult.errors = incrementalDataRecord.errors; - } - incrementalResults.push(incrementalResult); - } - return incrementalResults; -} - -function yieldSubsequentPayloads( - exeContext: ExecutionContext, -): AsyncGenerator { - let isDone = false; - - async function next(): Promise< - IteratorResult - > { if (isDone) { + // a different call to next has exhausted all payloads return { value: undefined, done: true }; } - await Promise.race( - Array.from(exeContext.subsequentPayloads).map((p) => p.promise), - ); + const [incremental, completed] = + exeContext.deferredExecutionManager.drainCompleted(); if (isDone) { // a different call to next has exhausted all payloads return { value: undefined, done: true }; } - const incremental = getCompletedIncrementalResults(exeContext); - const hasNext = exeContext.subsequentPayloads.size > 0; + const pending = exeContext.deferredExecutionManager.getPending(); + const hasNext = exeContext.deferredExecutionManager.hasNext(); - if (!incremental.length && hasNext) { + if (incremental.length === 0 && completed.length === 0 && hasNext) { return next(); } @@ -2701,24 +2400,43 @@ function yieldSubsequentPayloads( isDone = true; } + const incrementalResult: SubsequentIncrementalExecutionResult< + ObjMap, + ObjMap + > = { + hasNext, + }; + + if (pending.length > 0) { + incrementalResult.pending = pending; + } + + if (completed.length > 0) { + incrementalResult.completed = completed; + } + + if (incremental.length > 0) { + incrementalResult.incremental = incremental; + } + return { - value: incremental.length ? { incremental, hasNext } : { hasNext }, + value: incrementalResult, done: false, }; } - function returnStreamIterators() { - const promises: Array>> = []; - exeContext.subsequentPayloads.forEach((incrementalDataRecord) => { - if ( - isStreamItemsRecord(incrementalDataRecord) && - incrementalDataRecord.asyncIterator?.return - ) { - promises.push(incrementalDataRecord.asyncIterator.return()); - } - }); - return Promise.all(promises); - } + // function returnStreamIterators() { + // const promises: Array>> = []; + // exeContext.subsequentPayloads.forEach((incrementalDataRecord) => { + // if ( + // isStreamItemsRecord(incrementalDataRecord) && + // incrementalDataRecord.asyncIterator?.return + // ) { + // promises.push(incrementalDataRecord.asyncIterator.return()); + // } + // }); + // return Promise.all(promises); + // } return { [Symbol.asyncIterator]() { @@ -2726,133 +2444,29 @@ function yieldSubsequentPayloads( }, next, async return(): Promise< - IteratorResult + IteratorResult< + SubsequentIncrementalExecutionResult, ObjMap>, + void + > > { - await returnStreamIterators(); + // await returnStreamIterators(); isDone = true; return { value: undefined, done: true }; }, async throw( error?: unknown, - ): Promise> { - await returnStreamIterators(); + ): Promise< + IteratorResult< + SubsequentIncrementalExecutionResult, ObjMap>, + void + > + > { + // await returnStreamIterators(); isDone = true; return Promise.reject(error); }, }; } - -export type IncrementalDataRecord = DeferredFragmentRecord | StreamItemsRecord; - -function isStreamItemsRecord( - incrementalDataRecord: IncrementalDataRecord, -): incrementalDataRecord is StreamItemsRecord { - return incrementalDataRecord.type === "stream"; -} - -class DeferredFragmentRecord { - type: "defer"; - errors: Array; - label: string | undefined; - path: Array; - promise: Promise; - data: ObjMap | null; - parentContext: IncrementalDataRecord | undefined; - isCompleted: boolean; - _exeContext: ExecutionContext; - _resolve?: (arg: PromiseOrValue | null>) => void; - - constructor(opts: { - label: string | undefined; - path: Path | undefined; - parentContext: IncrementalDataRecord | undefined; - exeContext: ExecutionContext; - }) { - this.type = "defer"; - this.label = opts.label; - this.path = pathToArray(opts.path); - this.parentContext = opts.parentContext; - this.errors = []; - this._exeContext = opts.exeContext; - this._exeContext.subsequentPayloads.add(this); - this.isCompleted = false; - this.data = null; - this.promise = new Promise | null>((resolve) => { - this._resolve = (promiseOrValue) => { - resolve(promiseOrValue); - }; - }).then((data) => { - this.data = data; - this.isCompleted = true; - }); - } - - addData(data: PromiseOrValue | null>) { - const parentData = this.parentContext?.promise; - if (parentData) { - this._resolve?.(parentData.then(() => data)); - return; - } - this._resolve?.(data); - } -} - -class StreamItemsRecord { - type: "stream"; - errors: Array; - label: string | undefined; - path: Array; - items: Array | null; - promise: Promise; - parentContext: IncrementalDataRecord | undefined; - asyncIterator: AsyncIterator | undefined; - isCompletedAsyncIterator?: boolean; - isCompleted: boolean; - _exeContext: ExecutionContext; - _resolve?: (arg: PromiseOrValue | null>) => void; - - constructor(opts: { - label: string | undefined; - path: Path | undefined; - asyncIterator?: AsyncIterator; - parentContext: IncrementalDataRecord | undefined; - exeContext: ExecutionContext; - }) { - this.type = "stream"; - this.items = null; - this.label = opts.label; - this.path = pathToArray(opts.path); - this.parentContext = opts.parentContext; - this.asyncIterator = opts.asyncIterator; - this.errors = []; - this._exeContext = opts.exeContext; - this._exeContext.subsequentPayloads.add(this); - this.isCompleted = false; - this.items = null; - this.promise = new Promise | null>((resolve) => { - this._resolve = (promiseOrValue) => { - resolve(promiseOrValue); - }; - }).then((items) => { - this.items = items; - this.isCompleted = true; - }); - } - - addItems(items: PromiseOrValue | null>) { - const parentData = this.parentContext?.promise; - if (parentData) { - this._resolve?.(parentData.then(() => items)); - return; - } - this._resolve?.(items); - } - - setIsCompletedAsyncIterator() { - this.isCompletedAsyncIterator = true; - } -} - export function isIncrementalExecutionResult< TData = ObjMap, TExtensions = ObjMap, diff --git a/packages/supermassive/src/index.ts b/packages/supermassive/src/index.ts index 9d36c91aa..37eabef32 100644 --- a/packages/supermassive/src/index.ts +++ b/packages/supermassive/src/index.ts @@ -148,7 +148,3 @@ export type { BaseExecuteFieldHookArgs, ExecutionHooks, } from "./hooks/types"; - -export * as LegacyTypedAST from "./legacyAST/TypedAST"; -export { addTypesToRequestDocument as addSupermassiveLegacyTypesToRequestDocument } from "./legacyAST/addTypesToRequestDocument"; -export { annotateDocumentGraphQLTransform as annotateDocumentWithSupermassiveLegacyTypesGraphQLTransform } from "./legacyAST/annotateDocumentGraphQLTransform"; diff --git a/packages/supermassive/src/legacyAST/TypedAST.ts b/packages/supermassive/src/legacyAST/TypedAST.ts deleted file mode 100644 index 68fdcda8f..000000000 --- a/packages/supermassive/src/legacyAST/TypedAST.ts +++ /dev/null @@ -1,516 +0,0 @@ -/** - * Taken from node_modules/graphql/language/ast.d.ts - * License: MIT License - * Copyright (c): GraphQL Contributors - * - * Our changes MUST be annotated inline for ease of future merging. - */ - -/** - * [SUPERMASSIVE] - * - * We don't need `Location`, `Token`, and `isNode` , so remove it and its dependencies. - */ -type Location = unknown; - -/** - * The list of all possible AST node types. - */ -export type ASTNode = - | NameNode - | DocumentNode - | OperationDefinitionNode - | VariableDefinitionNode - | VariableNode - | SelectionSetNode - | FieldNode - | ArgumentNode - | FragmentSpreadNode - | InlineFragmentNode - | FragmentDefinitionNode - | IntValueNode - | FloatValueNode - | StringValueNode - | BooleanValueNode - | NullValueNode - | EnumValueNode - | ListValueNode - | ObjectValueNode - | ObjectFieldNode - | DirectiveNode - | NamedTypeNode - | ListTypeNode - | NonNullTypeNode - | SchemaDefinitionNode - | OperationTypeDefinitionNode - | ScalarTypeDefinitionNode - | ObjectTypeDefinitionNode - | FieldDefinitionNode - | InputValueDefinitionNode - | InterfaceTypeDefinitionNode - | UnionTypeDefinitionNode - | EnumTypeDefinitionNode - | EnumValueDefinitionNode - | InputObjectTypeDefinitionNode - | DirectiveDefinitionNode - | SchemaExtensionNode - | ScalarTypeExtensionNode - | ObjectTypeExtensionNode - | InterfaceTypeExtensionNode - | UnionTypeExtensionNode - | EnumTypeExtensionNode - | InputObjectTypeExtensionNode; - -/** - * Utility type listing all nodes indexed by their kind. - */ -export interface ASTKindToNode { - Name: NameNode; - Document: DocumentNode; - OperationDefinition: OperationDefinitionNode; - VariableDefinition: VariableDefinitionNode; - Variable: VariableNode; - SelectionSet: SelectionSetNode; - Field: FieldNode; - Argument: ArgumentNode; - FragmentSpread: FragmentSpreadNode; - InlineFragment: InlineFragmentNode; - FragmentDefinition: FragmentDefinitionNode; - IntValue: IntValueNode; - FloatValue: FloatValueNode; - StringValue: StringValueNode; - BooleanValue: BooleanValueNode; - NullValue: NullValueNode; - EnumValue: EnumValueNode; - ListValue: ListValueNode; - ObjectValue: ObjectValueNode; - ObjectField: ObjectFieldNode; - Directive: DirectiveNode; - NamedType: NamedTypeNode; - ListType: ListTypeNode; - NonNullType: NonNullTypeNode; - SchemaDefinition: SchemaDefinitionNode; - OperationTypeDefinition: OperationTypeDefinitionNode; - ScalarTypeDefinition: ScalarTypeDefinitionNode; - ObjectTypeDefinition: ObjectTypeDefinitionNode; - FieldDefinition: FieldDefinitionNode; - InputValueDefinition: InputValueDefinitionNode; - InterfaceTypeDefinition: InterfaceTypeDefinitionNode; - UnionTypeDefinition: UnionTypeDefinitionNode; - EnumTypeDefinition: EnumTypeDefinitionNode; - EnumValueDefinition: EnumValueDefinitionNode; - InputObjectTypeDefinition: InputObjectTypeDefinitionNode; - DirectiveDefinition: DirectiveDefinitionNode; - SchemaExtension: SchemaExtensionNode; - ScalarTypeExtension: ScalarTypeExtensionNode; - ObjectTypeExtension: ObjectTypeExtensionNode; - InterfaceTypeExtension: InterfaceTypeExtensionNode; - UnionTypeExtension: UnionTypeExtensionNode; - EnumTypeExtension: EnumTypeExtensionNode; - InputObjectTypeExtension: InputObjectTypeExtensionNode; -} - -// Name - -export interface NameNode { - readonly kind: "Name"; - readonly loc?: Location; - readonly value: string; -} - -// Document - -export interface DocumentNode { - readonly kind: "Document"; - readonly loc?: Location; - readonly definitions: ReadonlyArray; -} - -export type DefinitionNode = - | ExecutableDefinitionNode - | TypeSystemDefinitionNode - | TypeSystemExtensionNode; - -export type ExecutableDefinitionNode = - | OperationDefinitionNode - | FragmentDefinitionNode; - -export interface OperationDefinitionNode { - readonly kind: "OperationDefinition"; - readonly loc?: Location; - readonly operation: OperationTypeNode; - readonly name?: NameNode; - readonly variableDefinitions?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly selectionSet: SelectionSetNode; -} - -export type OperationTypeNode = "query" | "mutation" | "subscription"; - -export interface VariableDefinitionNode { - readonly kind: "VariableDefinition"; - readonly loc?: Location; - readonly variable: VariableNode; - readonly type: TypeNode; - readonly defaultValue?: ValueNode; - readonly directives?: ReadonlyArray; -} - -export interface VariableNode { - readonly kind: "Variable"; - readonly loc?: Location; - readonly name: NameNode; -} - -export interface SelectionSetNode { - kind: "SelectionSet"; - loc?: Location; - selections: ReadonlyArray; -} - -export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode; - -export interface FieldNode { - readonly __type: TypeNode; // [SUPERMASSIVE] Add the return type - readonly kind: "Field"; - readonly loc?: Location; - readonly alias?: NameNode; - readonly name: NameNode; - readonly arguments?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly selectionSet?: SelectionSetNode; -} - -export interface ArgumentNode { - readonly __type: TypeNode; // [SUPERMASSIVE] Add the value type - readonly __defaultValue?: ValueNode | null; // [SUPERMASSIVE] Add default value - readonly kind: "Argument"; - readonly loc?: Location; - readonly name: NameNode; - readonly value: ValueNode; -} - -// Fragments - -export interface FragmentSpreadNode { - readonly kind: "FragmentSpread"; - readonly loc?: Location; - readonly name: NameNode; - readonly directives?: ReadonlyArray; -} - -export interface InlineFragmentNode { - readonly kind: "InlineFragment"; - readonly loc?: Location; - readonly typeCondition?: NamedTypeNode; - readonly directives?: ReadonlyArray; - readonly selectionSet: SelectionSetNode; -} - -export interface FragmentDefinitionNode { - readonly kind: "FragmentDefinition"; - readonly loc?: Location; - readonly name: NameNode; - // Note: fragment variable definitions are experimental and may be changed - // or removed in the future. - readonly variableDefinitions?: ReadonlyArray; - readonly typeCondition: NamedTypeNode; - readonly directives?: ReadonlyArray; - readonly selectionSet: SelectionSetNode; -} - -// Values - -export type ValueNode = - | VariableNode - | IntValueNode - | FloatValueNode - | StringValueNode - | BooleanValueNode - | NullValueNode - | EnumValueNode - | ListValueNode - | ObjectValueNode; - -export interface IntValueNode { - readonly kind: "IntValue"; - readonly loc?: Location; - readonly value: string; -} - -export interface FloatValueNode { - readonly kind: "FloatValue"; - readonly loc?: Location; - readonly value: string; -} - -export interface StringValueNode { - readonly kind: "StringValue"; - readonly loc?: Location; - readonly value: string; - readonly block?: boolean; -} - -export interface BooleanValueNode { - readonly kind: "BooleanValue"; - readonly loc?: Location; - readonly value: boolean; -} - -export interface NullValueNode { - readonly kind: "NullValue"; - readonly loc?: Location; -} - -export interface EnumValueNode { - readonly kind: "EnumValue"; - readonly loc?: Location; - readonly value: string; -} - -export interface ListValueNode { - readonly kind: "ListValue"; - readonly loc?: Location; - readonly values: ReadonlyArray; -} - -export interface ObjectValueNode { - readonly kind: "ObjectValue"; - readonly loc?: Location; - readonly fields: ReadonlyArray; -} - -export interface ObjectFieldNode { - readonly kind: "ObjectField"; - readonly loc?: Location; - readonly name: NameNode; - readonly value: ValueNode; -} - -// Directives - -export interface DirectiveNode { - readonly kind: "Directive"; - readonly loc?: Location; - readonly name: NameNode; - readonly arguments?: ReadonlyArray; -} - -// Type Reference - -export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; - -export interface NamedTypeNode { - readonly kind: "NamedType"; - readonly loc?: Location; - readonly name: NameNode; -} - -export interface ListTypeNode { - readonly kind: "ListType"; - readonly loc?: Location; - readonly type: TypeNode; -} - -export interface NonNullTypeNode { - readonly kind: "NonNullType"; - readonly loc?: Location; - readonly type: NamedTypeNode | ListTypeNode; -} - -// Type System Definition - -export type TypeSystemDefinitionNode = - | SchemaDefinitionNode - | TypeDefinitionNode - | DirectiveDefinitionNode; - -export interface SchemaDefinitionNode { - readonly kind: "SchemaDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly directives?: ReadonlyArray; - readonly operationTypes: ReadonlyArray; -} - -export interface OperationTypeDefinitionNode { - readonly kind: "OperationTypeDefinition"; - readonly loc?: Location; - readonly operation: OperationTypeNode; - readonly type: NamedTypeNode; -} - -// Type Definition - -export type TypeDefinitionNode = - | ScalarTypeDefinitionNode - | ObjectTypeDefinitionNode - | InterfaceTypeDefinitionNode - | UnionTypeDefinitionNode - | EnumTypeDefinitionNode - | InputObjectTypeDefinitionNode; - -export interface ScalarTypeDefinitionNode { - readonly kind: "ScalarTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly directives?: ReadonlyArray; -} - -export interface ObjectTypeDefinitionNode { - readonly kind: "ObjectTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} - -export interface FieldDefinitionNode { - readonly kind: "FieldDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly arguments?: ReadonlyArray; - readonly type: TypeNode; - readonly directives?: ReadonlyArray; -} - -export interface InputValueDefinitionNode { - readonly kind: "InputValueDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly type: TypeNode; - readonly defaultValue?: ValueNode; - readonly directives?: ReadonlyArray; -} - -export interface InterfaceTypeDefinitionNode { - readonly kind: "InterfaceTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} - -export interface UnionTypeDefinitionNode { - readonly kind: "UnionTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly types?: ReadonlyArray; -} - -export interface EnumTypeDefinitionNode { - readonly kind: "EnumTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly values?: ReadonlyArray; -} - -export interface EnumValueDefinitionNode { - readonly kind: "EnumValueDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly directives?: ReadonlyArray; -} - -export interface InputObjectTypeDefinitionNode { - readonly kind: "InputObjectTypeDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} - -// Directive Definitions - -export interface DirectiveDefinitionNode { - readonly kind: "DirectiveDefinition"; - readonly loc?: Location; - readonly description?: StringValueNode; - readonly name: NameNode; - readonly arguments?: ReadonlyArray; - readonly repeatable: boolean; - readonly locations: ReadonlyArray; -} - -// Type System Extensions - -export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; - -export interface SchemaExtensionNode { - readonly kind: "SchemaExtension"; - readonly loc?: Location; - readonly directives?: ReadonlyArray; - readonly operationTypes?: ReadonlyArray; -} - -// Type Extensions - -export type TypeExtensionNode = - | ScalarTypeExtensionNode - | ObjectTypeExtensionNode - | InterfaceTypeExtensionNode - | UnionTypeExtensionNode - | EnumTypeExtensionNode - | InputObjectTypeExtensionNode; - -export interface ScalarTypeExtensionNode { - readonly kind: "ScalarTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly directives?: ReadonlyArray; -} - -export interface ObjectTypeExtensionNode { - readonly kind: "ObjectTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} - -export interface InterfaceTypeExtensionNode { - readonly kind: "InterfaceTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly interfaces?: ReadonlyArray; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} - -export interface UnionTypeExtensionNode { - readonly kind: "UnionTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly types?: ReadonlyArray; -} - -export interface EnumTypeExtensionNode { - readonly kind: "EnumTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly values?: ReadonlyArray; -} - -export interface InputObjectTypeExtensionNode { - readonly kind: "InputObjectTypeExtension"; - readonly loc?: Location; - readonly name: NameNode; - readonly directives?: ReadonlyArray; - readonly fields?: ReadonlyArray; -} diff --git a/packages/supermassive/src/legacyAST/__tests__/addTypesToRequestDocument.test.ts b/packages/supermassive/src/legacyAST/__tests__/addTypesToRequestDocument.test.ts deleted file mode 100644 index cffb873e0..000000000 --- a/packages/supermassive/src/legacyAST/__tests__/addTypesToRequestDocument.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { - addTypesToRequestDocument, - FieldNode, - OperationDefinitionNode, -} from "../addTypesToRequestDocument"; - -import { graphql } from "@graphitation/graphql-js-tag"; -import { buildASTSchema } from "graphql"; -import { FragmentDefinitionNode, InlineFragmentNode } from "../TypedAST"; - -const schema = buildASTSchema(graphql` - type Query { - film(id: ID!): Film - allFilms: [Film] - } - - type Mutation { - createFilm( - input: CreateFilmInput! = { filmType: GOOD, title: "Default" } - enumInput: FilmType! - ): Film - } - - type Film { - title(foo: String = "Bar"): String! - actors: [String!] - } - - enum FilmType { - GOOD - BAD - } - - input CreateFilmInput { - title: String! - filmType: FilmType! - } -`); - -describe(addTypesToRequestDocument, () => { - describe("concerning field selections", () => { - it("adds a named type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - query { - film(id: 42) { - title - } - } - `, - ); - - const operationNode = document.definitions[0] as OperationDefinitionNode; - const fieldNode = operationNode.selectionSet.selections[0] as FieldNode; - - expect(fieldNode.__type).toMatchInlineSnapshot(` - { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "Film", - }, - } - `); - }); - - it("adds a non-null type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - fragment FilmFragment on Film { - title - } - `, - ); - - const fragmentNode = document.definitions[0] as FragmentDefinitionNode; - const fieldNode = fragmentNode.selectionSet.selections[0] as FieldNode; - - expect(fieldNode.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "String", - }, - }, - } - `); - }); - - it("adds a list type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - query { - ... on Query { - allFilms { - title - } - } - } - `, - ); - - const operationNode = document.definitions[0] as OperationDefinitionNode; - const inlineFragmentNode = operationNode.selectionSet - .selections[0] as InlineFragmentNode; - const fieldNode = inlineFragmentNode.selectionSet - .selections[0] as FieldNode; - - expect(fieldNode.__type).toMatchInlineSnapshot(` - { - "kind": "ListType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "Film", - }, - }, - } - `); - }); - - it("adds a list type with non-null type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - fragment FilmFragment on Film { - actors - } - `, - ); - - const fragmentNode = document.definitions[0] as FragmentDefinitionNode; - const fieldNode = fragmentNode.selectionSet.selections[0] as FieldNode; - - expect(fieldNode.__type).toMatchInlineSnapshot(` - { - "kind": "ListType", - "type": { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "String", - }, - }, - }, - } - `); - }); - }); - - describe("concerning field arguments", () => { - it("adds a scalar type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - query { - film(id: 42) { - title - } - } - `, - ); - - const operationNode = document.definitions[0] as OperationDefinitionNode; - const fieldNode = operationNode.selectionSet.selections[0] as FieldNode; - const argumentNode = fieldNode.arguments?.[0]; - - expect(argumentNode?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "ID", - }, - }, - } - `); - }); - - it("adds an input object type node", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - mutation Test($filmType: FilmType) { - createFilm( - input: { title: "The Phantom Menace", filmType: $filmType } - enumInput: $filmType - ) { - title - } - } - `, - ); - - const operationNode = document.definitions[0] as OperationDefinitionNode; - const fieldNode = operationNode.selectionSet.selections[0] as FieldNode; - const argumentNode = fieldNode.arguments?.[0]; - - expect(argumentNode?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "CreateFilmInput", - }, - }, - } - `); - - expect(fieldNode.arguments?.[1]?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "FilmType", - }, - }, - } - `); - - const secondArgument = fieldNode.arguments?.[1]; - - expect(secondArgument?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "FilmType", - }, - }, - } - `); - - expect(secondArgument?.__defaultValue).toMatchInlineSnapshot(`undefined`); - }); - - it("adds missing types with default values", () => { - const document = addTypesToRequestDocument( - schema, - graphql` - mutation { - createFilm(enumInput: GOOD) { - title - } - } - `, - ); - - const operationNode = document.definitions[0] as OperationDefinitionNode; - const fieldNode = operationNode.selectionSet.selections[0] as FieldNode; - const argumentNode = fieldNode.arguments?.[0]; - - expect(argumentNode?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "FilmType", - }, - }, - } - `); - - expect(fieldNode.arguments?.[1]?.__type).toMatchInlineSnapshot(` - { - "kind": "NonNullType", - "type": { - "kind": "NamedType", - "name": { - "kind": "Name", - "value": "CreateFilmInput", - }, - }, - } - `); - }); - - it("errors nicely for unknown fields", () => { - expect(() => { - addTypesToRequestDocument( - schema, - graphql` - query filmQuery { - film(id: 42) { - title - format - } - } - `, - ); - }).toThrowError( - "Cannot find type for field: query filmQuery.film.format", - ); - - expect(() => { - addTypesToRequestDocument( - schema, - graphql` - query { - film(id: 42) { - title - format - } - } - `, - ); - }).toThrowError("Cannot find type for field: query.film.format"); - - expect(() => { - addTypesToRequestDocument( - schema, - graphql` - query { - film(ido: 42) { - title - } - } - `, - ); - }).toThrowError("Cannot find type for argument: query.film.ido"); - }); - }); -}); diff --git a/packages/supermassive/src/legacyAST/addTypesToRequestDocument.ts b/packages/supermassive/src/legacyAST/addTypesToRequestDocument.ts deleted file mode 100644 index 9894ed4bc..000000000 --- a/packages/supermassive/src/legacyAST/addTypesToRequestDocument.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - GraphQLType, - GraphQLSchema, - isListType, - isNamedType, - isNonNullType, - TypeInfo, - visit, - visitWithTypeInfo, - Kind, - astFromValue, - GraphQLArgument, -} from "graphql"; - -import * as TypelessAST from "graphql/language/ast"; -import * as TypedAST from "./TypedAST"; -export * from "./TypedAST"; - -export function addTypesToRequestDocument( - schema: GraphQLSchema, - document: TypelessAST.DocumentNode, -): TypedAST.DocumentNode { - const typeInfo = new TypeInfo(schema); - return visit( - document, - visitWithTypeInfo(typeInfo, { - Argument: { - leave(node, _key, _parent, _path, ancestors) { - const argument = typeInfo.getArgument(); - if (argument) { - const typeNode = generateTypeNode(argument.type); - const newNode: TypedAST.ArgumentNode = { - ...node, - __type: typeNode, - }; - // We only need default value for arguments with variable values - if (argument.defaultValue && node.value.kind === Kind.VARIABLE) { - (newNode.__defaultValue as - | TypedAST.ValueNode - | null - | undefined) = astFromValue( - argument.defaultValue, - argument.type, - ); - } - return newNode; - } - const errorPath = makeReadableErrorPath(ancestors); - throw new Error( - `Cannot find type for argument: ${errorPath.join(".")}.${ - node.name.value - }`, - ); - }, - }, - Field: { - leave( - node: Omit, - _key, - _parent, - _path, - ancestors, - ) { - const fieldDef = typeInfo.getFieldDef(); - if (fieldDef) { - const type = fieldDef.type; - if (type) { - const typeNode = generateTypeNode(type); - const missingArgs: Array = fieldDef.args.filter( - (argDef) => - argDef.defaultValue != null && - node.arguments?.findIndex( - (arg) => arg.name.value === argDef.name, - ) === -1, - ); - const newNode: TypedAST.FieldNode = { - ...(node as Omit< - TypelessAST.FieldNode, - "selectionSet" | "arguments" | "directives" - >), - __type: typeNode, - }; - if (missingArgs) { - (newNode.arguments as TypedAST.ArgumentNode[]) = ( - newNode.arguments || [] - ).concat( - missingArgs.map((arg) => ({ - __type: generateTypeNode(arg.type), - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: arg.name, - }, - value: astFromValue( - arg.defaultValue, - arg.type, - ) as TypedAST.ValueNode, - })), - ); - } - return newNode; - } - } - - const errorPath = makeReadableErrorPath(ancestors); - - // This happens whenever a new field is requested that hasn't been defined in schema - throw new Error( - `Cannot find type for field: ${errorPath.join(".")}.${ - node.name.value - }`, - ); - }, - }, - }), - ); -} - -function generateTypeNode(type: GraphQLType): TypedAST.TypeNode { - if (isNonNullType(type)) { - const typeNode = generateTypeNode(type.ofType) as - | TypedAST.NamedTypeNode - | TypedAST.ListTypeNode; - return { - kind: "NonNullType", - type: typeNode, - }; - } else if (isListType(type)) { - const typeNode = generateTypeNode(type.ofType) as - | TypedAST.NamedTypeNode - | TypedAST.NonNullTypeNode; - return { - kind: "ListType", - type: typeNode, - }; - } else if (isNamedType(type)) { - return { - kind: "NamedType", - name: { - kind: "Name", - value: type.name, - }, - }; - } - throw new Error(`Can't generate TypeNode for type: ${type}`); -} - -function makeReadableErrorPath( - ancestors: ReadonlyArray< - readonly TypelessAST.ASTNode[] | TypelessAST.ASTNode - >, -): string[] { - const path: string[] = []; - ancestors.forEach((ancestorOrArray) => { - let ancestor: TypelessAST.ASTNode; - if (!Array.isArray(ancestorOrArray)) { - ancestor = ancestorOrArray as TypelessAST.ASTNode; - if (ancestor && ancestor.kind === Kind.FIELD) { - path.push(ancestor.name.value); - } else if (ancestor && ancestor.kind === Kind.OPERATION_DEFINITION) { - let name; - if (ancestor.name) { - name = `${ancestor.operation} ${ancestor.name.value}`; - } else { - name = ancestor.operation; - } - path.push(name); - } - } - }); - return path; -} diff --git a/packages/supermassive/src/legacyAST/annotateDocumentGraphQLTransform.ts b/packages/supermassive/src/legacyAST/annotateDocumentGraphQLTransform.ts deleted file mode 100644 index ffaa79870..000000000 --- a/packages/supermassive/src/legacyAST/annotateDocumentGraphQLTransform.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - FragmentDefinitionNode, - GraphQLSchema, - Kind, - OperationDefinitionNode, -} from "graphql"; -import { addTypesToRequestDocument } from "./addTypesToRequestDocument"; - -export const annotateDocumentGraphQLTransform = (schema: GraphQLSchema) => { - return (node: FragmentDefinitionNode | OperationDefinitionNode) => { - const document = addTypesToRequestDocument(schema, { - kind: Kind.DOCUMENT, - definitions: [node], - }); - return document.definitions[0]; - }; -}; diff --git a/packages/supermassive/src/legacyAST/index.ts b/packages/supermassive/src/legacyAST/index.ts deleted file mode 100644 index 78fca5da0..000000000 --- a/packages/supermassive/src/legacyAST/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type { - NameNode, - DocumentNode, - OperationDefinitionNode, - VariableDefinitionNode, - VariableNode, - SelectionSetNode, - FieldNode, - ArgumentNode, - FragmentSpreadNode, - InlineFragmentNode, - FragmentDefinitionNode, - IntValueNode, - FloatValueNode, - StringValueNode, - BooleanValueNode, - NullValueNode, - EnumValueNode, - ListValueNode, - ObjectValueNode, - ObjectFieldNode, - DirectiveNode, - NamedTypeNode, - ListTypeNode, - NonNullTypeNode, - SchemaDefinitionNode, - OperationTypeDefinitionNode, - ScalarTypeDefinitionNode, - ObjectTypeDefinitionNode, - FieldDefinitionNode, - InputValueDefinitionNode, - InterfaceTypeDefinitionNode, - UnionTypeDefinitionNode, - EnumTypeDefinitionNode, - EnumValueDefinitionNode, - InputObjectTypeDefinitionNode, - DirectiveDefinitionNode, - SchemaExtensionNode, - ScalarTypeExtensionNode, - ObjectTypeExtensionNode, - InterfaceTypeExtensionNode, - UnionTypeExtensionNode, - EnumTypeExtensionNode, - InputObjectTypeExtensionNode, - SelectionNode, - TypeNode, -} from "./TypedAST"; -export { addTypesToRequestDocument } from "./addTypesToRequestDocument"; -export { annotateDocumentGraphQLTransform } from "./annotateDocumentGraphQLTransform"; diff --git a/packages/supermassive/src/schema/definition.ts b/packages/supermassive/src/schema/definition.ts index 9de6f5796..27aea94a7 100644 --- a/packages/supermassive/src/schema/definition.ts +++ b/packages/supermassive/src/schema/definition.ts @@ -1,7 +1,4 @@ -import { - DirectiveLocation as GraphQLDirectiveLocation, - DirectiveLocationEnum, -} from "graphql"; +import { DirectiveLocation as GraphQLDirectiveLocation } from "graphql"; import { isSpecifiedScalarType } from "./resolvers"; import { TypeName, TypeReference, typeNameFromReference } from "./reference"; @@ -432,13 +429,13 @@ export function getDirectiveLocations( } export function encodeDirectiveLocation( - location: DirectiveLocationEnum, + location: GraphQLDirectiveLocation, ): DirectiveLocation { return DirectiveLocation[location]; } export function decodeDirectiveLocation( location: DirectiveLocation, -): DirectiveLocationEnum { +): GraphQLDirectiveLocation { return DirectiveLocationToGraphQL[location]; } diff --git a/packages/supermassive/src/types.ts b/packages/supermassive/src/types.ts index 4dca9c2cb..d4dbbf2a2 100644 --- a/packages/supermassive/src/types.ts +++ b/packages/supermassive/src/types.ts @@ -92,7 +92,9 @@ export type UserResolvers = Record< export interface ResolveInfo { fieldName: string; + /** @deprecated */ fieldNodes: FieldGroup; + fieldGroup: FieldGroup; returnTypeName: string; parentTypeName: string; // readonly returnType: GraphQLOutputType; @@ -136,15 +138,6 @@ export type SubscriptionExecutionResult< TExtensions = ObjMap, > = AsyncGenerator>; -export interface FormattedTotalExecutionResult< - TData = ObjMap, - TExtensions = ObjMap, -> { - errors?: ReadonlyArray; - data?: TData | null; - extensions?: TExtensions; -} - export interface IncrementalExecutionResult< TData = ObjMap, TExtensions = ObjMap, @@ -156,95 +149,65 @@ export interface IncrementalExecutionResult< void >; } - export interface InitialIncrementalExecutionResult< TData = ObjMap, TExtensions = ObjMap, > extends TotalExecutionResult { - hasNext: boolean; - incremental?: ReadonlyArray>; + pending: ReadonlyArray; + hasNext: true; extensions?: TExtensions; } -export interface FormattedInitialIncrementalExecutionResult< - TData = ObjMap, +export interface SubsequentIncrementalExecutionResult< + TData = unknown, TExtensions = ObjMap, -> extends FormattedTotalExecutionResult { +> { + pending?: ReadonlyArray; + incremental?: ReadonlyArray>; + completed?: ReadonlyArray; hasNext: boolean; - incremental?: ReadonlyArray>; extensions?: TExtensions; } -export interface SubsequentIncrementalExecutionResult< +export interface IncrementalDeferResult< TData = ObjMap, TExtensions = ObjMap, > { - hasNext: boolean; - incremental?: ReadonlyArray>; + id: string; + subPath?: ReadonlyArray; extensions?: TExtensions; + errors?: ReadonlyArray; + data: TData; } -export interface FormattedSubsequentIncrementalExecutionResult< +export interface IncrementalStreamResult< TData = ObjMap, TExtensions = ObjMap, > { - hasNext: boolean; - incremental?: ReadonlyArray>; + id: string; + subPath?: ReadonlyArray; extensions?: TExtensions; + errors?: ReadonlyArray; + items: ReadonlyArray; } -export interface IncrementalDeferResult< - TData = ObjMap, +export type IncrementalResult< + TData = unknown, TExtensions = ObjMap, -> extends TotalExecutionResult { - path?: ReadonlyArray; - label?: string; -} +> = IncrementalDeferResult; +// | IncrementalStreamResult; -export interface FormattedIncrementalDeferResult< - TData = ObjMap, - TExtensions = ObjMap, -> extends FormattedTotalExecutionResult { - path?: ReadonlyArray; +export interface PendingResult { + id: string; + path: ReadonlyArray; label?: string; } -export interface IncrementalStreamResult< - TData = Array, - TExtensions = ObjMap, -> { +export interface CompletedResult { + id: string; errors?: ReadonlyArray; - items?: TData | null; - path?: ReadonlyArray; - label?: string; - extensions?: TExtensions; -} - -export interface FormattedIncrementalStreamResult< - TData = Array, - TExtensions = ObjMap, -> { - errors?: ReadonlyArray; - items?: TData | null; - path?: ReadonlyArray; - label?: string; - extensions?: TExtensions; } -export type IncrementalResult< - TData = ObjMap, - TExtensions = ObjMap, -> = - | IncrementalDeferResult - | IncrementalStreamResult; - -export type FormattedIncrementalResult< - TData = ObjMap, - TExtensions = ObjMap, -> = - | FormattedIncrementalDeferResult - | FormattedIncrementalStreamResult; - export interface CommonExecutionArgs { document: DocumentNode; rootValue?: unknown; diff --git a/packages/supermassive/src/utilities/encodeASTSchema.ts b/packages/supermassive/src/utilities/encodeASTSchema.ts index a8b1511a6..8c6923135 100644 --- a/packages/supermassive/src/utilities/encodeASTSchema.ts +++ b/packages/supermassive/src/utilities/encodeASTSchema.ts @@ -15,7 +15,7 @@ import { UnionTypeExtensionNode, EnumTypeExtensionNode, ScalarTypeExtensionNode, - DirectiveLocationEnum, + DirectiveLocation, } from "graphql"; import { DirectiveDefinitionTuple, @@ -200,7 +200,7 @@ function encodeDirective( return [ node.name.value, node.locations.map((node) => - encodeDirectiveLocation(node.value as DirectiveLocationEnum), + encodeDirectiveLocation(node.value as DirectiveLocation), ), encodeArguments(node), ]; @@ -208,7 +208,7 @@ function encodeDirective( return [ node.name.value, node.locations.map((node) => - encodeDirectiveLocation(node.value as DirectiveLocationEnum), + encodeDirectiveLocation(node.value as DirectiveLocation), ), ]; } diff --git a/yarn.lock b/yarn.lock index c16389cc1..2437ad493 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7390,6 +7390,11 @@ graphql@16.8.1: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== +graphql@17.0.0-alpha.7: + version "17.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-17.0.0-alpha.7.tgz#707e7457d7ed5316a8d7940f78809a2eb5854383" + integrity sha512-kdteHez9s0lfNAGntSwnDBpxSl09sBWEFxFRPS/Z8K1nCD4FZ2wVGwXuj5dvrTKcqOA+O8ujAJ3CiY/jXhs14g== + graphql@^14.5.3: version "14.7.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"