@omg-dev/server 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import { schemaDiff, schemaToSQL, type Schema, type CollectionConfig } from "@omg-dev/schema"
3
+ import { markScoped } from "./db.ts"
4
+
5
+ // ── migrate ───────────────────────────────────────────────────────────────────
6
+
7
+ /**
8
+ * Register which tables are user-scoped so db.ts filters by _owner.
9
+ * MUST be called at runtime even when migrations are skipped — otherwise
10
+ * scoped collections silently leak data across users.
11
+ */
12
+ export function registerScopes(schema: Schema): void {
13
+ for (const [tableName, col] of Object.entries(schema.collections)) {
14
+ if ((col as CollectionConfig).scope === "user") {
15
+ markScoped(tableName)
16
+ }
17
+ }
18
+ }
19
+
20
+ export function migrate(bunDb: Database, schema: Schema): void {
21
+ console.log("[vibes:migrator] Running migrations...")
22
+
23
+ registerScopes(schema)
24
+
25
+ // Read existing tables from sqlite_master
26
+ const existingTablesResult = bunDb
27
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
28
+ .all() as { name: string }[]
29
+
30
+ const existingTableNames = new Set(existingTablesResult.map(r => r.name))
31
+
32
+ // Build old schema from existing DB structure
33
+ const oldCollections: Record<string, {
34
+ fields: Record<string, { type: "string" }>
35
+ scope: "global"
36
+ indexes: never[]
37
+ uniqueIndexes: never[]
38
+ }> = {}
39
+ for (const tableName of existingTableNames) {
40
+ const columns = bunDb
41
+ .prepare(`PRAGMA table_info(${tableName})`)
42
+ .all() as { name: string; type: string }[]
43
+
44
+ const userFields: Record<string, { type: "string" }> = {}
45
+ for (const col of columns) {
46
+ if (["id", "created_at", "updated_at", "_owner"].includes(col.name)) continue
47
+ userFields[col.name] = { type: "string" }
48
+ }
49
+ oldCollections[tableName] = {
50
+ fields: userFields,
51
+ scope: "global",
52
+ indexes: [],
53
+ uniqueIndexes: [],
54
+ }
55
+ }
56
+
57
+ const oldSchema: Schema = { collections: oldCollections }
58
+
59
+ // If no existing tables, just run the full CREATE TABLE statements
60
+ if (existingTableNames.size === 0) {
61
+ const sqls = schemaToSQL(schema)
62
+ for (const sql of sqls) {
63
+ console.log(`[vibes:migrator] Executing: ${sql.split("\n")[0]}...`)
64
+ bunDb.exec(sql)
65
+ }
66
+ console.log("[vibes:migrator] Initial migration complete.")
67
+ return
68
+ }
69
+
70
+ // Ensure the implicit `_owner` column exists on any pre-existing table that
71
+ // is now scope:"user". `_owner` is NOT a declared field, so schemaDiff never
72
+ // emits an add_column for it — yet schemaToSQL adds it on fresh CREATEs. A
73
+ // table first created while global (no _owner) that later becomes scoped
74
+ // would otherwise (a) crash any `.index("_owner", …)` add_index below with
75
+ // "no such column: _owner", and (b) silently lose WS owner-filtering when no
76
+ // other diff exists (the migrations.length === 0 early-return). Add it up
77
+ // front so both the index pass and runtime filtering work. This is the
78
+ // global→user transition path the default-private flip (schema 0.3.0) made
79
+ // reachable for any collection scoped after its table already existed.
80
+ for (const [tableName, col] of Object.entries(schema.collections)) {
81
+ if ((col as CollectionConfig).scope !== "user") continue
82
+ if (!existingTableNames.has(tableName)) continue
83
+ const cols = bunDb
84
+ .prepare(`PRAGMA table_info(${tableName})`)
85
+ .all() as { name: string }[]
86
+ if (cols.some(c => c.name === "_owner")) continue
87
+ console.log(`[vibes:migrator] ADD COLUMN ${tableName}._owner (scope→user)`)
88
+ bunDb.exec(`ALTER TABLE ${tableName} ADD COLUMN _owner TEXT`)
89
+ }
90
+
91
+ // Compute diff and apply safe migrations
92
+ const migrations = schemaDiff(oldSchema, schema)
93
+
94
+ if (migrations.length === 0) {
95
+ console.log("[vibes:migrator] Schema is up to date.")
96
+ return
97
+ }
98
+
99
+ for (const migration of migrations) {
100
+ switch (migration.type) {
101
+ case "create_table": {
102
+ // Use schemaToSQL for this table only
103
+ const partial: Schema = {
104
+ collections: { [migration.table]: schema.collections[migration.table] },
105
+ }
106
+ const sqls = schemaToSQL(partial)
107
+ for (const sql of sqls) {
108
+ console.log(`[vibes:migrator] CREATE TABLE ${migration.table}`)
109
+ bunDb.exec(sql)
110
+ }
111
+ break
112
+ }
113
+
114
+ case "add_column": {
115
+ // SQLite supports ADD COLUMN
116
+ const colType = fieldTypeToSQL(migration.field.type)
117
+ const sql = `ALTER TABLE ${migration.table} ADD COLUMN ${migration.column} ${colType}`
118
+ console.log(`[vibes:migrator] ADD COLUMN ${migration.table}.${migration.column}`)
119
+ bunDb.exec(sql)
120
+ break
121
+ }
122
+
123
+ case "drop_table": {
124
+ // Safety: we log but skip destructive drops in automatic migrations
125
+ console.warn(
126
+ `[vibes:migrator] SKIP drop_table ${migration.table} — run manually if intended`
127
+ )
128
+ break
129
+ }
130
+
131
+ case "drop_column": {
132
+ // SQLite doesn't support DROP COLUMN in older versions — skip
133
+ console.warn(
134
+ `[vibes:migrator] SKIP drop_column ${migration.table}.${migration.column} — run manually if intended`
135
+ )
136
+ break
137
+ }
138
+
139
+ case "add_index": {
140
+ const indexName = `idx_${migration.table}_${migration.columns.join("_")}`
141
+ const sql = `CREATE INDEX IF NOT EXISTS ${indexName} ON ${migration.table} (${migration.columns.join(", ")})`
142
+ console.log(`[vibes:migrator] ADD INDEX ${indexName}`)
143
+ bunDb.exec(sql)
144
+ break
145
+ }
146
+
147
+ case "add_unique_index": {
148
+ const indexName = `uidx_${migration.table}_${migration.columns.join("_")}`
149
+ const sql = `CREATE UNIQUE INDEX IF NOT EXISTS ${indexName} ON ${migration.table} (${migration.columns.join(", ")})`
150
+ console.log(`[vibes:migrator] ADD UNIQUE INDEX ${indexName}`)
151
+ bunDb.exec(sql)
152
+ break
153
+ }
154
+ }
155
+ }
156
+
157
+ console.log(`[vibes:migrator] Applied ${migrations.length} migration(s).`)
158
+ }
159
+
160
+ function fieldTypeToSQL(type: unknown): string {
161
+ if (type === "string") return "TEXT"
162
+ if (type === "number") return "REAL"
163
+ if (type === "boolean") return "INTEGER"
164
+ if (type === "date") return "TEXT"
165
+ if (typeof type === "object" && type !== null) return "TEXT"
166
+ return "TEXT"
167
+ }