Files
supabase/apps/docs/resources/error/errorModel.ts
Charis 4e916fc16a feat(graphql): add paginated errors collection query (#36149)
* 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
2025-06-09 10:24:17 -04:00

196 lines
4.7 KiB
TypeScript

import { type PostgrestError } from '@supabase/supabase-js'
import {
ApiErrorGeneric,
CollectionQueryError,
convertPostgrestToApiError,
NoDataError,
} from '~/app/api/utils'
import { Result } from '~/features/helpers.fn'
import { supabase } from '~/lib/supabase'
import { type CollectionFetch } from '../utils/connections'
export const SERVICES = {
AUTH: {
value: 'AUTH',
},
REALTIME: {
value: 'REALTIME',
},
STORAGE: {
value: 'STORAGE',
},
} as const
type Service = keyof typeof SERVICES
type ErrorCollectionFetch = CollectionFetch<ErrorModel, { service?: Service }>['fetch']
export class ErrorModel {
public id: string
public code: string
public service: Service
public httpStatusCode?: number
public message?: string
constructor({
id,
code,
service,
httpStatusCode,
message,
}: {
id: string
code: string
service: Service
httpStatusCode?: number
message?: string
}) {
this.id = id
this.code = code
this.service = service
this.httpStatusCode = httpStatusCode
this.message = message
}
static async loadSingleError({
code,
service,
}: {
code: string
service: Service
}): Promise<Result<ErrorModel, ApiErrorGeneric>> {
return new Result(
await supabase()
.schema('content')
.from('error')
.select('id, code, service(name), httpStatusCode:http_status_code, message')
.eq('code', code)
.eq('service.name', service)
.is('deleted_at', null)
.single<{
id: string
code: string
service: {
name: Service
}
httpStatusCode?: number
message?: string
}>()
)
.map((data) => {
return new ErrorModel({
...data,
service: data.service.name,
})
})
.mapError((error) => {
if (error.code === 'PGRST116') {
return new NoDataError('Error for given code and service does not exist', error)
}
return convertPostgrestToApiError(error)
})
}
static async loadErrors(
args: Parameters<ErrorCollectionFetch>[0]
): ReturnType<ErrorCollectionFetch> {
const PAGE_SIZE = 20
const limit = args?.first ?? args?.last ?? PAGE_SIZE
const service = args?.additionalArgs?.service as Service | undefined
const [countResult, errorCodesResult] = await Promise.all([
fetchTotalErrorCount(service),
fetchErrorDescriptions({
after: args?.after ?? undefined,
before: args?.before ?? undefined,
reverse: !!args?.last,
limit: limit + 1,
service,
}),
])
return countResult
.join(errorCodesResult)
.map(([count, errorCodes]) => {
const hasMoreItems = errorCodes.length > limit
const items = args?.last ? errorCodes.slice(1) : errorCodes.slice(0, limit)
return {
items: items.map((errorCode) => new ErrorModel(errorCode)),
totalCount: count,
hasNextPage: args?.last ? !!args?.before : hasMoreItems,
hasPreviousPage: args?.last ? hasMoreItems : !!args?.after,
}
})
.mapError(([countError, errorCodeError]) => {
return CollectionQueryError.fromErrors(countError, errorCodeError)
})
}
}
async function fetchTotalErrorCount(service?: Service): Promise<Result<number, PostgrestError>> {
const query = supabase()
.schema('content')
.from('error')
.select('id, service!inner(name)', { count: 'exact', head: true })
.is('deleted_at', null)
if (service) {
query.eq('service.name', service)
}
const { count, error } = await query
if (error) {
return Result.error(error)
}
return Result.ok(count ?? 0)
}
type ErrorDescription = {
id: string
code: string
service: Service
httpStatusCode?: number
message?: string
}
async function fetchErrorDescriptions({
after,
before,
reverse,
limit,
service,
}: {
after?: string
before?: string
reverse: boolean
limit: number
service?: Service
}): Promise<Result<ErrorDescription[], PostgrestError>> {
const query = supabase()
.schema('content')
.from('error')
.select('id, code, service!inner(name), httpStatusCode: http_status_code, message')
.is('deleted_at', null)
.order('id', { ascending: reverse ? false : true })
if (service) {
query.eq('service.name', service)
}
if (after != undefined) {
query.gt('id', after)
}
if (before != undefined) {
query.lt('id', before)
}
query.limit(limit)
const result = await query
return new Result(result).map((results) => {
const transformedResults = (reverse ? results.toReversed() : results).map((error) => ({
...error,
service: error.service.name,
}))
return transformedResults as ErrorDescription[]
})
}