From fba3f7bd45f67f349cd39eebe2c2760ce8a61789 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:55:19 -0400 Subject: [PATCH] feat(docs): add error code filtering to GraphQL errors endpoint (#36284) - Add support for filtering errors by specific error codes - Implement `errors` query that accepts array of error code IDs - Add tests for error code filtering functionality - Update GraphQL schema with new error filtering capabilities --- .../graphql/__snapshots__/route.test.ts.snap | 3 + .../graphql/tests/errors.collection.test.ts | 102 ++++++++++++++++++ apps/docs/resources/error/errorModel.ts | 23 +++- apps/docs/resources/error/errorResolver.ts | 19 +++- 4 files changed, 140 insertions(+), 7 deletions(-) diff --git a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap index 9aec10f970..9578e32541 100644 --- a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap +++ b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap @@ -151,6 +151,9 @@ type RootQueryType { """Filter errors by a specific Supabase service""" service: Service + + """Filter errors by a specific error code""" + code: String ): ErrorCollection } diff --git a/apps/docs/app/api/graphql/tests/errors.collection.test.ts b/apps/docs/app/api/graphql/tests/errors.collection.test.ts index 0192d7950a..2ab6fa001c 100644 --- a/apps/docs/app/api/graphql/tests/errors.collection.test.ts +++ b/apps/docs/app/api/graphql/tests/errors.collection.test.ts @@ -353,4 +353,106 @@ describe('/api/graphql errors collection', () => { ) } }) + + it('filters by code when code argument is provided', async () => { + // First, get the first error code from the database to test with + const { data: dbErrors } = await supabase() + .schema('content') + .from('error') + .select('code') + .is('deleted_at', null) + .limit(1) + + expect(dbErrors).not.toBe(null) + expect(dbErrors).toHaveLength(1) + const testCode = dbErrors![0].code + + const codeFilterQuery = ` + query { + errors(code: "${testCode}") { + totalCount + nodes { + code + service + } + } + } + ` + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: codeFilterQuery }), + }) + + const result = await POST(request) + const json = await result.json() + expect(json.errors).toBeUndefined() + + // Verify all returned errors have the specified code + expect(json.data.errors.nodes.length).toBeGreaterThan(0) + expect(json.data.errors.nodes.every((e: any) => e.code === testCode)).toBe(true) + }) + + it('filters by both service and code when both arguments are provided', async () => { + // Get an error that exists for AUTH service + const { data: authError } = await supabase() + .schema('content') + .from('error') + .select('code, ...service(service:name)') + .is('deleted_at', null) + .eq('service.name', 'AUTH') + .limit(1) + + expect(authError).not.toBe(null) + expect(authError).toHaveLength(1) + const testCode = authError![0].code + + const bothFiltersQuery = ` + query { + errors(service: AUTH, code: "${testCode}") { + totalCount + nodes { + code + service + } + } + } + ` + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: bothFiltersQuery }), + }) + + const result = await POST(request) + const json = await result.json() + expect(json.errors).toBeUndefined() + + // Verify all returned errors match both filters + expect(json.data.errors.nodes.length).toBeGreaterThan(0) + expect( + json.data.errors.nodes.every((e: any) => e.code === testCode && e.service === 'AUTH') + ).toBe(true) + }) + + it('returns empty list when code filter matches no errors', async () => { + const nonExistentCodeQuery = ` + query { + errors(code: "NONEXISTENT_CODE_12345") { + totalCount + nodes { + code + } + } + } + ` + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: nonExistentCodeQuery }), + }) + + const result = await POST(request) + const json = await result.json() + expect(json.errors).toBeUndefined() + expect(json.data.errors.totalCount).toBe(0) + expect(json.data.errors.nodes).toHaveLength(0) + }) }) diff --git a/apps/docs/resources/error/errorModel.ts b/apps/docs/resources/error/errorModel.ts index cc02e19707..9a1fb748ac 100644 --- a/apps/docs/resources/error/errorModel.ts +++ b/apps/docs/resources/error/errorModel.ts @@ -22,7 +22,10 @@ export const SERVICES = { } as const type Service = keyof typeof SERVICES -type ErrorCollectionFetch = CollectionFetch['fetch'] +type ErrorCollectionFetch = CollectionFetch< + ErrorModel, + { service?: Service; code?: string } +>['fetch'] export class ErrorModel { public id: string @@ -96,15 +99,17 @@ export class ErrorModel { const PAGE_SIZE = 20 const limit = args?.first ?? args?.last ?? PAGE_SIZE const service = args?.additionalArgs?.service as Service | undefined + const code = args?.additionalArgs?.code as string | undefined const [countResult, errorCodesResult] = await Promise.all([ - fetchTotalErrorCount(service), + fetchTotalErrorCount(service, code), fetchErrorDescriptions({ after: args?.after ?? undefined, before: args?.before ?? undefined, reverse: !!args?.last, limit: limit + 1, service, + code, }), ]) @@ -127,7 +132,10 @@ export class ErrorModel { } } -async function fetchTotalErrorCount(service?: Service): Promise> { +async function fetchTotalErrorCount( + service?: Service, + code?: string +): Promise> { const query = supabase() .schema('content') .from('error') @@ -138,6 +146,10 @@ async function fetchTotalErrorCount(service?: Service): Promise> { const query = supabase() .schema('content') @@ -176,6 +190,9 @@ async function fetchErrorDescriptions({ if (service) { query.eq('service.name', service) } + if (code) { + query.eq('code', code) + } if (after != undefined) { query.gt('id', after) } diff --git a/apps/docs/resources/error/errorResolver.ts b/apps/docs/resources/error/errorResolver.ts index 454c04e9e4..f645d6fee8 100644 --- a/apps/docs/resources/error/errorResolver.ts +++ b/apps/docs/resources/error/errorResolver.ts @@ -61,18 +61,25 @@ async function resolveErrors( return ( await Result.tryCatchFlat( async (...args) => { - const fetch: CollectionFetch['fetch'] = async ( - fetchArgs - ) => { + const fetch: CollectionFetch< + ErrorModel, + { service?: Service; code?: string }, + ApiError + >['fetch'] = async (fetchArgs) => { const result = await ErrorModel.loadErrors({ ...fetchArgs, additionalArgs: { service: args[0].service ?? undefined, + code: args[0].code ?? undefined, }, }) return result.mapError((error) => new ApiError('Failed to resolve error codes', error)) } - return await GraphQLCollectionBuilder.create({ + return await GraphQLCollectionBuilder.create< + ErrorModel, + { service?: Service; code?: string }, + ApiError + >({ fetch, args: { ...args[0], @@ -122,6 +129,10 @@ export const errorsRoot = { type: GraphQLEnumTypeService, description: 'Filter errors by a specific Supabase service', }, + code: { + type: GraphQLString, + description: 'Filter errors by a specific error code', + }, }, type: createCollectionType(GraphQLObjectTypeError), resolve: resolveErrors,