spine-framework 0.3.73 → 0.3.75
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.
|
@@ -23,6 +23,7 @@ import { resolve, dirname, join, basename } from 'path'
|
|
|
23
23
|
import { fileURLToPath } from 'url'
|
|
24
24
|
import { execSync } from 'child_process'
|
|
25
25
|
import { createClient } from '@supabase/supabase-js'
|
|
26
|
+
import { runMigrations } from './migrate.js'
|
|
26
27
|
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url)
|
|
28
29
|
const __dirname = dirname(__filename)
|
|
@@ -85,12 +86,30 @@ export function registerInstallAppCommands(program: Command) {
|
|
|
85
86
|
}
|
|
86
87
|
console.log(` ✓ App files copied to custom/apps/${appSlug}/`)
|
|
87
88
|
|
|
88
|
-
// 5.
|
|
89
|
+
// 5. Run pending migrations before seeding so schema is up to date
|
|
90
|
+
console.log(' Running migrations...')
|
|
91
|
+
try {
|
|
92
|
+
const migrateResult = await runMigrations({ frameworkOnly: false })
|
|
93
|
+
if (migrateResult === null) {
|
|
94
|
+
console.log(' ○ Skipping migrations (SUPABASE_DB_PASSWORD not set)')
|
|
95
|
+
} else if (migrateResult.applied > 0) {
|
|
96
|
+
console.log(` ✓ Migrations: ${migrateResult.applied} applied, ${migrateResult.skipped} skipped`)
|
|
97
|
+
} else {
|
|
98
|
+
console.log(` ✓ Migrations: up to date (${migrateResult.skipped} already applied)`)
|
|
99
|
+
}
|
|
100
|
+
if (migrateResult && migrateResult.failed > 0) {
|
|
101
|
+
console.warn(` ⚠ ${migrateResult.failed} migration(s) failed — seed may fail`)
|
|
102
|
+
}
|
|
103
|
+
} catch (migrateErr: any) {
|
|
104
|
+
console.warn(` ⚠ Migration error: ${migrateErr.message} — continuing with seed`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 6. Seed types, roles, link-types into database
|
|
89
108
|
if (!opts.seed === false) {
|
|
90
109
|
await seedApp(appDest, appSlug)
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
//
|
|
112
|
+
// 7. Aggregate registration config from manifest into spine.config.json
|
|
94
113
|
const manifestPath = join(appDest, 'manifest.json')
|
|
95
114
|
let manifest: Record<string, any> = {}
|
|
96
115
|
try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {}
|
|
@@ -35,62 +35,32 @@ interface MigrateOptions {
|
|
|
35
35
|
frameworkOnly: boolean
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Run all pending migrations programmatically.
|
|
40
|
+
* Returns { applied, skipped, failed } — throws on connection failure.
|
|
41
|
+
* Silent on missing SUPABASE_DB_PASSWORD (returns null to indicate skipped).
|
|
42
|
+
*/
|
|
43
|
+
export async function runMigrations(opts: { frameworkOnly?: boolean } = {}): Promise<{ applied: number; skipped: number; failed: number } | null> {
|
|
44
44
|
const supabaseUrl = process.env.SUPABASE_URL
|
|
45
45
|
if (!supabaseUrl || supabaseUrl.includes('placeholder') || supabaseUrl.includes('your-project')) {
|
|
46
|
-
|
|
47
|
-
console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/api')
|
|
48
|
-
process.exit(1)
|
|
46
|
+
return null
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
const dbPassword =
|
|
49
|
+
const dbPassword = process.env.SUPABASE_DB_PASSWORD
|
|
52
50
|
if (!dbPassword) {
|
|
53
|
-
|
|
54
|
-
console.error(' Add it to .env or pass --db-password <password>')
|
|
55
|
-
console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/database')
|
|
56
|
-
process.exit(1)
|
|
51
|
+
return null
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
const projectRef = supabaseUrl.replace('https://', '').split('.')[0]
|
|
60
55
|
const connectionString = `postgresql://postgres:${dbPassword}@db.${projectRef}.supabase.co:5432/postgres`
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (migrationFiles.length === 0) {
|
|
66
|
-
console.log('No migration files found.')
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
console.log(` Found ${migrationFiles.length} migration file(s)\n`)
|
|
71
|
-
|
|
72
|
-
if (options.dryRun) {
|
|
73
|
-
for (const file of migrationFiles) {
|
|
74
|
-
console.log(` [dry-run] Would apply: ${file.name}`)
|
|
75
|
-
}
|
|
76
|
-
console.log('\nDry run complete — no changes made.\n')
|
|
77
|
-
return
|
|
78
|
-
}
|
|
57
|
+
const migrationFiles = collectMigrations(opts.frameworkOnly ?? false)
|
|
58
|
+
if (migrationFiles.length === 0) return { applied: 0, skipped: 0, failed: 0 }
|
|
79
59
|
|
|
80
|
-
// Dynamically import pg to avoid loading it at module init time
|
|
81
60
|
const { default: pg } = await import('pg')
|
|
82
61
|
const client = new pg.Client({ connectionString })
|
|
62
|
+
await client.connect()
|
|
83
63
|
|
|
84
|
-
try {
|
|
85
|
-
await client.connect()
|
|
86
|
-
console.log(' Connected to database\n')
|
|
87
|
-
} catch (err: any) {
|
|
88
|
-
console.error(`Error: Could not connect to database: ${err.message}`)
|
|
89
|
-
console.error(' Check your SUPABASE_DB_PASSWORD and that the project is active.')
|
|
90
|
-
process.exit(1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Create migration tracking table if it doesn't exist
|
|
94
64
|
try {
|
|
95
65
|
await client.query(`
|
|
96
66
|
CREATE TABLE IF NOT EXISTS _spine_migrations (
|
|
@@ -99,40 +69,28 @@ async function migrateCommand(options: MigrateOptions): Promise<void> {
|
|
|
99
69
|
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
100
70
|
)
|
|
101
71
|
`)
|
|
102
|
-
} catch {
|
|
103
|
-
// Non-fatal: proceed without tracking
|
|
104
|
-
}
|
|
72
|
+
} catch { /* non-fatal */ }
|
|
105
73
|
|
|
106
|
-
// Load already-applied migrations
|
|
107
74
|
let appliedMigrations = new Set<string>()
|
|
108
75
|
try {
|
|
109
76
|
const { rows } = await client.query('SELECT filename FROM _spine_migrations')
|
|
110
77
|
appliedMigrations = new Set(rows.map((r: { filename: string }) => r.filename))
|
|
111
|
-
} catch {
|
|
112
|
-
// Non-fatal: proceed without tracking
|
|
113
|
-
}
|
|
78
|
+
} catch { /* non-fatal */ }
|
|
114
79
|
|
|
115
|
-
let applied = 0
|
|
116
|
-
let skipped = 0
|
|
117
|
-
let failed = 0
|
|
80
|
+
let applied = 0, skipped = 0, failed = 0
|
|
118
81
|
|
|
119
82
|
for (const { name, path } of migrationFiles) {
|
|
120
83
|
if (appliedMigrations.has(name)) {
|
|
121
|
-
console.log(` Skipping ${name} (already applied)`)
|
|
122
84
|
skipped++
|
|
123
85
|
continue
|
|
124
86
|
}
|
|
125
87
|
|
|
126
88
|
const sql = readFileSync(path, 'utf8')
|
|
127
89
|
process.stdout.write(` Applying ${name}... `)
|
|
128
|
-
|
|
129
90
|
const statements = splitSqlStatements(sql)
|
|
130
91
|
|
|
131
92
|
try {
|
|
132
|
-
for (const stmt of statements)
|
|
133
|
-
await client.query(stmt)
|
|
134
|
-
}
|
|
135
|
-
// Record as applied
|
|
93
|
+
for (const stmt of statements) await client.query(stmt)
|
|
136
94
|
await client.query(
|
|
137
95
|
`INSERT INTO _spine_migrations (filename) VALUES ($1) ON CONFLICT (filename) DO NOTHING`,
|
|
138
96
|
[name]
|
|
@@ -147,10 +105,60 @@ async function migrateCommand(options: MigrateOptions): Promise<void> {
|
|
|
147
105
|
}
|
|
148
106
|
|
|
149
107
|
await client.end()
|
|
108
|
+
return { applied, skipped, failed }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function migrateCommand(options: MigrateOptions): Promise<void> {
|
|
112
|
+
console.log('\nRunning Spine Framework migrations...\n')
|
|
113
|
+
|
|
114
|
+
// Load .env from project root
|
|
115
|
+
loadEnv()
|
|
116
|
+
|
|
117
|
+
const supabaseUrl = process.env.SUPABASE_URL
|
|
118
|
+
if (!supabaseUrl || supabaseUrl.includes('placeholder') || supabaseUrl.includes('your-project')) {
|
|
119
|
+
console.error('Error: SUPABASE_URL not set. Edit your .env file first.')
|
|
120
|
+
console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/api')
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dbPassword = options.dbPassword || process.env.SUPABASE_DB_PASSWORD
|
|
125
|
+
if (!dbPassword) {
|
|
126
|
+
console.error('Error: SUPABASE_DB_PASSWORD not set.')
|
|
127
|
+
console.error(' Add it to .env or pass --db-password <password>')
|
|
128
|
+
console.error(' Get it from: https://supabase.com/dashboard/project/_/settings/database')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Override env with CLI flag if provided
|
|
133
|
+
if (options.dbPassword) process.env.SUPABASE_DB_PASSWORD = options.dbPassword
|
|
134
|
+
|
|
135
|
+
const migrationFiles = collectMigrations(options.frameworkOnly)
|
|
136
|
+
|
|
137
|
+
if (migrationFiles.length === 0) {
|
|
138
|
+
console.log('No migration files found.')
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(` Found ${migrationFiles.length} migration file(s)\n`)
|
|
143
|
+
|
|
144
|
+
if (options.dryRun) {
|
|
145
|
+
for (const file of migrationFiles) {
|
|
146
|
+
console.log(` [dry-run] Would apply: ${file.name}`)
|
|
147
|
+
}
|
|
148
|
+
console.log('\nDry run complete — no changes made.\n')
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = await runMigrations({ frameworkOnly: options.frameworkOnly })
|
|
153
|
+
|
|
154
|
+
if (!result) {
|
|
155
|
+
console.error('Could not connect: SUPABASE_URL or SUPABASE_DB_PASSWORD missing.')
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
150
158
|
|
|
151
|
-
console.log(`\n ${applied} applied, ${skipped} skipped, ${failed} failed`)
|
|
159
|
+
console.log(`\n ${result.applied} applied, ${result.skipped} skipped, ${result.failed} failed`)
|
|
152
160
|
|
|
153
|
-
if (failed > 0) {
|
|
161
|
+
if (result.failed > 0) {
|
|
154
162
|
console.error('\nSome migrations failed. Check errors above.')
|
|
155
163
|
process.exit(1)
|
|
156
164
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
/api/*
|
|
2
|
-
/v2/*
|
|
3
|
-
|
|
1
|
+
/api/* /.netlify/functions/:splat 200
|
|
2
|
+
/v2/* /.netlify/v2/functions/:splat 200
|
|
3
|
+
/spine.config.json /spine.config.json 200
|
|
4
|
+
/* /index.html 200
|