types: restore Supabase row typing, fix parseInput inference (586→76 tsc errors)
The installed supabase-js (2.99, 5 generics) is incompatible with @supabase/[email protected] (calls it with 3), collapsing the server client's schema to `never` — the root cause of ~450 of the 586 hidden type errors (next.config typescript.ignoreBuildErrors masks them all). - lib/supabase/server.ts: annotate the factory as SupabaseClient<Database> and bridge the ssr/js impedance mismatch at the one documented boundary; type the setAll cookie params. - lib/supabase/proxy.ts, proxy.ts: type the setAll cookie params. - lib/auth-helpers.ts: parseInput now infers its result from the schema via z.infer (was a fragile ZodSchema<T> that widened outputs to {}). - lib/validations.ts: annotate flexibleDateTime's output type. - types/database.ts: add class_signup_tx / class_cancel_tx RPC signatures (called but absent; pending full regeneration). - tsconfig.json: target ES2020 (door-log-logic uses BigInt literals). - vitest.setup.ts: import beforeAll/afterAll instead of relying on globals. No runtime behavior change; 639 tests still green. ignoreBuildErrors stays on until the remaining errors reach zero. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9ac0c2486c
commit
1766780bca
+2
-3
@@ -124,13 +124,12 @@ export async function joinSpace(formData: {
|
||||
|
||||
// Try the new multi-code invites table first. Falls back to the legacy
|
||||
// spaces.invite_code (a permanent default) if the code isn't found there.
|
||||
let invite: { id: string; space_id: string; max_uses: number | null; uses_count: number; expires_at: string | null; is_enabled: boolean; role: string } | null = null
|
||||
const { data: inviteRow } = await admin
|
||||
.from('space_invites')
|
||||
.select('id, space_id, max_uses, uses_count, expires_at, is_enabled, role')
|
||||
.eq('code', code)
|
||||
.maybeSingle()
|
||||
if (inviteRow) invite = inviteRow as typeof invite
|
||||
const invite = inviteRow
|
||||
|
||||
let spaceId: string | null = null
|
||||
if (invite) {
|
||||
@@ -171,7 +170,7 @@ export async function joinSpace(formData: {
|
||||
.from('space_members')
|
||||
.select('id')
|
||||
.eq('user_id', user.id)
|
||||
.in('status', ACTIVE_STATUSES as unknown as string[])
|
||||
.in('status', ACTIVE_STATUSES)
|
||||
.maybeSingle()
|
||||
if (existingMembership) {
|
||||
return { error: 'You are already a member of a space.' }
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Tables } from '@/types/database'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { ZodTypeAny, TypeOf } from 'zod'
|
||||
import { ACTIVE_STATUSES, hasRole, isPrivilegeEligible, type Role } from '@/lib/permissions'
|
||||
|
||||
/**
|
||||
@@ -97,7 +97,10 @@ export async function requireMemberWithRole(
|
||||
* so callers can `if (!v.ok) return { error: v.error }` and have `v.data`
|
||||
* narrowed to T.
|
||||
*/
|
||||
export function parseInput<T>(schema: ZodSchema<T>, input: unknown): Result<T> {
|
||||
export function parseInput<S extends ZodTypeAny>(
|
||||
schema: S,
|
||||
input: unknown,
|
||||
): Result<TypeOf<S>> {
|
||||
const parsed = schema.safeParse(input)
|
||||
if (!parsed.success) {
|
||||
return { ok: false, error: parsed.error.errors[0]?.message ?? 'Invalid input' }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
/** Shape `@supabase/ssr` passes to `setAll` (typing lost to the ssr/js version drift). */
|
||||
type CookiesToSet = { name: string; value: string; options: CookieOptions }[]
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
@@ -16,7 +19,7 @@ export async function updateSession(request: NextRequest) {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
setAll(cookiesToSet: CookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) =>
|
||||
request.cookies.set(name, value),
|
||||
)
|
||||
|
||||
+21
-4
@@ -1,16 +1,27 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Database } from '@/types/database'
|
||||
|
||||
/** Shape `@supabase/ssr` passes to `setAll` (typing lost to the ssr/js version drift). */
|
||||
type CookiesToSet = { name: string; value: string; options: CookieOptions }[]
|
||||
|
||||
/**
|
||||
* Especially important if using Fluid compute: Don't put this client in a
|
||||
* global variable. Always create a new client within each function when using
|
||||
* it.
|
||||
*
|
||||
* The explicit `SupabaseClient<Database>` return annotation is load-bearing:
|
||||
* `@supabase/ssr`'s `createServerClient` still returns supabase-js's older
|
||||
* 3-generic client shape, which — against the installed 5-generic supabase-js —
|
||||
* collapses the schema to `never` (every `.from().select()` row becomes `never`).
|
||||
* Re-asserting `SupabaseClient<Database>` here lets the client recompute its
|
||||
* schema from `Database`, restoring full row typing across every caller.
|
||||
*/
|
||||
export async function createClient() {
|
||||
export async function createClient(): Promise<SupabaseClient<Database>> {
|
||||
const cookieStore = await cookies()
|
||||
|
||||
return createServerClient<Database>(
|
||||
const client = createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
@@ -18,7 +29,7 @@ export async function createClient() {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
setAll(cookiesToSet: CookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options),
|
||||
@@ -32,4 +43,10 @@ export async function createClient() {
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Single documented boundary cast: `createServerClient` returns the older
|
||||
// 3-generic client shape; re-assert the 5-generic `SupabaseClient<Database>`
|
||||
// so callers get full row typing. This is the ONE place the impedance
|
||||
// mismatch is bridged.
|
||||
return client as unknown as SupabaseClient<Database>
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import { API_METHODS } from './api-call-logic'
|
||||
* Returns a Zod schema that produces a `string | null` (after preprocess +
|
||||
* `.datetime().nullable()`).
|
||||
*/
|
||||
export const flexibleDateTime = () =>
|
||||
export const flexibleDateTime = (): z.ZodType<string | null, z.ZodTypeDef, unknown> =>
|
||||
z.preprocess(
|
||||
(val) => {
|
||||
if (val === null || val === undefined || val === '') return null
|
||||
|
||||
Reference in New Issue
Block a user