types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)
Per-file type-only fixes surfaced after the Supabase client was correctly typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error. - lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database> (same ssr/js drift as server.ts; fixes it centrally for all callers). - Supabase embedded-select joins whose FK metadata is missing from the generated types are bridged with `as unknown as <target>` + a TODO to remove after regenerating types/database.ts. - setState optimistic-update variance and raw <select> enum values cast to the precise element/union type; .insert payload fields (Json, enums) given the precise column type via satisfies/field casts. - Added narrowing guards where a value was legitimately nullable (comms channel, public form page) without changing the intended path. The 14 remaining errors are confined to the four files fixed in Phase B (real bugs + the tautological actions.test rewrite). 639 tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1766780bca
commit
b1edb5c460
@@ -4,7 +4,7 @@ import { sendEmail } from '@/lib/email/send'
|
||||
const OLD = { ...process.env }
|
||||
|
||||
function mockFetch(status: number, body: unknown) {
|
||||
return vi.fn(async () =>
|
||||
return vi.fn(async (_input?: Request | string | URL, _init?: RequestInit) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
headingText,
|
||||
slugifyHeading,
|
||||
@@ -13,7 +14,7 @@ describe('headingText', () => {
|
||||
})
|
||||
it('flattens arrays and element children', () => {
|
||||
expect(headingText(['Get ', 'started'])).toBe('Get started')
|
||||
expect(headingText({ props: { children: ['Bold ', { props: { children: 'bit' } }] } })).toBe('Bold bit')
|
||||
expect(headingText({ props: { children: ['Bold ', { props: { children: 'bit' } }] } } as ReactNode)).toBe('Bold bit')
|
||||
})
|
||||
it('ignores null/boolean', () => {
|
||||
expect(headingText(null)).toBe('')
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('enqueueNotification', () => {
|
||||
const admin = mockAdmin({ upsertResult: { error: null } })
|
||||
await enqueueNotification(admin as never, params)
|
||||
expect(admin.from).toHaveBeenCalledWith('notifications')
|
||||
const [row, opts] = admin.upsert.mock.calls[0] as [Row, Row]
|
||||
const [row, opts] = admin.upsert.mock.calls[0] as unknown as [Row, Row]
|
||||
expect(row).toMatchObject({
|
||||
space_id: 'space-1',
|
||||
member_id: 'member-1',
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('String Utilities', () => {
|
||||
})
|
||||
|
||||
it('should handle empty name', () => {
|
||||
const name = ''
|
||||
const name: string = ''
|
||||
const initials = name ? name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) : '??'
|
||||
expect(initials).toBe('??')
|
||||
})
|
||||
|
||||
@@ -85,11 +85,11 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
m.id.startsWith('temp-') &&
|
||||
m.user_id === payload.new.user_id &&
|
||||
m.content === payload.new.content
|
||||
? payload.new
|
||||
? (payload.new as Message)
|
||||
: m,
|
||||
)
|
||||
}
|
||||
return [...prev, payload.new]
|
||||
return [...prev, payload.new as Message]
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -105,6 +105,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
}, [messages])
|
||||
|
||||
async function loadMessages() {
|
||||
if (!selectedChannel) return
|
||||
const supabase = createClient()
|
||||
const { data } = await supabase
|
||||
.from('comms_messages')
|
||||
@@ -119,6 +120,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
e.preventDefault()
|
||||
const content = newMessage.trim()
|
||||
if (!content || sending) return
|
||||
if (!selectedChannel) return
|
||||
setSending(true)
|
||||
|
||||
// Optimistic insert — message appears immediately for the sender
|
||||
@@ -133,7 +135,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
content,
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
setMessages(prev => [...prev, optimisticMsg])
|
||||
setMessages(prev => [...prev, optimisticMsg as Message])
|
||||
setNewMessage('')
|
||||
|
||||
const supabase = createClient()
|
||||
@@ -142,7 +144,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
.insert({
|
||||
channel_id: selectedChannel.id,
|
||||
space_id: space?.id ?? member.space_id,
|
||||
user_id: member.user_id,
|
||||
user_id: member.user_id as string,
|
||||
display_name: member.display_name,
|
||||
handle: member.handle,
|
||||
content,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Tables } from '@/types/database'
|
||||
import CommsClient from './comms-client'
|
||||
|
||||
export default async function CommsPage() {
|
||||
@@ -17,5 +18,5 @@ export default async function CommsPage() {
|
||||
.order('channel_type')
|
||||
.order('name')
|
||||
|
||||
return <CommsClient member={member} space={member?.spaces} channels={channels ?? []} />
|
||||
return <CommsClient member={member} space={member?.spaces as Tables<'spaces'>} channels={channels ?? []} />
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ContactsClient({ contacts: initialContacts }: { contacts: Contac
|
||||
tags: form.tags ? form.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
|
||||
})
|
||||
if (result.error) { setError(result.error); setLoading(false); return }
|
||||
setContacts(prev => prev.map(c => c.id === editContact.id ? { ...c, ...form } : c))
|
||||
setContacts(prev => prev.map(c => c.id === editContact.id ? { ...c, ...form } as unknown as Contact : c))
|
||||
setEditContact(null)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function TiersPanel({ isAdmin, tiers: initial }: { isAdmin: boolean; tier
|
||||
onSubmit={async e => {
|
||||
e.preventDefault()
|
||||
const cents = Math.round(parseFloat(d.dollars || '0') * 100)
|
||||
const result = await createTier({ slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || undefined, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence })
|
||||
const result = await createTier({ slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || undefined, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence as 'monthly' | 'quarterly' | 'annual' | 'one_time' | 'custom' })
|
||||
if ('error' in result && result.error) { toast.error(result.error); return }
|
||||
toast.success('Tier created')
|
||||
setTiers(prev => [...prev, { id: (result as { id: string }).id, slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || null, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence, is_system: false, is_archived: false, sort_order: 100 }])
|
||||
|
||||
@@ -30,7 +30,16 @@ export default async function FormResultsPage({ params }: { params: Promise<{ id
|
||||
formId={form.id}
|
||||
title={form.title}
|
||||
fields={parseFormSchema(form.schema)}
|
||||
submissions={submissions ?? []}
|
||||
submissions={
|
||||
(submissions ?? []) as {
|
||||
id: string
|
||||
member_id: string | null
|
||||
submitter_email: string | null
|
||||
answers: Record<string, unknown> | null
|
||||
form_version: number
|
||||
created_at: string
|
||||
}[]
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function ResultsClient({
|
||||
toast.error(res.error || 'Export failed')
|
||||
return
|
||||
}
|
||||
if (!res.data) return
|
||||
const { filename, csv } = res.data
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
@@ -98,10 +98,12 @@ export default async function MePage() {
|
||||
}))
|
||||
|
||||
const signupRes = await getMyClassSignups()
|
||||
const classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as ClassSignup[]) : []
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as unknown as ClassSignup[]) : []
|
||||
|
||||
const reservationRes = await getMyReservations()
|
||||
const reservations: Reservation[] = 'data' in reservationRes ? (reservationRes.data as Reservation[]) : []
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const reservations: Reservation[] = 'data' in reservationRes ? (reservationRes.data as unknown as Reservation[]) : []
|
||||
|
||||
const cardsRes = await getMyCards()
|
||||
const myCards: MyCard[] = 'data' in cardsRes ? (cardsRes.data as MyCard[]) : []
|
||||
@@ -121,7 +123,7 @@ export default async function MePage() {
|
||||
|
||||
const notifsRes = await getMyNotifications()
|
||||
const notifs: MyNotif[] = 'data' in notifsRes ? (notifsRes.data as MyNotif[]) : []
|
||||
const unreadNotifs = 'unreadCount' in notifsRes ? notifsRes.unreadCount : 0
|
||||
const unreadNotifs = 'unreadCount' in notifsRes ? (notifsRes.unreadCount ?? 0) : 0
|
||||
|
||||
const notifPrefsRes = await getMyNotificationPreferences()
|
||||
const notifPrefs: PrefMap = 'data' in notifPrefsRes ? notifPrefsRes.data : {}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function KbEntryEditor({ entry, member }: Props) {
|
||||
<label className="font-sans text-xs font-medium text-muted-foreground block mb-1.5">Visibility</label>
|
||||
<select
|
||||
value={visibility}
|
||||
onChange={e => setVisibility(e.target.value)}
|
||||
onChange={e => setVisibility(e.target.value as 'board' | 'all_members' | 'admin_only')}
|
||||
className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition"
|
||||
>
|
||||
<option value="all_members">All Members</option>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import KbEntryEditor from './kb-entry-editor'
|
||||
import type { Tables } from '@/types/database'
|
||||
|
||||
export default async function OpsEntryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
@@ -19,5 +20,5 @@ export default async function OpsEntryPage({ params }: { params: Promise<{ id: s
|
||||
|
||||
if (!entry) notFound()
|
||||
|
||||
return <KbEntryEditor entry={entry} member={member} />
|
||||
return <KbEntryEditor entry={entry} member={member as Tables<'space_members'>} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Tables } from '@/types/database'
|
||||
import KbEntryEditor from '../[id]/kb-entry-editor'
|
||||
|
||||
export default async function NewOpsEntryPage() {
|
||||
@@ -10,5 +11,5 @@ export default async function NewOpsEntryPage() {
|
||||
.from('space_members').select('space_id, role, display_name').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
|
||||
if (!member) return null
|
||||
|
||||
return <KbEntryEditor member={member} />
|
||||
return <KbEntryEditor member={member as Tables<'space_members'>} />
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { revealSecret, deleteSecret } from '@/lib/actions/secrets'
|
||||
import { OpsAclEditor } from '@/components/ops/ops-acl-editor'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { toast } from 'sonner'
|
||||
import type { Tables, TablesInsert } from '@/types/database'
|
||||
import type { Tables, TablesInsert, TablesUpdate, Enums } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
@@ -135,7 +135,7 @@ function KbModal({
|
||||
<label className="font-sans text-xs font-medium text-muted-foreground block mb-1.5">Visibility</label>
|
||||
<select
|
||||
value={visibility}
|
||||
onChange={e => setVisibility(e.target.value)}
|
||||
onChange={e => setVisibility(e.target.value as Enums<'kb_visibility'>)}
|
||||
className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition"
|
||||
>
|
||||
<option value="all_members">All Members</option>
|
||||
@@ -212,7 +212,11 @@ function AddSecretModal({
|
||||
label: title.trim(),
|
||||
area: area.trim() || null,
|
||||
value: value.trim(),
|
||||
}).select('id, title, area, created_at').single()
|
||||
// `satisfies` keeps the payload type-checked; the trailing `as never`
|
||||
// only bridges the typed browser client whose .insert() generic collapses
|
||||
// to `never`.
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesInsert<'secrets'> as never).select('id, title, area, created_at').single()
|
||||
setSaving(false)
|
||||
if (error) { toast.error(error.message); return }
|
||||
toast.success('Secret saved')
|
||||
@@ -298,14 +302,16 @@ function AreaLeadModal({
|
||||
area_name: areaName.trim(),
|
||||
lead_handle: memberName.trim(),
|
||||
description: contactInfo.trim() || null,
|
||||
}).eq('id', lead.id).select('*').single()
|
||||
// TODO(types): remove `as never` after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesUpdate<'area_leads'> as never).eq('id', lead.id).select('*').single()
|
||||
} else {
|
||||
result = await supabase.from('area_leads').insert({
|
||||
space_id: spaceId,
|
||||
area_name: areaName.trim(),
|
||||
lead_handle: memberName.trim(),
|
||||
description: contactInfo.trim() || null,
|
||||
}).select('*').single()
|
||||
// TODO(types): remove `as never` after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesInsert<'area_leads'> as never).select('*').single()
|
||||
}
|
||||
setSaving(false)
|
||||
if (result.error) { toast.error(result.error.message); return }
|
||||
|
||||
@@ -55,7 +55,7 @@ export default async function OpsPage() {
|
||||
spaceId={member.space_id}
|
||||
kbEntries={kbEntries ?? []}
|
||||
areaLeads={areaLeads ?? []}
|
||||
secrets={secretsResult.data ?? []}
|
||||
secrets={(secretsResult.data ?? []) as Tables<'secrets'>[]}
|
||||
canSeeSecrets={canSeeSecrets}
|
||||
canManageAcl={canManageAcl}
|
||||
aclRoleOptions={aclRoleOptions}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { PaymentsClient } from './payments-client'
|
||||
|
||||
@@ -27,7 +28,7 @@ export default async function PaymentsPage() {
|
||||
|
||||
return (
|
||||
<PaymentsClient
|
||||
payments={payments ?? []}
|
||||
payments={(payments ?? []) as ComponentProps<typeof PaymentsClient>['payments']}
|
||||
members={members ?? []}
|
||||
integrations={integrations ?? []}
|
||||
currentRole={member.role}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ProjectsClient({
|
||||
async function handleStatusChange(projectId: string, newStatus: string) {
|
||||
const result = await updateProjectStatus(projectId, newStatus)
|
||||
if (result?.error) { toast.error(result.error); return }
|
||||
setProjects(prev => prev.map(p => p.id === projectId ? { ...p, status: newStatus } : p))
|
||||
setProjects(prev => prev.map(p => p.id === projectId ? { ...p, status: newStatus as Project['status'] } : p))
|
||||
}
|
||||
|
||||
async function handleDelete(projectId: string) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export default async function SettingsPage() {
|
||||
const { data: space } = await supabase
|
||||
.from('spaces').select('*').eq('id', member.space_id).single()
|
||||
|
||||
if (!space) redirect('/dashboard')
|
||||
|
||||
const { data: integrations } = await supabase
|
||||
.from('integrations').select('*').eq('space_id', member.space_id)
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
|
||||
const existing = integrations.find(i => i.platform === config.platform)
|
||||
const initialForm: Record<string, string> = {}
|
||||
config.fields.forEach(f => {
|
||||
initialForm[f.key] = existing?.config?.[f.key] || ''
|
||||
initialForm[f.key] = (existing?.config as Record<string, string> | undefined)?.[f.key] || ''
|
||||
})
|
||||
setIntegrationForm(initialForm)
|
||||
setEditingIntegration(config)
|
||||
|
||||
@@ -86,7 +86,7 @@ export function TasksClient({ tasks: initialTasks, members, currentUserId, space
|
||||
const member = members.find(m => m.user_id === currentUserId)
|
||||
setTasks(prev => prev.map(t =>
|
||||
t.id === taskId
|
||||
? { ...t, status: 'claimed', claimed_by: currentUserId, claimed_by_name: member?.display_name }
|
||||
? { ...t, status: 'claimed', claimed_by: currentUserId, claimed_by_name: member?.display_name } as Task
|
||||
: t
|
||||
))
|
||||
}
|
||||
|
||||
@@ -265,15 +265,18 @@ export async function POST(
|
||||
.eq('external_id', inv.id)
|
||||
.maybeSingle()
|
||||
if (!dupe) {
|
||||
await admin.from('payments').insert(
|
||||
stripeInvoiceToPaymentRow({
|
||||
inv,
|
||||
spaceId,
|
||||
memberId,
|
||||
eventId: event.id,
|
||||
nowIso: new Date().toISOString(),
|
||||
}),
|
||||
)
|
||||
const paymentRow = stripeInvoiceToPaymentRow({
|
||||
inv,
|
||||
spaceId,
|
||||
memberId,
|
||||
eventId: event.id,
|
||||
nowIso: new Date().toISOString(),
|
||||
})
|
||||
await admin.from('payments').insert({
|
||||
...paymentRow,
|
||||
status: paymentRow.status as 'linked' | 'unlinked',
|
||||
link_status: paymentRow.link_status as 'linked' | 'unlinked',
|
||||
})
|
||||
}
|
||||
if (memberId) {
|
||||
await admin
|
||||
|
||||
@@ -25,6 +25,16 @@ export default async function PublicFormPage({
|
||||
}
|
||||
|
||||
const form = res.data
|
||||
if (!form) {
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center p-6 text-center">
|
||||
<h1 className="text-xl font-semibold">This form is not available</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
It may be unpublished, closed, or the link is wrong.
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -30,7 +30,7 @@ export function TrackClient({
|
||||
setError(res.error)
|
||||
return
|
||||
}
|
||||
setView(res.data)
|
||||
setView(res.data ?? null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -68,7 +68,8 @@ export function MemberCertificationsDialog({
|
||||
if ('error' in g && g.error) {
|
||||
toast.error(g.error)
|
||||
} else {
|
||||
setGrants(((g as { data: Grant[] }).data) ?? [])
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
setGrants(((g as unknown as { data: Grant[] }).data) ?? [])
|
||||
}
|
||||
if (!('error' in t) || !t.error) {
|
||||
setTypes(((t as { data: CertType[] }).data ?? []).filter(c => c.is_active))
|
||||
|
||||
@@ -35,7 +35,8 @@ export function SessionAttendance({
|
||||
toast.error(res.error)
|
||||
return
|
||||
}
|
||||
setRows(((res as { data: Signup[] }).data) ?? [])
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
setRows(((res as unknown as { data: Signup[] }).data) ?? [])
|
||||
}, [sessionId])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,7 +33,8 @@ export function EquipmentReservations({ equipmentId }: { equipmentId: string })
|
||||
toast.error(res.error)
|
||||
return
|
||||
}
|
||||
setRows(((res as { data: Reservation[] }).data) ?? [])
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
setRows(((res as unknown as { data: Reservation[] }).data) ?? [])
|
||||
}, [equipmentId])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+24
-3
@@ -432,7 +432,26 @@ export async function listUpcomingSessions() {
|
||||
.order('starts_at', { ascending: true })
|
||||
if (error) return { error: error.message }
|
||||
|
||||
const rows = (sessions ?? []).filter(
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const rows = ((sessions ?? []) as unknown as Array<{
|
||||
id: string
|
||||
class_id: string
|
||||
starts_at: string
|
||||
ends_at: string | null
|
||||
location: string | null
|
||||
capacity: number | null
|
||||
status: string
|
||||
notes: string | null
|
||||
classes: {
|
||||
title: string
|
||||
description: string | null
|
||||
payment_link: string | null
|
||||
capacity: number | null
|
||||
is_active: boolean
|
||||
grants_certification_id: string | null
|
||||
required_form_id: string | null
|
||||
} | null
|
||||
}>).filter(
|
||||
(s: { classes?: { is_active?: boolean } | null }) => s.classes?.is_active !== false,
|
||||
)
|
||||
const ids = rows.map((s: { id: string }) => s.id)
|
||||
@@ -536,7 +555,8 @@ export async function signUpForClass(input: unknown) {
|
||||
.eq('space_id', member.space_id)
|
||||
.maybeSingle()
|
||||
if (!session) return { error: 'Session not found' }
|
||||
const cls = (session as {
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const cls = (session as unknown as {
|
||||
classes?: { title: string | null; capacity: number | null; is_active: boolean; required_form_id: string | null } | null
|
||||
}).classes
|
||||
if (cls && cls.is_active === false) return { error: 'This class is no longer available.' }
|
||||
@@ -827,7 +847,8 @@ export async function completeSession(input: unknown) {
|
||||
.eq('space_id', member.space_id)
|
||||
if (upErr) return { error: upErr.message }
|
||||
|
||||
const cls = (session as { classes?: { title: string; grants_certification_id: string | null } | null }).classes
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const cls = (session as unknown as { classes?: { title: string; grants_certification_id: string | null } | null }).classes
|
||||
let issued = 0
|
||||
let certSkipped = false
|
||||
if (cls?.grants_certification_id) {
|
||||
|
||||
+2
-1
@@ -619,7 +619,8 @@ export async function listDoorCards(input: unknown) {
|
||||
|
||||
return {
|
||||
data: (cards ?? []).map(c => {
|
||||
const sm = c.space_members as { display_name: string | null } | { display_name: string | null }[] | null
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const sm = c.space_members as unknown as { display_name: string | null } | { display_name: string | null }[] | null
|
||||
const name = Array.isArray(sm) ? sm[0]?.display_name : sm?.display_name
|
||||
return {
|
||||
cardId: c.id as string,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
getSpaceName,
|
||||
buildManageUrl,
|
||||
} from '@/lib/notifications/enqueue'
|
||||
import type { Json } from '@/types/database'
|
||||
|
||||
// Associate prior unlinked submissions in a space with a member by email
|
||||
// (case-insensitive, ILIKE-escaped so `_`/`%` in an address are literal).
|
||||
@@ -530,7 +531,7 @@ export async function submitForm(input: unknown) {
|
||||
space_id: form.space_id,
|
||||
member_id: linkedMemberId,
|
||||
submitter_email: submitterEmail,
|
||||
answers: answers.value,
|
||||
answers: answers.value as Json,
|
||||
form_snapshot: form.schema,
|
||||
legal_text_snapshot: form.legal_text ?? null,
|
||||
form_version: form.version,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createClient } from '@/lib/supabase/server'
|
||||
import { requireMemberWithRole } from '@/lib/auth-helpers'
|
||||
import { ADMIN_ROLES } from '@/lib/permissions'
|
||||
import { importMembersSchema } from '@/lib/validations'
|
||||
import type { TablesInsert } from '@/types/database'
|
||||
|
||||
export async function importMembers(rows: unknown) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
@@ -19,7 +20,7 @@ export async function importMembers(rows: unknown) {
|
||||
// Validate per row so one bad row does not reject the whole file; the
|
||||
// schema lowercases email and normalizes dates via flexibleDateTime.
|
||||
const rowSchema = importMembersSchema.element
|
||||
const valid: Array<Record<string, unknown>> = []
|
||||
const valid: Array<TablesInsert<'space_members'>> = []
|
||||
let skipped = 0
|
||||
for (const raw of rows) {
|
||||
const r = rowSchema.safeParse(raw)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { createAdminClient } from '@/lib/supabase/admin'
|
||||
import type { Json } from '@/types/database'
|
||||
import { requireMember, requireMemberWithRole, parseInput } from '@/lib/auth-helpers'
|
||||
import {
|
||||
createOnboardingStepSchema,
|
||||
@@ -45,7 +46,7 @@ export async function createOnboardingStep(input: {
|
||||
step_type: v.data.step_type,
|
||||
title: v.data.title,
|
||||
body: v.data.body ?? null,
|
||||
config: v.data.config ?? {},
|
||||
config: (v.data.config ?? {}) as Json,
|
||||
is_enabled: v.data.is_enabled ?? true,
|
||||
is_required: v.data.is_required ?? false,
|
||||
is_system: false,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
linkPaymentSchema,
|
||||
importPaymentsCsvSchema,
|
||||
} from '@/lib/validations'
|
||||
import type { Enums } from '@/types/database'
|
||||
import type { Enums, TablesInsert } from '@/types/database'
|
||||
|
||||
type PaymentPlatform = Enums<'payment_platform'>
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function importPaymentsCsv(rows: unknown) {
|
||||
// transaction_date. Invalid rows are skipped and counted, not silently
|
||||
// dropped without feedback.
|
||||
const rowSchema = importPaymentsCsvSchema.element
|
||||
const inserts: Array<Record<string, unknown>> = []
|
||||
const inserts: Array<TablesInsert<'payments'>> = []
|
||||
let skipped = 0
|
||||
for (const raw of rows) {
|
||||
const r = rowSchema.safeParse(raw)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { TablesInsert } from '@/types/database'
|
||||
import { requireMember, requireMemberWithRole, parseInput } from '@/lib/auth-helpers'
|
||||
import { ADMIN_ROLES } from '@/lib/permissions'
|
||||
import { createSecretSchema, updateSecretSchema, uuidSchema } from '@/lib/validations'
|
||||
@@ -45,7 +46,7 @@ export async function createSecret(formData: {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('secrets')
|
||||
.insert(row)
|
||||
.insert(row as TablesInsert<'secrets'>)
|
||||
.select('id, title, label, area, description, icon, encryption_version, created_at, updated_at, space_id, created_by')
|
||||
.single()
|
||||
|
||||
|
||||
@@ -257,7 +257,8 @@ export async function listMemberBilling() {
|
||||
if (error) return { error: error.message }
|
||||
return {
|
||||
data: (data ?? []).map(r => {
|
||||
const sm = r.space_members as { display_name: string | null; email: string | null; tier: string | null } | { display_name: string | null; email: string | null; tier: string | null }[] | null
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const sm = r.space_members as unknown as { display_name: string | null; email: string | null; tier: string | null } | { display_name: string | null; email: string | null; tier: string | null }[] | null
|
||||
const mem = Array.isArray(sm) ? sm[0] : sm
|
||||
return {
|
||||
memberId: r.member_id as string,
|
||||
|
||||
@@ -111,7 +111,7 @@ async function egress(opts: {
|
||||
}
|
||||
}
|
||||
const snippet = redactDoorSecrets(received.slice(0, opts.snippetLen), opts.redactSecret, opts.redactAuthParam)
|
||||
return { ok: res.ok, status: res.status, snippet }
|
||||
return { ok: res.ok, status: res.status, snippet } as DoorCallResult
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'request failed'
|
||||
return { ok: false, reason: redactDoorSecrets(msg, opts.redactSecret, opts.redactAuthParam) }
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@
|
||||
// outbound actions.
|
||||
|
||||
import type { createAdminClient } from '@/lib/supabase/admin'
|
||||
import type { TablesInsert } from '@/types/database'
|
||||
import { callDoor } from './executor'
|
||||
import { resolveDoorSecret } from './secret'
|
||||
import { encodeHeatSyncControl, applyTemplate, redactDoorSecrets } from '@/lib/door-logic'
|
||||
@@ -88,7 +89,7 @@ export async function ingestEvents(
|
||||
null,
|
||||
)
|
||||
|
||||
const row: Record<string, unknown> = {
|
||||
const row: TablesInsert<'door_access_log'> = {
|
||||
space_id: spaceId,
|
||||
connection_id: connectionId,
|
||||
actor_member_id: null,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// module: it takes a Supabase admin client and is imported by server actions
|
||||
// and route handlers. Secrets are server-only and never returned to a client.
|
||||
import type { createAdminClient } from '@/lib/supabase/admin'
|
||||
import type { TablesInsert } from '@/types/database'
|
||||
import { encryptSecret, decryptSecret, encryptionAvailable } from '@/lib/secrets/crypto'
|
||||
|
||||
type Admin = ReturnType<typeof createAdminClient>
|
||||
@@ -33,7 +34,11 @@ export async function storeSecret(
|
||||
row.value = value
|
||||
row.encryption_version = 0
|
||||
}
|
||||
const { data, error } = await admin.from('secrets').insert(row).select('id').single()
|
||||
const { data, error } = await admin
|
||||
.from('secrets')
|
||||
.insert(row as TablesInsert<'secrets'>)
|
||||
.select('id')
|
||||
.single()
|
||||
if (error || !data) return null
|
||||
return data.id as string
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import Stripe from 'stripe'
|
||||
import { STRIPE_API_VERSION } from '@/lib/stripe-logic'
|
||||
|
||||
export function getStripe(secretKey: string): Stripe {
|
||||
return new Stripe(secretKey, { apiVersion: STRIPE_API_VERSION as Stripe.LatestApiVersion })
|
||||
return new Stripe(secretKey, { apiVersion: STRIPE_API_VERSION })
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// webhook signing secret live in the AES-256-GCM secrets vault, referenced
|
||||
// by id from config.
|
||||
import type { createAdminClient } from '@/lib/supabase/admin'
|
||||
import type { TablesInsert } from '@/types/database'
|
||||
import { encryptSecret, decryptSecret, encryptionAvailable } from '@/lib/secrets/crypto'
|
||||
|
||||
type Admin = ReturnType<typeof createAdminClient>
|
||||
@@ -42,7 +43,11 @@ export async function storeStripeSecret(
|
||||
row.value = value
|
||||
row.encryption_version = 0
|
||||
}
|
||||
const { data, error } = await admin.from('secrets').insert(row).select('id').single()
|
||||
const { data, error } = await admin
|
||||
.from('secrets')
|
||||
.insert(row as TablesInsert<'secrets'>)
|
||||
.select('id')
|
||||
.single()
|
||||
if (error || !data) return null
|
||||
return data.id as string
|
||||
}
|
||||
|
||||
+10
-2
@@ -1,9 +1,17 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Database } from '@/types/database'
|
||||
|
||||
export function createClient() {
|
||||
/**
|
||||
* Browser Supabase client. The explicit `SupabaseClient<Database>` return type
|
||||
* mirrors `lib/supabase/server.ts`: `@supabase/ssr` returns supabase-js's older
|
||||
* 3-generic client shape, which — against the installed 5-generic supabase-js —
|
||||
* collapses the schema to `never`, erasing row typing for every caller. The one
|
||||
* boundary cast re-asserts `SupabaseClient<Database>` so callers get typed rows.
|
||||
*/
|
||||
export function createClient(): SupabaseClient<Database> {
|
||||
return createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
)
|
||||
) as unknown as SupabaseClient<Database>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user