* 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
137 lines
3.1 KiB
TypeScript
137 lines
3.1 KiB
TypeScript
import { type PostgrestError } from '@supabase/supabase-js'
|
|
import { type ZodError } from 'zod'
|
|
import { isObject } from '~/features/helpers.misc'
|
|
|
|
type ObjectOrNever = object | never
|
|
|
|
export type ApiErrorGeneric = ApiError<ObjectOrNever>
|
|
|
|
export class ApiError<Details extends ObjectOrNever = never> extends Error {
|
|
constructor(
|
|
message: string,
|
|
public source?: unknown,
|
|
public details?: Details
|
|
) {
|
|
super(message)
|
|
}
|
|
|
|
isPrivate() {
|
|
return true
|
|
}
|
|
|
|
isUserError() {
|
|
return false
|
|
}
|
|
|
|
statusCode() {
|
|
return 500
|
|
}
|
|
}
|
|
|
|
export class InvalidRequestError<Details extends ObjectOrNever = never> extends ApiError<Details> {
|
|
constructor(message: string, source?: unknown, details?: Details) {
|
|
super(`Invalid request: ${message}`, source, details)
|
|
}
|
|
|
|
isPrivate() {
|
|
return false
|
|
}
|
|
|
|
isUserError() {
|
|
return true
|
|
}
|
|
|
|
statusCode() {
|
|
return 400
|
|
}
|
|
}
|
|
|
|
export class NoDataError<Details extends ObjectOrNever = never> extends ApiError<Details> {
|
|
constructor(message: string, source?: unknown, details?: Details) {
|
|
super(`Data not found: ${message}`, source, details)
|
|
}
|
|
|
|
isPrivate() {
|
|
return false
|
|
}
|
|
|
|
isUserError() {
|
|
return true
|
|
}
|
|
|
|
statusCode() {
|
|
return 404
|
|
}
|
|
}
|
|
|
|
export class MultiError<ErrorType = unknown, Details extends ObjectOrNever = never> extends Error {
|
|
constructor(
|
|
message: string,
|
|
cause?: Array<ErrorType>,
|
|
public details?: Details
|
|
) {
|
|
super(message, { cause })
|
|
}
|
|
|
|
get totalErrors(): number {
|
|
return (this.cause as Array<ErrorType>)?.length || 0
|
|
}
|
|
|
|
appendError(message: string, error: ErrorType): this {
|
|
this.message = `${this.message}\n\t${message}`
|
|
;((this.cause ?? (this.cause = [])) as Array<ErrorType>).push(error)
|
|
return this
|
|
}
|
|
}
|
|
|
|
export class CollectionQueryError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly queryErrors: {
|
|
count?: PostgrestError
|
|
data?: PostgrestError
|
|
}
|
|
) {
|
|
super(message)
|
|
}
|
|
|
|
public static fromErrors(
|
|
countError: PostgrestError | undefined,
|
|
dataError: PostgrestError | undefined
|
|
): CollectionQueryError {
|
|
const fetchFailedFor =
|
|
countError && dataError ? 'count and collection' : countError ? 'count' : 'collection'
|
|
return new CollectionQueryError(`Failed to fetch ${fetchFailedFor}`, {
|
|
count: countError,
|
|
data: dataError,
|
|
})
|
|
}
|
|
}
|
|
|
|
export function convertUnknownToApiError(error: unknown): ApiError {
|
|
return new ApiError('Unknown error', error)
|
|
}
|
|
|
|
export function convertPostgrestToApiError(error: PostgrestError): ApiError {
|
|
const message = `${error.code}: ${error.hint}`
|
|
return new ApiError(message, error)
|
|
}
|
|
|
|
export function convertZodToInvalidRequestError(
|
|
error: ZodError,
|
|
prelude?: string
|
|
): InvalidRequestError {
|
|
const issue = error.issues[0]
|
|
const pathStr = issue.path.join('.')
|
|
const message = `${prelude ? `${prelude}: ` : ''}${issue.message} at key "${pathStr}"`
|
|
|
|
return new InvalidRequestError(message, error)
|
|
}
|
|
|
|
export function extractMessageFromAnyError(error: unknown): string {
|
|
if (isObject(error) && 'message' in error && typeof error.message === 'string') {
|
|
return error.message
|
|
}
|
|
return String(error)
|
|
}
|