@@ -123,6 +123,67 @@ const registrationMode = (env: Env): "open" | "approval_required" => {
123123 return value === "open" ? "open" : "approval_required" ;
124124} ;
125125
126+ const REQUIRED_COLUMNS : Record < string , string [ ] > = {
127+ users : [
128+ "id" ,
129+ "username" ,
130+ "email" ,
131+ "bio" ,
132+ "access_request_note" ,
133+ "idp_email" ,
134+ "idp_email_verified" ,
135+ "avatar_url" ,
136+ "is_admin" ,
137+ "is_approved" ,
138+ "approved_at" ,
139+ "approved_by_user_id" ,
140+ "created_at" ,
141+ "updated_at" ,
142+ ] ,
143+ sites : [
144+ "id" ,
145+ "owner_user_id" ,
146+ "created_by_user_id" ,
147+ "last_edited_by_user_id" ,
148+ "created_at" ,
149+ "last_edited_at" ,
150+ "name" ,
151+ "visibility" ,
152+ "payload_json" ,
153+ "updated_at" ,
154+ ] ,
155+ simulations : [
156+ "id" ,
157+ "owner_user_id" ,
158+ "created_by_user_id" ,
159+ "last_edited_by_user_id" ,
160+ "created_at" ,
161+ "last_edited_at" ,
162+ "name" ,
163+ "visibility" ,
164+ "payload_json" ,
165+ "updated_at" ,
166+ ] ,
167+ deleted_users : [ "id" , "deleted_at" , "deleted_by_user_id" ] ,
168+ site_roles : [ "site_id" , "user_id" , "role" , "created_at" ] ,
169+ simulation_roles : [ "simulation_id" , "user_id" , "role" , "created_at" ] ,
170+ resource_changes : [ "id" , "resource_kind" , "resource_id" , "action" , "actor_user_id" , "changed_at" , "note" ] ,
171+ } ;
172+
173+ export const getSchemaDiagnostics = async ( env : Env ) : Promise < {
174+ ok : boolean ;
175+ missing : Array < { table : string ; columns : string [ ] } > ;
176+ } > => {
177+ const missing : Array < { table : string ; columns : string [ ] } > = [ ] ;
178+ for ( const [ table , required ] of Object . entries ( REQUIRED_COLUMNS ) ) {
179+ const pragma = await env . DB . prepare ( `PRAGMA table_info(${ table } )` ) . all < { name : string } > ( ) ;
180+ const existing = new Set ( pragma . results . map ( ( col ) => col . name ) ) ;
181+ const missingColumns = required . filter ( ( col ) => ! existing . has ( col ) ) ;
182+ if ( missingColumns . length ) missing . push ( { table, columns : missingColumns } ) ;
183+ }
184+ return { ok : missing . length === 0 , missing } ;
185+ } ;
186+
126187const ensureSchema = async ( env : Env ) : Promise < void > => {
127188 if ( ! schemaReady ) {
128189 schemaReady = ( async ( ) => {
@@ -224,47 +285,12 @@ const ensureSchema = async (env: Env): Promise<void> => {
224285 env . DB . prepare ( "CREATE INDEX IF NOT EXISTS idx_resource_changes_lookup ON resource_changes(resource_kind, resource_id, changed_at DESC)" ) ,
225286 ] ) ;
226287
227- const userColumns = await env . DB . prepare ( "PRAGMA table_info(users)" ) . all < { name : string } > ( ) ;
228- const userNames = new Set ( userColumns . results . map ( ( col ) => col . name ) ) ;
229- for ( const query of [
230- ! userNames . has ( "username" ) ? "ALTER TABLE users ADD COLUMN username TEXT" : "" ,
231- ! userNames . has ( "email" ) ? "ALTER TABLE users ADD COLUMN email TEXT" : "" ,
232- ! userNames . has ( "bio" ) ? "ALTER TABLE users ADD COLUMN bio TEXT" : "" ,
233- ! userNames . has ( "access_request_note" ) ? "ALTER TABLE users ADD COLUMN access_request_note TEXT" : "" ,
234- ! userNames . has ( "idp_email" ) ? "ALTER TABLE users ADD COLUMN idp_email TEXT" : "" ,
235- ! userNames . has ( "idp_email_verified" )
236- ? "ALTER TABLE users ADD COLUMN idp_email_verified INTEGER NOT NULL DEFAULT 0"
237- : "" ,
238- ! userNames . has ( "avatar_url" ) ? "ALTER TABLE users ADD COLUMN avatar_url TEXT" : "" ,
239- ! userNames . has ( "is_admin" ) ? "ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0" : "" ,
240- ! userNames . has ( "is_approved" ) ? "ALTER TABLE users ADD COLUMN is_approved INTEGER NOT NULL DEFAULT 0" : "" ,
241- ! userNames . has ( "approved_at" ) ? "ALTER TABLE users ADD COLUMN approved_at TEXT" : "" ,
242- ! userNames . has ( "approved_by_user_id" ) ? "ALTER TABLE users ADD COLUMN approved_by_user_id TEXT" : "" ,
243- ! userNames . has ( "updated_at" ) ? "ALTER TABLE users ADD COLUMN updated_at TEXT" : "" ,
244- ] ) {
245- if ( query ) await env . DB . prepare ( query ) . run ( ) ;
246- }
247-
248- const siteColumns = await env . DB . prepare ( "PRAGMA table_info(sites)" ) . all < { name : string } > ( ) ;
249- const siteNames = new Set ( siteColumns . results . map ( ( col ) => col . name ) ) ;
250- for ( const query of [
251- ! siteNames . has ( "created_by_user_id" ) ? "ALTER TABLE sites ADD COLUMN created_by_user_id TEXT" : "" ,
252- ! siteNames . has ( "last_edited_by_user_id" ) ? "ALTER TABLE sites ADD COLUMN last_edited_by_user_id TEXT" : "" ,
253- ! siteNames . has ( "created_at" ) ? "ALTER TABLE sites ADD COLUMN created_at TEXT" : "" ,
254- ! siteNames . has ( "last_edited_at" ) ? "ALTER TABLE sites ADD COLUMN last_edited_at TEXT" : "" ,
255- ] ) {
256- if ( query ) await env . DB . prepare ( query ) . run ( ) ;
257- }
258-
259- const simColumns = await env . DB . prepare ( "PRAGMA table_info(simulations)" ) . all < { name : string } > ( ) ;
260- const simNames = new Set ( simColumns . results . map ( ( col ) => col . name ) ) ;
261- for ( const query of [
262- ! simNames . has ( "created_by_user_id" ) ? "ALTER TABLE simulations ADD COLUMN created_by_user_id TEXT" : "" ,
263- ! simNames . has ( "last_edited_by_user_id" ) ? "ALTER TABLE simulations ADD COLUMN last_edited_by_user_id TEXT" : "" ,
264- ! simNames . has ( "created_at" ) ? "ALTER TABLE simulations ADD COLUMN created_at TEXT" : "" ,
265- ! simNames . has ( "last_edited_at" ) ? "ALTER TABLE simulations ADD COLUMN last_edited_at TEXT" : "" ,
266- ] ) {
267- if ( query ) await env . DB . prepare ( query ) . run ( ) ;
288+ const diagnostics = await getSchemaDiagnostics ( env ) ;
289+ if ( ! diagnostics . ok ) {
290+ const summary = diagnostics . missing
291+ . map ( ( entry ) => `${ entry . table } : ${ entry . columns . join ( "," ) } ` )
292+ . join ( " | " ) ;
293+ throw new Error ( `Schema out of date. Run D1 migrations. Missing: ${ summary } ` ) ;
268294 }
269295 } ) ( ) ;
270296 }
0 commit comments