Phase 1: generate database types and type the Supabase clients
Types are generated from the live database's API schema by scripts/gen-types.mjs (npm run gen:types), in the same shape as supabase gen types, so no Supabase CLI login is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -9,7 +9,8 @@
|
||||
"lint": "eslint",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"check:rls": "node --env-file=.env.local scripts/check-rls.mjs"
|
||||
"check:rls": "node --env-file=.env.local scripts/check-rls.mjs",
|
||||
"gen:types": "node --env-file=.env.local scripts/gen-types.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/ssr": "^0.12.7",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// Generates src/lib/database.types.ts from the live database's API schema, in the same
|
||||
// shape as `supabase gen types typescript` — without needing a Supabase CLI login.
|
||||
// Run with: npm run gen:types (after every migration)
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!url || !serviceKey) {
|
||||
console.error("Missing Supabase env vars. Fill in .env.local first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Foreign keys whose constraint name isn't Postgres' default `<table>_<column>_fkey`.
|
||||
const FK_NAME_OVERRIDES = { "events.current_game_id": "events_current_game_fk" };
|
||||
|
||||
// Columns with a DB default that the API schema doesn't report (it omits array defaults).
|
||||
const EXTRA_DEFAULTS = new Set(["games.placement_points"]);
|
||||
|
||||
const response = await fetch(`${url}/rest/v1/`, {
|
||||
headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Could not read the schema: HTTP ${response.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const { definitions = {} } = await response.json();
|
||||
const tables = Object.keys(definitions).sort();
|
||||
if (tables.length === 0) {
|
||||
console.error("No tables found. Has the migration been applied?");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const enums = {};
|
||||
|
||||
function tsType(column) {
|
||||
if (column.enum && column.format?.startsWith("public.")) {
|
||||
const name = column.format.slice("public.".length);
|
||||
enums[name] = column.enum;
|
||||
return `Database["public"]["Enums"]["${name}"]`;
|
||||
}
|
||||
if (column.type === "array") return `${tsType(column.items)}[]`;
|
||||
if (column.format === "json" || column.format === "jsonb") return "Json";
|
||||
if (column.type === "integer" || column.type === "number") return "number";
|
||||
if (column.type === "boolean") return "boolean";
|
||||
return "string";
|
||||
}
|
||||
|
||||
function tableBlock(table) {
|
||||
const { properties, required = [] } = definitions[table];
|
||||
const columns = Object.keys(properties).sort();
|
||||
const row = [];
|
||||
const insert = [];
|
||||
const update = [];
|
||||
const relationships = [];
|
||||
|
||||
for (const name of columns) {
|
||||
const column = properties[name];
|
||||
const nullable = !required.includes(name);
|
||||
const type = tsType(column) + (nullable ? " | null" : "");
|
||||
const optionalOnInsert =
|
||||
nullable || column.default !== undefined || EXTRA_DEFAULTS.has(`${table}.${name}`);
|
||||
row.push(` ${name}: ${type};`);
|
||||
insert.push(` ${name}${optionalOnInsert ? "?" : ""}: ${type};`);
|
||||
update.push(` ${name}?: ${type};`);
|
||||
|
||||
const fk = column.description?.match(/<fk table='(\w+)' column='(\w+)'\/>/);
|
||||
if (fk) {
|
||||
const fkName = FK_NAME_OVERRIDES[`${table}.${name}`] ?? `${table}_${name}_fkey`;
|
||||
relationships.push(
|
||||
[
|
||||
" {",
|
||||
` foreignKeyName: "${fkName}";`,
|
||||
` columns: ["${name}"];`,
|
||||
" isOneToOne: false;",
|
||||
` referencedRelation: "${fk[1]}";`,
|
||||
` referencedColumns: ["${fk[2]}"];`,
|
||||
" },",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
` ${table}: {`,
|
||||
" Row: {",
|
||||
...row,
|
||||
" };",
|
||||
" Insert: {",
|
||||
...insert,
|
||||
" };",
|
||||
" Update: {",
|
||||
...update,
|
||||
" };",
|
||||
relationships.length ? ` Relationships: [\n${relationships.join("\n")}\n ];` : " Relationships: [];",
|
||||
" };",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
const tableBlocks = tables.map(tableBlock).join("\n");
|
||||
const enumBlock = Object.keys(enums)
|
||||
.sort()
|
||||
.map((name) => ` ${name}: ${enums[name].map((value) => `"${value}"`).join(" | ")};`)
|
||||
.join("\n");
|
||||
|
||||
const output = `// GENERATED by scripts/gen-types.mjs from the live database — do not edit by hand.
|
||||
// Regenerate with: npm run gen:types
|
||||
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
${tableBlocks}
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
${enumBlock}
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Tables<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Row"];
|
||||
export type TablesInsert<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Insert"];
|
||||
export type TablesUpdate<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Update"];
|
||||
export type Enums<T extends keyof Database["public"]["Enums"]> = Database["public"]["Enums"][T];
|
||||
`;
|
||||
|
||||
writeFileSync(new URL("../src/lib/database.types.ts", import.meta.url), output);
|
||||
console.log(`Wrote src/lib/database.types.ts (${tables.length} tables, ${Object.keys(enums).length} enums).`);
|
||||
@@ -0,0 +1,398 @@
|
||||
// GENERATED by scripts/gen-types.mjs from the live database — do not edit by hand.
|
||||
// Regenerate with: npm run gen:types
|
||||
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
adjustments: {
|
||||
Row: {
|
||||
created_at: string;
|
||||
event_id: string;
|
||||
game_id: string | null;
|
||||
id: string;
|
||||
points: number;
|
||||
reason: string | null;
|
||||
team_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string;
|
||||
event_id: string;
|
||||
game_id?: string | null;
|
||||
id?: string;
|
||||
points: number;
|
||||
reason?: string | null;
|
||||
team_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string;
|
||||
event_id?: string;
|
||||
game_id?: string | null;
|
||||
id?: string;
|
||||
points?: number;
|
||||
reason?: string | null;
|
||||
team_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "adjustments_event_id_fkey";
|
||||
columns: ["event_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "events";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "adjustments_game_id_fkey";
|
||||
columns: ["game_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "games";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "adjustments_team_id_fkey";
|
||||
columns: ["team_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "teams";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
criteria: {
|
||||
Row: {
|
||||
description: string | null;
|
||||
game_id: string;
|
||||
id: string;
|
||||
input_type: Database["public"]["Enums"]["criterion_input"];
|
||||
label: string;
|
||||
max_value: number;
|
||||
min_value: number;
|
||||
sort_order: number;
|
||||
step: number;
|
||||
weight: number;
|
||||
};
|
||||
Insert: {
|
||||
description?: string | null;
|
||||
game_id: string;
|
||||
id?: string;
|
||||
input_type?: Database["public"]["Enums"]["criterion_input"];
|
||||
label: string;
|
||||
max_value?: number;
|
||||
min_value?: number;
|
||||
sort_order?: number;
|
||||
step?: number;
|
||||
weight?: number;
|
||||
};
|
||||
Update: {
|
||||
description?: string | null;
|
||||
game_id?: string;
|
||||
id?: string;
|
||||
input_type?: Database["public"]["Enums"]["criterion_input"];
|
||||
label?: string;
|
||||
max_value?: number;
|
||||
min_value?: number;
|
||||
sort_order?: number;
|
||||
step?: number;
|
||||
weight?: number;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "criteria_game_id_fkey";
|
||||
columns: ["game_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "games";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
events: {
|
||||
Row: {
|
||||
created_at: string;
|
||||
current_game_id: string | null;
|
||||
event_date: string | null;
|
||||
frozen_snapshot: Json | null;
|
||||
id: string;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
reveal_active: boolean;
|
||||
reveal_count: number;
|
||||
scoreboard_token: string;
|
||||
scoreboard_visibility: Database["public"]["Enums"]["scoreboard_visibility"];
|
||||
updated_at: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string;
|
||||
current_game_id?: string | null;
|
||||
event_date?: string | null;
|
||||
frozen_snapshot?: Json | null;
|
||||
id?: string;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
reveal_active?: boolean;
|
||||
reveal_count?: number;
|
||||
scoreboard_token?: string;
|
||||
scoreboard_visibility?: Database["public"]["Enums"]["scoreboard_visibility"];
|
||||
updated_at?: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string;
|
||||
current_game_id?: string | null;
|
||||
event_date?: string | null;
|
||||
frozen_snapshot?: Json | null;
|
||||
id?: string;
|
||||
name?: string;
|
||||
owner_id?: string;
|
||||
reveal_active?: boolean;
|
||||
reveal_count?: number;
|
||||
scoreboard_token?: string;
|
||||
scoreboard_visibility?: Database["public"]["Enums"]["scoreboard_visibility"];
|
||||
updated_at?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "events_current_game_fk";
|
||||
columns: ["current_game_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "games";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
games: {
|
||||
Row: {
|
||||
admin_notes: string | null;
|
||||
created_at: string;
|
||||
description: string | null;
|
||||
event_id: string;
|
||||
id: string;
|
||||
judge_notes: string | null;
|
||||
judging_locked: boolean;
|
||||
multiplier: number;
|
||||
open_link_enabled: boolean;
|
||||
open_token: string;
|
||||
placement_points: number[];
|
||||
points_mode: Database["public"]["Enums"]["points_mode"];
|
||||
scoring_mode: Database["public"]["Enums"]["scoring_mode"];
|
||||
sort_order: number;
|
||||
spotlight_team_id: string | null;
|
||||
status: Database["public"]["Enums"]["game_status"];
|
||||
title: string;
|
||||
updated_at: string;
|
||||
};
|
||||
Insert: {
|
||||
admin_notes?: string | null;
|
||||
created_at?: string;
|
||||
description?: string | null;
|
||||
event_id: string;
|
||||
id?: string;
|
||||
judge_notes?: string | null;
|
||||
judging_locked?: boolean;
|
||||
multiplier?: number;
|
||||
open_link_enabled?: boolean;
|
||||
open_token?: string;
|
||||
placement_points?: number[];
|
||||
points_mode?: Database["public"]["Enums"]["points_mode"];
|
||||
scoring_mode?: Database["public"]["Enums"]["scoring_mode"];
|
||||
sort_order?: number;
|
||||
spotlight_team_id?: string | null;
|
||||
status?: Database["public"]["Enums"]["game_status"];
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Update: {
|
||||
admin_notes?: string | null;
|
||||
created_at?: string;
|
||||
description?: string | null;
|
||||
event_id?: string;
|
||||
id?: string;
|
||||
judge_notes?: string | null;
|
||||
judging_locked?: boolean;
|
||||
multiplier?: number;
|
||||
open_link_enabled?: boolean;
|
||||
open_token?: string;
|
||||
placement_points?: number[];
|
||||
points_mode?: Database["public"]["Enums"]["points_mode"];
|
||||
scoring_mode?: Database["public"]["Enums"]["scoring_mode"];
|
||||
sort_order?: number;
|
||||
spotlight_team_id?: string | null;
|
||||
status?: Database["public"]["Enums"]["game_status"];
|
||||
title?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "games_event_id_fkey";
|
||||
columns: ["event_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "events";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "games_spotlight_team_id_fkey";
|
||||
columns: ["spotlight_team_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "teams";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
judges: {
|
||||
Row: {
|
||||
created_at: string;
|
||||
game_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
revoked: boolean;
|
||||
source: Database["public"]["Enums"]["judge_source"];
|
||||
token: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string;
|
||||
game_id: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
revoked?: boolean;
|
||||
source?: Database["public"]["Enums"]["judge_source"];
|
||||
token?: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string;
|
||||
game_id?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
revoked?: boolean;
|
||||
source?: Database["public"]["Enums"]["judge_source"];
|
||||
token?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "judges_game_id_fkey";
|
||||
columns: ["game_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "games";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
scores: {
|
||||
Row: {
|
||||
criterion_id: string;
|
||||
game_id: string;
|
||||
id: string;
|
||||
judge_id: string | null;
|
||||
team_id: string;
|
||||
updated_at: string;
|
||||
value: number;
|
||||
};
|
||||
Insert: {
|
||||
criterion_id: string;
|
||||
game_id: string;
|
||||
id?: string;
|
||||
judge_id?: string | null;
|
||||
team_id: string;
|
||||
updated_at?: string;
|
||||
value: number;
|
||||
};
|
||||
Update: {
|
||||
criterion_id?: string;
|
||||
game_id?: string;
|
||||
id?: string;
|
||||
judge_id?: string | null;
|
||||
team_id?: string;
|
||||
updated_at?: string;
|
||||
value?: number;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "scores_criterion_id_fkey";
|
||||
columns: ["criterion_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "criteria";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "scores_game_id_fkey";
|
||||
columns: ["game_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "games";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "scores_judge_id_fkey";
|
||||
columns: ["judge_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "judges";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "scores_team_id_fkey";
|
||||
columns: ["team_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "teams";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
teams: {
|
||||
Row: {
|
||||
color: string;
|
||||
created_at: string;
|
||||
event_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
};
|
||||
Insert: {
|
||||
color?: string;
|
||||
created_at?: string;
|
||||
event_id: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
sort_order?: number;
|
||||
};
|
||||
Update: {
|
||||
color?: string;
|
||||
created_at?: string;
|
||||
event_id?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
sort_order?: number;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "teams_event_id_fkey";
|
||||
columns: ["event_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "events";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
criterion_input: "blocks" | "number";
|
||||
game_status: "upcoming" | "live" | "done";
|
||||
judge_source: "nominated" | "open_link";
|
||||
points_mode: "raw" | "placement";
|
||||
scoreboard_visibility: "visible" | "frozen" | "hidden";
|
||||
scoring_mode: "judged" | "direct";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Tables<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Row"];
|
||||
export type TablesInsert<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Insert"];
|
||||
export type TablesUpdate<T extends keyof Database["public"]["Tables"]> =
|
||||
Database["public"]["Tables"][T]["Update"];
|
||||
export type Enums<T extends keyof Database["public"]["Enums"]> = Database["public"]["Enums"][T];
|
||||
@@ -2,6 +2,7 @@ import "server-only";
|
||||
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
import type { Database } from "@/lib/database.types";
|
||||
import { serverEnv } from "@/lib/env.server";
|
||||
|
||||
/**
|
||||
@@ -9,7 +10,7 @@ import { serverEnv } from "@/lib/env.server";
|
||||
* AFTER the scoreboard/judge token has been validated, and scope every query by that token.
|
||||
*/
|
||||
export function createAdminClient() {
|
||||
return createClient(serverEnv.NEXT_PUBLIC_SUPABASE_URL, serverEnv.SUPABASE_SERVICE_ROLE_KEY, {
|
||||
return createClient<Database>(serverEnv.NEXT_PUBLIC_SUPABASE_URL, serverEnv.SUPABASE_SERVICE_ROLE_KEY, {
|
||||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
import type { Database } from "@/lib/database.types";
|
||||
import { publicEnv } from "@/lib/env.public";
|
||||
|
||||
/** Anon-key client for the browser: Realtime subscriptions + the admin auth session. */
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
return createBrowserClient<Database>(
|
||||
publicEnv.NEXT_PUBLIC_SUPABASE_URL,
|
||||
publicEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
);
|
||||
|
||||
@@ -3,13 +3,14 @@ import "server-only";
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import type { Database } from "@/lib/database.types";
|
||||
import { publicEnv } from "@/lib/env.public";
|
||||
|
||||
/** Cookie-based client for admin server components/actions. RLS applies as the signed-in admin. */
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(
|
||||
return createServerClient<Database>(
|
||||
publicEnv.NEXT_PUBLIC_SUPABASE_URL,
|
||||
publicEnv.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user