mirror of
https://github.com/supabase/supabase.git
synced 2026-05-25 21:24:01 +08:00
* 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
196 lines
4.7 KiB
TypeScript
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[]
|
|
})
|
|
}
|