Skip to content

Commit

Permalink
Support nested promises at any level in arrays. (#59)
Browse files Browse the repository at this point in the history
unpromisify had a special case to handle nested promises in an array so it could handle Array<Promise<any>>.

This change allows promises to be present anywhere.
  • Loading branch information
ruiaraujo committed Aug 9, 2019
1 parent b4f4b08 commit aad8506
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 53 deletions.
99 changes: 96 additions & 3 deletions src/__tests__/lists.test.ts
Expand Up @@ -13,7 +13,7 @@ import {
GraphQLType,
parse
} from "graphql";
import { compileQuery } from "../index";
import { compileQuery, isCompiledQuery } from "../index";

// resolved() is shorthand for Promise.resolve()
const resolved = Promise.resolve.bind(Promise);
Expand Down Expand Up @@ -41,7 +41,10 @@ function check(testType: any, testData: any, expected: any) {
const schema = new GraphQLSchema({ query: dataType });

const ast = parse("{ nest { test } }");
const prepared: any = compileQuery(schema, ast, "");
const prepared = compileQuery(schema, ast, "");
if (!isCompiledQuery(prepared)) {
throw prepared;
}
const response = await prepared.query(data, undefined, {});
expect(response).toEqual(expected);
};
Expand Down Expand Up @@ -559,7 +562,10 @@ describe("Execute: Handles nested lists", () => {
});
const schema = new GraphQLSchema({ query: dataType });
const ast = parse(query || "{ test }");
const prepared: any = compileQuery(schema, ast, "");
const prepared = compileQuery(schema, ast, "");
if (!isCompiledQuery(prepared)) {
throw prepared;
}
const response = await prepared.query(testData, undefined, {});
expect(response).toEqual(expected);
};
Expand All @@ -569,6 +575,25 @@ describe("Execute: Handles nested lists", () => {
"[[Scalar]]",
check(GraphQLString, undefined, [["test"]], { data: { test: [["test"]] } })
);
test(
"[[Promise<Scalar>]]",
check(GraphQLString, undefined, [[Promise.resolve("test")]], {
data: { test: [["test"]] }
})
);
test(
"[[PromiseRejected<Scalar>]]",
check(GraphQLString, undefined, [[Promise.reject("test")]], {
data: { test: [[null]] },
errors: [
{
locations: [{ column: 3, line: 1 }],
message: "test",
path: ["test", 0, 0]
}
]
})
);
test(
"[[Object]]",
check(
Expand All @@ -585,6 +610,74 @@ describe("Execute: Handles nested lists", () => {
{ data: { test: [[{ obj: "test" }]] } }
)
);
test(
"[[Promise<Object>]]",
check(
new GraphQLObjectType({
name: "Object",
fields: () => ({
obj: {
type: GraphQLString
}
})
}),
"{test {obj}}",
[[Promise.resolve({ obj: "test" })]],
{ data: { test: [[{ obj: "test" }]] } }
)
);
test(
"[[PromiseRejected<Object>]]",
check(
new GraphQLObjectType({
name: "Object",
fields: () => ({
obj: {
type: GraphQLString
}
})
}),
"{test {obj}}",
[[{ obj: "test" }, Promise.reject("bad")]],
{
data: { test: [[{ obj: "test" }, null]] },
errors: [
{
locations: [{ column: 2, line: 1 }],
message: "bad",
path: ["test", 0, 1]
}
]
}
)
);
test(
"[[PromiseRejected<Object>!]]",
check(
new GraphQLNonNull(
new GraphQLObjectType({
name: "Object",
fields: () => ({
obj: {
type: GraphQLString
}
})
})
),
"{test {obj}}",
[[{ obj: "test" }, Promise.reject("bad")]],
{
data: { test: [null] },
errors: [
{
locations: [{ column: 2, line: 1 }],
message: "bad",
path: ["test", 0, 1]
}
]
}
)
);
});

describe("resolved fields in object list", () => {
Expand Down
87 changes: 37 additions & 50 deletions src/execution.ts
Expand Up @@ -832,10 +832,11 @@ function compileListType(
const listContext = createSubCompilationContext(context);
// context depth will be mutated, so we cache the current value.
const newDepth = ++listContext.depth;
const fieldType = type.ofType;
const dataBody = compileType(
listContext,
parentType,
type.ofType,
fieldType,
fieldNodes,
["__safeMapNode"],
["__child"],
Expand All @@ -852,12 +853,35 @@ function compileListType(
errorMessage
)}), null)`;
return `(typeof ${name} === "string" || typeof ${name}[Symbol.iterator] !== "function") ? ${errorCase} :
__safeMap(${name}, (__safeMapNode, idx${newDepth}) => {
${generateUniqueDeclarations(listContext)}
const __child = ${dataBody};
${compileDeferredFields(listContext)}
return __child;
})`;
__safeMap(${name}, (__safeMapNode, idx${newDepth}, newArray) => {
if (${isPromiseInliner("__safeMapNode")}) {
${GLOBAL_EXECUTOR_NAME}(() => __safeMapNode,
(${GLOBAL_PARENT_NAME}, __safeMapNode, err) => {
if (err != null) {
${
isNonNullType(fieldType)
? GLOBAL_NULL_ERRORS_NAME
: GLOBAL_ERRORS_NAME
}.push(${createErrorObject(
context,
fieldNodes,
addPath(responsePath, "idx" + newDepth, "variable"),
"err.message != null ? err.message : err",
"err"
)});
return;
}
${generateUniqueDeclarations(listContext)}
${GLOBAL_PARENT_NAME}[idx${newDepth}] = ${dataBody};\n
${compileDeferredFields(listContext)}
}, newArray, ${GLOBAL_DATA_NAME}, ${GLOBAL_ERRORS_NAME}, ${GLOBAL_NULL_ERRORS_NAME});
return null;
}
${generateUniqueDeclarations(listContext)}
const __child = ${dataBody};
${compileDeferredFields(listContext)}
return __child;
})`;
}

/**
Expand All @@ -884,51 +908,10 @@ function unpromisify(
if (isPromise(value)) {
value.then(success, error).catch(errorHandler);
return;
} else if (Array.isArray(value)) {
return handleArrayValue(value, success, error, errorHandler);
}
success(value);
}

/**
* Ensure that an array with possible local errors are handled cleanly.
*
* @param {any[]} value Array<Promise<any> | any> array of value
* @param {SuccessCallback} success callback to be called with the result, the cb should only called once
* @param {ErrorCallback} error callback to be called in case of errors, the cb should only called once
* @param errorHandler handler for unexpected errors caused by bugs
*/
function handleArrayValue(
value: any[],
success: SuccessCallback,
error: ErrorCallback,
errorHandler: (err: Error) => void
): void {
// The array might have local errors which need to be handled locally in order for proper error messages
let hasPromises = false;
const values = value.map(item => {
if (isPromise(item)) {
// return the error
// the following transformations will take care of the error
hasPromises = true;
return item.catch((err: Error) => {
return err;
});
}
return item;
});
if (hasPromises) {
return unpromisify(
// This promise should not reject but it is handled anyway
() => Promise.all(values),
success,
error,
errorHandler
);
}
success(values);
}

/**
* Implements a generic map operation for any iterable.
*
Expand All @@ -939,12 +922,12 @@ function handleArrayValue(
*/
function safeMap(
iterable: Iterable<any> | string,
cb: (a: any, idx: number) => any
cb: (a: any, idx: number, newArray: any[]) => any
): any[] {
let index = 0;
const result = [];
for (const a of iterable) {
const item = cb(a, index);
const item = cb(a, index, result);
result.push(item);
++index;
}
Expand Down Expand Up @@ -1181,6 +1164,10 @@ export function isPromise(value: any): value is Promise<any> {
);
}

export function isPromiseInliner(value: string): string {
return `${value} != null && typeof ${value} === "object" && typeof ${value}.then === "function"`;
}

/**
* Serializes the response path for an error response.
*
Expand Down
1 change: 1 addition & 0 deletions tslint.json
Expand Up @@ -3,6 +3,7 @@
"tslint:recommended"
],
"rules": {
"max-line-length": false,
"arrow-parens": false,
"no-shadowed-variable": false,
"interface-name": false,
Expand Down

0 comments on commit aad8506

Please sign in to comment.