* feat(graphql): add paginated errors collection query - Add new GraphQL query field 'errors' with cursor-based pagination - Add UUID id column to content.error table for cursor pagination - Implement error collection resolver with forward/backward pagination - Add comprehensive test suite for pagination functionality - Update database types and schema to support new error collection - Add utility functions for handling collection queries and errors - Add seed data for testing pagination scenarios This change allows clients to efficiently paginate through error codes using cursor-based pagination, supporting both forward and backward traversal. The implementation follows the Relay connection specification and includes proper error handling and type safety. * docs(graphql): add comprehensive GraphQL architecture documentation Add detailed documentation for the docs GraphQL endpoint architecture, including: - Modular query pattern and folder structure - Step-by-step guide for creating new top-level queries - Best practices for error handling, field optimization, and testing - Code examples for schemas, models, resolvers, and tests * feat(graphql): add service filtering to errors collection query Enable filtering error codes by Supabase service in the GraphQL errors collection: - Add optional service argument to errors query resolver - Update error model to support service-based filtering in database queries - Maintain pagination compatibility with service filtering - Add comprehensive tests for service filtering with and without pagination * feat(graphql): add service filtering and fix cursor encoding for errors collection - Add service parameter to errors GraphQL query for filtering by Supabase service - Implement base64 encoding/decoding for pagination cursors in error resolver - Fix test cursor encoding to match resolver implementation - Update GraphQL schema snapshot to reflect new service filter field * docs(graphql): fix codegen instruction
130 lines
3.7 KiB
TypeScript
130 lines
3.7 KiB
TypeScript
import { GraphQLError, GraphQLNonNull, GraphQLResolveInfo, GraphQLString } from 'graphql'
|
|
import type {
|
|
ErrorCollection,
|
|
RootQueryTypeErrorArgs,
|
|
RootQueryTypeErrorsArgs,
|
|
Service,
|
|
} from '~/__generated__/graphql'
|
|
import { ApiError, convertUnknownToApiError } from '~/app/api/utils'
|
|
import { Result } from '~/features/helpers.fn'
|
|
import {
|
|
createCollectionType,
|
|
GraphQLCollectionBuilder,
|
|
paginationArgs,
|
|
type CollectionFetch,
|
|
} from '../utils/connections'
|
|
import { ErrorModel } from './errorModel'
|
|
import {
|
|
GRAPHQL_FIELD_ERROR_GLOBAL,
|
|
GRAPHQL_FIELD_ERRORS_GLOBAL,
|
|
GraphQLEnumTypeService,
|
|
GraphQLObjectTypeError,
|
|
} from './errorSchema'
|
|
|
|
/**
|
|
* Encodes a string to base64
|
|
*/
|
|
function encodeBase64(str: string): string {
|
|
return Buffer.from(str, 'utf8').toString('base64')
|
|
}
|
|
|
|
/**
|
|
* Decodes a base64 string back to the original string
|
|
*/
|
|
function decodeBase64(base64: string): string {
|
|
return Buffer.from(base64, 'base64').toString('utf8')
|
|
}
|
|
|
|
async function resolveSingleError(
|
|
_parent: unknown,
|
|
args: RootQueryTypeErrorArgs,
|
|
_context: unknown,
|
|
_info: GraphQLResolveInfo
|
|
): Promise<ErrorModel | GraphQLError> {
|
|
return (
|
|
await Result.tryCatchFlat(ErrorModel.loadSingleError, convertUnknownToApiError, args)
|
|
).match(
|
|
(data) => data,
|
|
(error) => {
|
|
console.error(`Error resolving ${GRAPHQL_FIELD_ERROR_GLOBAL}:`, error)
|
|
return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
|
|
}
|
|
)
|
|
}
|
|
|
|
async function resolveErrors(
|
|
_parent: unknown,
|
|
args: RootQueryTypeErrorsArgs,
|
|
_context: unknown,
|
|
_info: GraphQLResolveInfo
|
|
): Promise<ErrorCollection | GraphQLError> {
|
|
return (
|
|
await Result.tryCatchFlat(
|
|
async (...args) => {
|
|
const fetch: CollectionFetch<ErrorModel, { service?: Service }, ApiError>['fetch'] = async (
|
|
fetchArgs
|
|
) => {
|
|
const result = await ErrorModel.loadErrors({
|
|
...fetchArgs,
|
|
additionalArgs: {
|
|
service: args[0].service ?? undefined,
|
|
},
|
|
})
|
|
return result.mapError((error) => new ApiError('Failed to resolve error codes', error))
|
|
}
|
|
return await GraphQLCollectionBuilder.create<ErrorModel, { service?: Service }, ApiError>({
|
|
fetch,
|
|
args: {
|
|
...args[0],
|
|
// Decode base64 cursors before passing to fetch function
|
|
after: args[0].after ? decodeBase64(args[0].after) : undefined,
|
|
before: args[0].before ? decodeBase64(args[0].before) : undefined,
|
|
},
|
|
getCursor: (item) => encodeBase64(item.id),
|
|
})
|
|
},
|
|
convertUnknownToApiError,
|
|
args
|
|
)
|
|
).match(
|
|
(data) => data as ErrorCollection,
|
|
(error) => {
|
|
console.error(`Error resolving ${GRAPHQL_FIELD_ERRORS_GLOBAL}:`, error)
|
|
return error instanceof GraphQLError
|
|
? error
|
|
: new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
|
|
}
|
|
)
|
|
}
|
|
|
|
export const errorRoot = {
|
|
[GRAPHQL_FIELD_ERROR_GLOBAL]: {
|
|
description: 'Get the details of an error code returned from a Supabase service',
|
|
args: {
|
|
code: {
|
|
type: new GraphQLNonNull(GraphQLString),
|
|
},
|
|
service: {
|
|
type: new GraphQLNonNull(GraphQLEnumTypeService),
|
|
},
|
|
},
|
|
type: GraphQLObjectTypeError,
|
|
resolve: resolveSingleError,
|
|
},
|
|
}
|
|
|
|
export const errorsRoot = {
|
|
[GRAPHQL_FIELD_ERRORS_GLOBAL]: {
|
|
description: 'Get error codes that can potentially be returned by Supabase services',
|
|
args: {
|
|
...paginationArgs,
|
|
service: {
|
|
type: GraphQLEnumTypeService,
|
|
description: 'Filter errors by a specific Supabase service',
|
|
},
|
|
},
|
|
type: createCollectionType(GraphQLObjectTypeError),
|
|
resolve: resolveErrors,
|
|
},
|
|
}
|