Skip to content

Commit

Permalink
fix(url-loader): @stream support
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Nov 2, 2022
1 parent 7d657f8 commit d83b196
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/plenty-squids-tie.md
@@ -0,0 +1,6 @@
---
'@graphql-tools/url-loader': patch
'@graphql-tools/utils': patch
---

Fix @stream support
12 changes: 10 additions & 2 deletions packages/loaders/url/src/handleMultipartMixedResponse.ts
Expand Up @@ -39,9 +39,17 @@ export async function handleMultipartMixedResponse(response: Response, controlle
const executionResult: ExecutionResult = {};

function handleResult(result: ExecutionResult) {
if (result.path && result.data) {
if (result.path) {
const path = ['data', ...result.path];
executionResult.data = executionResult.data || {};
dset(executionResult, ['data', ...result.path], result.data);
if (result.items) {
for (const item of result.items) {
dset(executionResult, path, item);
}
}
if (result.data) {
dset(executionResult, ['data', ...result.path], result.data);
}
} else if (result.data) {
executionResult.data = executionResult.data || {};
Object.assign(executionResult.data, result.data);
Expand Down
46 changes: 45 additions & 1 deletion packages/loaders/url/tests/url-loader-browser.spec.ts
Expand Up @@ -22,6 +22,7 @@ describe('[url-loader] webpack bundle compat', () => {
typeDefs: /* GraphQL */ `
type Query {
foo: Boolean
countdown(from: Int): [Int]
}
type Subscription {
foo: Boolean
Expand All @@ -30,6 +31,12 @@ describe('[url-loader] webpack bundle compat', () => {
resolvers: {
Query: {
foo: () => new Promise(resolve => setTimeout(() => resolve(true), 300)),
countdown: async function* (_, { from }) {
for (let i = from; i >= 0; i--) {
yield i;
await new Promise(resolve => setTimeout(resolve, 100));
}
},
},
Subscription: {
foo: {
Expand Down Expand Up @@ -175,7 +182,7 @@ describe('[url-loader] webpack bundle compat', () => {
expect(result).toStrictEqual(expectedData);
});

it('handles executing a operation using multipart responses', async () => {
it('handles executing a @defer operation using multipart responses', async () => {
const document = parse(/* GraphQL */ `
query {
... on Query @defer {
Expand Down Expand Up @@ -206,6 +213,43 @@ describe('[url-loader] webpack bundle compat', () => {
expect(results).toEqual([{ data: {} }, { data: { foo: true } }]);
});

it('handles executing a @stream operation using multipart responses', async () => {
const document = parse(/* GraphQL */ `
query {
countdown(from: 3) @stream
}
`);

const results = await page.evaluate(
async (httpAddress, document) => {
const module = window['GraphQLToolsUrlLoader'] as typeof UrlLoaderModule;
const loader = new module.UrlLoader();
const executor = loader.getExecutorAsync(httpAddress + '/graphql');
const result = await executor({
document,
});
const results = [];
for await (const currentResult of result as any[]) {
if (currentResult) {
results.push(JSON.parse(JSON.stringify(currentResult)));
}
}
return results;
},
httpAddress,
document as any
);

expect(results).toEqual([
{ data: { countdown: [] } },
{ data: { countdown: [3] } },
{ data: { countdown: [3, 2] } },
{ data: { countdown: [3, 2, 1] } },
{ data: { countdown: [3, 2, 1, 0] } },
{ data: { countdown: [3, 2, 1, 0] } },
]);
});

it('handles SSE subscription operations', async () => {
const expectedDatas = [{ data: { foo: true } }, { data: { foo: false } }];

Expand Down
81 changes: 77 additions & 4 deletions packages/loaders/url/tests/yoga-compat.spec.ts
Expand Up @@ -18,7 +18,34 @@ describe('Yoga Compatibility', () => {
let serverPath: string;
let active = false;
let cnt = 0;

const alphabet = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
beforeAll(async () => {
const yoga = createYoga({
schema: createSchema({
Expand All @@ -27,6 +54,11 @@ describe('Yoga Compatibility', () => {
type Query {
foo: Foo
cnt: Int
"""
Resolves the alphabet slowly. 1 character per second
Maybe you want to @stream this field ;)
"""
alphabet(waitFor: Int! = 1000): [String]
}
type Foo {
a: Int
Expand All @@ -40,10 +72,22 @@ describe('Yoga Compatibility', () => {
Query: {
foo: () => ({}),
cnt: () => cnt,
async *alphabet(_, { waitFor }) {
for (const character of alphabet) {
yield character;
await sleep(waitFor);
}
},
},
Foo: {
a: () => new Promise(resolve => setTimeout(() => resolve(1), 300)),
b: () => new Promise(resolve => setTimeout(() => resolve(2), 600)),
a: async () => {
await sleep(300);
return 1;
},
b: async () => {
await sleep(600);
return 2;
},
},
Subscription: {
foo: {
Expand Down Expand Up @@ -91,7 +135,7 @@ describe('Yoga Compatibility', () => {
}
});

it('should handle multipart response result', async () => {
it('should handle defer', async () => {
expect.assertions(5);
const expectedDatas: ExecutionResult[] = [
{
Expand Down Expand Up @@ -138,6 +182,35 @@ describe('Yoga Compatibility', () => {
expect(expectedDatas.length).toBe(0);
});

it('should handle stream', async () => {
const document = parse(/* GraphQL */ `
query StreamAlphabet {
alphabet(waitFor: 100) @stream
}
`);

const executor = loader.getExecutorAsync(serverPath);
const result = await executor({
document,
});

assertAsyncIterable(result);

let i = 0;
let finalResult: ExecutionResult | undefined;
for await (const chunk of result) {
if (chunk) {
expect(chunk.data?.alphabet?.length).toBe(i);
i++;
if (i > alphabet.length) {
finalResult = chunk;
break;
}
}
}
expect(finalResult?.data?.alphabet).toEqual(alphabet);
});

it('should handle SSE subscription result', async () => {
const expectedDatas: ExecutionResult[] = [{ data: { foo: 1 } }, { data: { foo: 2 } }, { data: { foo: 3 } }];

Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/Interfaces.ts
Expand Up @@ -64,6 +64,7 @@ export interface ExecutionResult<TData = any, TExtensions = any> {
extensions?: TExtensions;
label?: string;
path?: ReadonlyArray<string | number>;
items?: TData | null;
}

export interface ExecutionRequest<
Expand Down
19 changes: 18 additions & 1 deletion yarn.lock
Expand Up @@ -1416,7 +1416,7 @@
"@changesets/types" "^5.2.0"
dotenv "^8.1.0"

"@changesets/cli@2.25.2":
"@changesets/cli@2.25.2", "@changesets/cli@^2.16.0":
version "2.25.2"
resolved "https://registry.yarnpkg.com/@changesets/cli/-/cli-2.25.2.tgz#fc5e894aa6f85c60749a035352dec3dcbd275c71"
integrity sha512-ACScBJXI3kRyMd2R8n8SzfttDHi4tmKSwVwXBazJOylQItSRSF4cGmej2E4FVf/eNfGy6THkL9GzAahU9ErZrA==
Expand Down Expand Up @@ -1676,6 +1676,16 @@
dependencies:
giscus "^1.2.0"

"@graphql-tools/executor@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.4.tgz#384aad9260a6dfb644f3f08b114d3f7196afa547"
integrity sha512-EBV3wBslLfYOJERjFHrV2iy2U0XCIsXkDKC383lw2b0mpBHZP8y7s8dDLGLxEvuPgou2qer511O/gqI2NEVUNw==
dependencies:
"@graphql-tools/utils" "9.0.0"
"@graphql-typed-document-node/core" "3.1.1"
"@repeaterjs/repeater" "3.0.4"
value-or-promise "1.0.1"

"@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.8.0":
version "8.13.1"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-8.13.1.tgz#b247607e400365c2cd87ff54654d4ad25a7ac491"
Expand Down Expand Up @@ -12148,6 +12158,13 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"

value-or-promise@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.1.tgz#7021919262c7a13605da701bcbd3c9ae8219bf68"
integrity sha512-luIWMQACiZgNXrrCVX0B1Lm5bTT+osgLG/uiBMVvxYa52oqHGoF9YGpW+azBThx84N6bAm5MyaodRvsWaYmVbQ==
dependencies:
"@changesets/cli" "^2.16.0"

value-or-promise@1.0.11, value-or-promise@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140"
Expand Down

0 comments on commit d83b196

Please sign in to comment.