diff --git a/__tests__/email-send.test.ts b/__tests__/email-send.test.ts index 340e254..ceb5633 100644 --- a/__tests__/email-send.test.ts +++ b/__tests__/email-send.test.ts @@ -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' }, diff --git a/__tests__/markdown-anchors.test.ts b/__tests__/markdown-anchors.test.ts index df3411f..db13e3c 100644 --- a/__tests__/markdown-anchors.test.ts +++ b/__tests__/markdown-anchors.test.ts @@ -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('') diff --git a/__tests__/notifications-enqueue.test.ts b/__tests__/notifications-enqueue.test.ts index 85d691e..92313a1 100644 --- a/__tests__/notifications-enqueue.test.ts +++ b/__tests__/notifications-enqueue.test.ts @@ -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', diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index a95b5ad..461521c 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -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('??') }) diff --git a/app/(app)/comms/comms-client.tsx b/app/(app)/comms/comms-client.tsx index 8ee964c..bb6ecf2 100644 --- a/app/(app)/comms/comms-client.tsx +++ b/app/(app)/comms/comms-client.tsx @@ -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, diff --git a/app/(app)/comms/page.tsx b/app/(app)/comms/page.tsx index 2b2bcf2..9079d6f 100644 --- a/app/(app)/comms/page.tsx +++ b/app/(app)/comms/page.tsx @@ -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 + return } channels={channels ?? []} /> } diff --git a/app/(app)/contacts/contacts-client.tsx b/app/(app)/contacts/contacts-client.tsx index 82fafe0..e8b1c12 100644 --- a/app/(app)/contacts/contacts-client.tsx +++ b/app/(app)/contacts/contacts-client.tsx @@ -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) } diff --git a/app/(app)/customize/panels/tiers-panel.tsx b/app/(app)/customize/panels/tiers-panel.tsx index 098eafc..fc39635 100644 --- a/app/(app)/customize/panels/tiers-panel.tsx +++ b/app/(app)/customize/panels/tiers-panel.tsx @@ -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 }]) diff --git a/app/(app)/forms/[id]/results/page.tsx b/app/(app)/forms/[id]/results/page.tsx index a2bc01d..0870487 100644 --- a/app/(app)/forms/[id]/results/page.tsx +++ b/app/(app)/forms/[id]/results/page.tsx @@ -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 | null + form_version: number + created_at: string + }[] + } /> ) } diff --git a/app/(app)/forms/[id]/results/results-client.tsx b/app/(app)/forms/[id]/results/results-client.tsx index c06ad23..8678ba9 100644 --- a/app/(app)/forms/[id]/results/results-client.tsx +++ b/app/(app)/forms/[id]/results/results-client.tsx @@ -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) diff --git a/app/(app)/me/page.tsx b/app/(app)/me/page.tsx index 16b4a3d..9fb5e5d 100644 --- a/app/(app)/me/page.tsx +++ b/app/(app)/me/page.tsx @@ -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 : {} diff --git a/app/(app)/ops/[id]/kb-entry-editor.tsx b/app/(app)/ops/[id]/kb-entry-editor.tsx index 445415c..40821ac 100644 --- a/app/(app)/ops/[id]/kb-entry-editor.tsx +++ b/app/(app)/ops/[id]/kb-entry-editor.tsx @@ -96,7 +96,7 @@ export default function KbEntryEditor({ entry, member }: Props) { 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" > @@ -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 } diff --git a/app/(app)/ops/page.tsx b/app/(app)/ops/page.tsx index ec28945..64555ea 100644 --- a/app/(app)/ops/page.tsx +++ b/app/(app)/ops/page.tsx @@ -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} diff --git a/app/(app)/payments/page.tsx b/app/(app)/payments/page.tsx index a67478c..0f5b2f7 100644 --- a/app/(app)/payments/page.tsx +++ b/app/(app)/payments/page.tsx @@ -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 ( ['payments']} members={members ?? []} integrations={integrations ?? []} currentRole={member.role} diff --git a/app/(app)/projects/projects-client.tsx b/app/(app)/projects/projects-client.tsx index 78bc4a0..2a9f662 100644 --- a/app/(app)/projects/projects-client.tsx +++ b/app/(app)/projects/projects-client.tsx @@ -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) { diff --git a/app/(app)/settings/page.tsx b/app/(app)/settings/page.tsx index 15add88..52ddfae 100644 --- a/app/(app)/settings/page.tsx +++ b/app/(app)/settings/page.tsx @@ -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) diff --git a/app/(app)/settings/settings-client.tsx b/app/(app)/settings/settings-client.tsx index 222b7c6..4fba035 100644 --- a/app/(app)/settings/settings-client.tsx +++ b/app/(app)/settings/settings-client.tsx @@ -194,7 +194,7 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo const existing = integrations.find(i => i.platform === config.platform) const initialForm: Record = {} config.fields.forEach(f => { - initialForm[f.key] = existing?.config?.[f.key] || '' + initialForm[f.key] = (existing?.config as Record | undefined)?.[f.key] || '' }) setIntegrationForm(initialForm) setEditingIntegration(config) diff --git a/app/(app)/tasks/tasks-client.tsx b/app/(app)/tasks/tasks-client.tsx index edac896..1d46047 100644 --- a/app/(app)/tasks/tasks-client.tsx +++ b/app/(app)/tasks/tasks-client.tsx @@ -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 )) } diff --git a/app/api/stripe/webhook/[space]/route.ts b/app/api/stripe/webhook/[space]/route.ts index 0e181e5..62c86d7 100644 --- a/app/api/stripe/webhook/[space]/route.ts +++ b/app/api/stripe/webhook/[space]/route.ts @@ -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 diff --git a/app/f/[space]/[slug]/page.tsx b/app/f/[space]/[slug]/page.tsx index 6649b61..d0b0c9a 100644 --- a/app/f/[space]/[slug]/page.tsx +++ b/app/f/[space]/[slug]/page.tsx @@ -25,6 +25,16 @@ export default async function PublicFormPage({ } const form = res.data + if (!form) { + return ( +
+

This form is not available

+

+ It may be unpublished, closed, or the link is wrong. +

+
+ ) + } const supabase = await createClient() const { data: { user }, diff --git a/app/track/track-client.tsx b/app/track/track-client.tsx index 1faaa33..ea00754 100644 --- a/app/track/track-client.tsx +++ b/app/track/track-client.tsx @@ -30,7 +30,7 @@ export function TrackClient({ setError(res.error) return } - setView(res.data) + setView(res.data ?? null) } useEffect(() => { diff --git a/components/certifications/member-certifications-dialog.tsx b/components/certifications/member-certifications-dialog.tsx index 3645f6b..6bcbb6c 100644 --- a/components/certifications/member-certifications-dialog.tsx +++ b/components/certifications/member-certifications-dialog.tsx @@ -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)) diff --git a/components/classes/session-attendance.tsx b/components/classes/session-attendance.tsx index 58c116b..137326c 100644 --- a/components/classes/session-attendance.tsx +++ b/components/classes/session-attendance.tsx @@ -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(() => { diff --git a/components/equipment/equipment-reservations.tsx b/components/equipment/equipment-reservations.tsx index bed9f59..95fbf3d 100644 --- a/components/equipment/equipment-reservations.tsx +++ b/components/equipment/equipment-reservations.tsx @@ -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(() => { diff --git a/lib/actions/classes.ts b/lib/actions/classes.ts index c612e2e..e7dfb43 100644 --- a/lib/actions/classes.ts +++ b/lib/actions/classes.ts @@ -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) { diff --git a/lib/actions/door.ts b/lib/actions/door.ts index ee4b8d3..c57944d 100644 --- a/lib/actions/door.ts +++ b/lib/actions/door.ts @@ -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, diff --git a/lib/actions/forms.ts b/lib/actions/forms.ts index 9f5126e..584db14 100644 --- a/lib/actions/forms.ts +++ b/lib/actions/forms.ts @@ -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, diff --git a/lib/actions/imports.ts b/lib/actions/imports.ts index 761be74..d42d41d 100644 --- a/lib/actions/imports.ts +++ b/lib/actions/imports.ts @@ -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> = [] + const valid: Array> = [] let skipped = 0 for (const raw of rows) { const r = rowSchema.safeParse(raw) diff --git a/lib/actions/onboarding.ts b/lib/actions/onboarding.ts index 1a65314..c8b3388 100644 --- a/lib/actions/onboarding.ts +++ b/lib/actions/onboarding.ts @@ -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, diff --git a/lib/actions/payments.ts b/lib/actions/payments.ts index b11b70c..dcced2f 100644 --- a/lib/actions/payments.ts +++ b/lib/actions/payments.ts @@ -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> = [] + const inserts: Array> = [] let skipped = 0 for (const raw of rows) { const r = rowSchema.safeParse(raw) diff --git a/lib/actions/secrets.ts b/lib/actions/secrets.ts index e3af728..c62ee6e 100644 --- a/lib/actions/secrets.ts +++ b/lib/actions/secrets.ts @@ -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() diff --git a/lib/actions/stripe.ts b/lib/actions/stripe.ts index 385b144..0e6386f 100644 --- a/lib/actions/stripe.ts +++ b/lib/actions/stripe.ts @@ -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, diff --git a/lib/door/executor.ts b/lib/door/executor.ts index 3398db6..651824b 100644 --- a/lib/door/executor.ts +++ b/lib/door/executor.ts @@ -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) } diff --git a/lib/door/ingest.ts b/lib/door/ingest.ts index 9fe30fd..63ec452 100644 --- a/lib/door/ingest.ts +++ b/lib/door/ingest.ts @@ -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 = { + const row: TablesInsert<'door_access_log'> = { space_id: spaceId, connection_id: connectionId, actor_member_id: null, diff --git a/lib/secrets/vault.ts b/lib/secrets/vault.ts index 3152a31..422f321 100644 --- a/lib/secrets/vault.ts +++ b/lib/secrets/vault.ts @@ -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 @@ -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 } diff --git a/lib/stripe/client.ts b/lib/stripe/client.ts index b82ce0b..614b7b7 100644 --- a/lib/stripe/client.ts +++ b/lib/stripe/client.ts @@ -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 }) } diff --git a/lib/stripe/config.ts b/lib/stripe/config.ts index 8c0bc7a..c92f06a 100644 --- a/lib/stripe/config.ts +++ b/lib/stripe/config.ts @@ -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 @@ -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 } diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts index ace8e20..ace7992 100644 --- a/lib/supabase/client.ts +++ b/lib/supabase/client.ts @@ -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` 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` so callers get typed rows. + */ +export function createClient(): SupabaseClient { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ) + ) as unknown as SupabaseClient }