Skip to content

Commit

Permalink
add helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
yaacovCR committed Jul 31, 2023
1 parent 1e6e4b0 commit 3820d60
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 37 deletions.
50 changes: 49 additions & 1 deletion src/execution/IncrementalPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,12 +615,16 @@ export class IncrementalPublisher {
}

this._introduce(subsequentResultRecord);
subsequentResultRecord.publish();
return;
}

if (subsequentResultRecord._pending.size === 0) {
this._push(subsequentResultRecord);
} else {
for (const deferredGroupedFieldSetRecord of subsequentResultRecord.deferredGroupedFieldSetRecords) {
deferredGroupedFieldSetRecord.publish();
}
this._introduce(subsequentResultRecord);
}
}
Expand Down Expand Up @@ -701,33 +705,56 @@ function isStreamItemsRecord(
export class InitialResultRecord {
errors: Array<GraphQLError>;
children: Set<SubsequentResultRecord>;
priority: number;
deferPriority: number;
published: true;
constructor() {
this.errors = [];
this.children = new Set();
this.priority = 0;
this.deferPriority = 0;
this.published = true;
}
}

/** @internal */
export class DeferredGroupedFieldSetRecord {
path: ReadonlyArray<string | number>;
priority: number;
deferPriority: number;
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
groupedFieldSet: GroupedFieldSet;
shouldInitiateDefer: boolean;
errors: Array<GraphQLError>;
data: ObjMap<unknown> | undefined;
published: true | Promise<void>;
publish: () => void;
sent: boolean;

constructor(opts: {
path: Path | undefined;
priority: number;
deferPriority: number;
deferredFragmentRecords: ReadonlyArray<DeferredFragmentRecord>;
groupedFieldSet: GroupedFieldSet;
shouldInitiateDefer: boolean;
}) {
this.path = pathToArray(opts.path);
this.priority = opts.priority;
this.deferPriority = opts.deferPriority;
this.deferredFragmentRecords = opts.deferredFragmentRecords;
this.groupedFieldSet = opts.groupedFieldSet;
this.shouldInitiateDefer = opts.shouldInitiateDefer;
this.errors = [];
// promiseWithResolvers uses void only as a generic type parameter
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const { promise: published, resolve } = promiseWithResolvers<void>();
this.published = published;
this.publish = () => {
resolve();
this.published = true;
};
this.sent = false;
}
}
Expand Down Expand Up @@ -778,21 +805,42 @@ export class StreamItemsRecord {
errors: Array<GraphQLError>;
streamRecord: StreamRecord;
path: ReadonlyArray<string | number>;
priority: number;
deferPriority: number;
items: Array<unknown>;
children: Set<SubsequentResultRecord>;
isFinalRecord?: boolean;
isCompletedAsyncIterator?: boolean;
isCompleted: boolean;
filtered: boolean;
published: true | Promise<void>;
publish: () => void;
sent: boolean;

constructor(opts: { streamRecord: StreamRecord; path: Path | undefined }) {
constructor(opts: {
streamRecord: StreamRecord;
path: Path | undefined;
priority: number;
}) {
this.streamRecord = opts.streamRecord;
this.path = pathToArray(opts.path);
this.priority = opts.priority;
this.deferPriority = 0;
this.children = new Set();
this.errors = [];
this.isCompleted = false;
this.filtered = false;
this.items = [];
// promiseWithResolvers uses void only as a generic type parameter
// see: https://typescript-eslint.io/rules/no-invalid-void-type/
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
const { promise: published, resolve } = promiseWithResolvers<void>();
this.published = published;
this.publish = () => {
resolve();
this.published = true;
};
this.sent = false;
}
}

Expand Down
174 changes: 173 additions & 1 deletion src/execution/__tests__/defer-test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { expect } from 'chai';
import { assert, expect } from 'chai';
import { describe, it } from 'mocha';

import { expectJSON } from '../../__testUtils__/expectJSON.js';
import { expectPromise } from '../../__testUtils__/expectPromise.js';
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';

import { isPromise } from '../../jsutils/isPromise.js';

import type { DocumentNode } from '../../language/ast.js';
import { Kind } from '../../language/kinds.js';
import { parse } from '../../language/parser.js';

import type { FieldDetails } from '../../type/definition.js';
import {
GraphQLList,
GraphQLNonNull,
Expand Down Expand Up @@ -226,6 +230,174 @@ describe('Execute: defer directive', () => {
},
});
});
it('Can provides correct info about deferred execution state when resolver could defer', async () => {
let fieldDetails: ReadonlyArray<FieldDetails> | undefined;
let deferPriority;
let published;
let resumed;

const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
someField: {
type: GraphQLString,
resolve: () => Promise.resolve('someField'),
},
deferredField: {
type: GraphQLString,
resolve: async (_parent, _args, _context, info) => {
fieldDetails = info.fieldDetails;
deferPriority = info.deferPriority;
published = info.published;
await published;
resumed = true;
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
someField
... @defer {
deferredField
}
}
`);

const operation = document.definitions[0];
assert(operation.kind === Kind.OPERATION_DEFINITION);
const fragment = operation.selectionSet.selections[1];
assert(fragment.kind === Kind.INLINE_FRAGMENT);
const field = fragment.selectionSet.selections[0];

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

expect(fieldDetails).to.equal(undefined);
expect(deferPriority).to.equal(undefined);
expect(published).to.equal(undefined);
expect(resumed).to.equal(undefined);

const initialPayload = await result;
assert('initialResult' in initialPayload);
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
await iterator.next();

assert(fieldDetails !== undefined);
expect(fieldDetails[0].node).to.equal(field);
expect(fieldDetails[0].target?.deferPriority).to.equal(1);
expect(deferPriority).to.equal(1);
expect(isPromise(published)).to.equal(true);
expect(resumed).to.equal(true);
});
it('Can provides correct info about deferred execution state when deferred field is masked by non-deferred field', async () => {
let fieldDetails: ReadonlyArray<FieldDetails> | undefined;
let deferPriority;
let published;

const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
someField: {
type: GraphQLString,
resolve: (_parent, _args, _context, info) => {
fieldDetails = info.fieldDetails;
deferPriority = info.deferPriority;
published = info.published;
return 'someField';
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
someField
... @defer {
someField
}
}
`);

const operation = document.definitions[0];
assert(operation.kind === Kind.OPERATION_DEFINITION);
const node1 = operation.selectionSet.selections[0];
const fragment = operation.selectionSet.selections[1];
assert(fragment.kind === Kind.INLINE_FRAGMENT);
const node2 = fragment.selectionSet.selections[0];

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

const initialPayload = await result;
assert('initialResult' in initialPayload);
expect(initialPayload.initialResult).to.deep.equal({
data: {
someField: 'someField',
},
pending: [{ path: [] }],
hasNext: true,
});

assert(fieldDetails !== undefined);
expect(fieldDetails[0].node).to.equal(node1);
expect(fieldDetails[0].target).to.equal(undefined);
expect(fieldDetails[1].node).to.equal(node2);
expect(fieldDetails[1].target?.deferPriority).to.equal(1);
expect(deferPriority).to.equal(0);
expect(published).to.equal(true);
});
it('Can provides correct info about deferred execution state when resolver need not defer', async () => {
let deferPriority;
let published;
const SomeType = new GraphQLObjectType({
name: 'SomeType',
fields: {
deferredField: {
type: GraphQLString,
resolve: (_parent, _args, _context, info) => {
deferPriority = info.deferPriority;
published = info.published;
},
},
},
});

const someSchema = new GraphQLSchema({ query: SomeType });

const document = parse(`
query {
... @defer {
deferredField
}
}
`);

const result = experimentalExecuteIncrementally({
schema: someSchema,
document,
});

expect(deferPriority).to.equal(undefined);
expect(published).to.equal(undefined);

const initialPayload = await result;
assert('initialResult' in initialPayload);
const iterator = initialPayload.subsequentResults[Symbol.asyncIterator]();
await iterator.next();

expect(deferPriority).to.equal(1);
expect(published).to.equal(true);
});
it('Does not disable defer with null if argument', async () => {
const document = parse(`
query HeroNameQuery($shouldDefer: Boolean) {
Expand Down
22 changes: 18 additions & 4 deletions src/execution/__tests__/executor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { inspect } from '../../jsutils/inspect.js';
import { Kind } from '../../language/kinds.js';
import { parse } from '../../language/parser.js';

import type { GraphQLResolveInfo } from '../../type/definition.js';
import {
GraphQLInterfaceType,
GraphQLList,
Expand Down Expand Up @@ -191,7 +192,7 @@ describe('Execute: Handles basic execution tasks', () => {
});

it('provides info about current execution state', () => {
let resolvedInfo;
let resolvedInfo: GraphQLResolveInfo | undefined;
const testType = new GraphQLObjectType({
name: 'Test',
fields: {
Expand All @@ -213,7 +214,7 @@ describe('Execute: Handles basic execution tasks', () => {

expect(resolvedInfo).to.have.all.keys(
'fieldName',
'fieldNodes',
'fieldDetails',
'returnType',
'parentType',
'path',
Expand All @@ -222,6 +223,9 @@ describe('Execute: Handles basic execution tasks', () => {
'rootValue',
'operation',
'variableValues',
'priority',
'deferPriority',
'published',
);

const operation = document.definitions[0];
Expand All @@ -234,14 +238,24 @@ describe('Execute: Handles basic execution tasks', () => {
schema,
rootValue,
operation,
priority: 0,
deferPriority: 0,
published: true,
});

const field = operation.selectionSet.selections[0];
expect(resolvedInfo).to.deep.include({
fieldNodes: [field],
path: { prev: undefined, key: 'result', typename: 'Test' },
variableValues: { var: 'abc' },
});

const fieldDetails = resolvedInfo?.fieldDetails;
assert(fieldDetails !== undefined);

const field = operation.selectionSet.selections[0];
expect(fieldDetails[0]).to.deep.include({
node: field,
target: undefined,
});
});

it('populates path correctly with complex types', () => {
Expand Down
Loading

0 comments on commit 3820d60

Please sign in to comment.