Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/breezy-weeks-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-tools/batch-delegate': patch
'@graphql-tools/delegate': patch
---

Correct error paths in case of batch delegation with the same error
10 changes: 9 additions & 1 deletion packages/batch-delegate/src/batchDelegateToSchema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { pathToArray, relocatedError } from '@graphql-tools/utils';
import { GraphQLError } from 'graphql';
import { getLoader } from './getLoader.js';
import { BatchDelegateOptions } from './types.js';

Expand All @@ -11,5 +13,11 @@ export function batchDelegateToSchema<TContext = any, K = any, V = any, C = K>(
return [];
}
const loader = getLoader(options);
return Array.isArray(key) ? loader.loadMany(key) : loader.load(key);
const res = Array.isArray(key) ? loader.loadMany(key) : loader.load(key);
return res.catch((error) => {
if (options.info?.path && error instanceof GraphQLError) {
return relocatedError(error, pathToArray(options.info.path));
}
return error;
});
}
162 changes: 162 additions & 0 deletions packages/batch-delegate/tests/errorPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { batchDelegateToSchema } from '@graphql-tools/batch-delegate';
import { delegateToSchema } from '@graphql-tools/delegate';
import { normalizedExecutor } from '@graphql-tools/executor';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { stitchSchemas } from '@graphql-tools/stitch';
import { GraphQLError, parse } from 'graphql';
import { beforeEach, describe, expect, test, vi } from 'vitest';

class NotFoundError extends GraphQLError {
constructor(id: unknown) {
super('Not Found', {
extensions: { id },
});
}
}

describe('preserves error path indices', () => {
const getProperty = vi.fn((id: unknown) => {
return new NotFoundError(id);
});

beforeEach(() => {
getProperty.mockClear();
});

const subschema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Property {
id: ID!
}

type Object {
id: ID!
propertyId: ID!
}

type Query {
objects: [Object!]!
propertyById(id: ID!): Property
propertiesByIds(ids: [ID!]!): [Property]!
}
`,
resolvers: {
Query: {
objects: () => {
return [
{ id: '1', propertyId: '1' },
{ id: '2', propertyId: '1' },
];
},
propertyById: (_, args) => getProperty(args.id),
propertiesByIds: (_, args) => args.ids.map(getProperty),
},
},
});

const subschemas = [subschema];
const typeDefs = /* GraphQL */ `
extend type Object {
property: Property
}
`;

const query = /* GraphQL */ `
query {
objects {
id
property {
id
}
}
}
`;

const expected = {
errors: [
{
message: 'Not Found',
extensions: { id: '1' },
path: ['objects', 0, 'property'],
},
{
message: 'Not Found',
extensions: { id: '1' },
path: ['objects', 1, 'property'],
},
],
data: {
objects: [
{
id: '1',
property: null as null,
},
{
id: '2',
property: null as null,
},
],
},
};

test('using delegateToSchema', async () => {
const schema = stitchSchemas({
subschemas,
typeDefs,
resolvers: {
Object: {
property: {
selectionSet: '{ propertyId }',
resolve: (source, _, context, info) => {
return delegateToSchema({
schema: subschema,
fieldName: 'propertyById',
args: { id: source.propertyId },
context,
info,
});
},
},
},
},
});

const result = await normalizedExecutor({
schema,
document: parse(query),
});

expect(getProperty).toHaveBeenCalledTimes(2);
expect(result).toMatchObject(expected);
});

test('using batchDelegateToSchema', async () => {
const schema = stitchSchemas({
subschemas,
typeDefs,
resolvers: {
Object: {
property: {
selectionSet: '{ propertyId }',
resolve: (source, _, context, info) =>
batchDelegateToSchema({
schema: subschema,
fieldName: 'propertiesByIds',
key: source.propertyId,
context,
info,
}),
},
},
},
});

const result = await normalizedExecutor({
schema,
document: parse(query),
});

expect(getProperty).toHaveBeenCalledTimes(1);
expect(result).toMatchObject(expected);
});
});
4 changes: 2 additions & 2 deletions packages/delegate/src/resolveExternalValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,15 @@ function resolveExternalList<TContext extends Record<string, any>>(
);
}

const reportedErrors = new WeakMap<GraphQLError, boolean>();
const reportedErrors = new WeakSet<GraphQLError>();

function reportUnpathedErrorsViaNull(unpathedErrors: Array<GraphQLError>) {
if (unpathedErrors.length) {
const unreportedErrors: Array<GraphQLError> = [];
for (const error of unpathedErrors) {
if (!reportedErrors.has(error)) {
unreportedErrors.push(error);
reportedErrors.set(error, true);
reportedErrors.add(error);
}
}

Expand Down
Loading