This commit is contained in:
parent
2dc78cb95d
commit
756603bae1
@ -34,6 +34,7 @@ COPY --from=build /workspace/package.json /app/package.json
|
||||
COPY --from=build /workspace/node_modules /app/node_modules
|
||||
COPY --from=build /workspace/apps/api/package.json /app/apps/api/package.json
|
||||
COPY --from=build /workspace/apps/api/dist /app/apps/api/dist
|
||||
COPY --from=build /workspace/migrations /app/migrations
|
||||
COPY --from=build /workspace/packages/contracts/package.json /app/packages/contracts/package.json
|
||||
COPY --from=build /workspace/packages/contracts/dist /app/packages/contracts/dist
|
||||
COPY --from=build /workspace/apps/web/dist /app/web
|
||||
|
||||
@ -32,4 +32,4 @@ npm run verify
|
||||
docker build -t fieldlens .
|
||||
```
|
||||
|
||||
Production migrations are applied explicitly before application deployment. The API runtime account is not expected to perform schema DDL on startup.
|
||||
For Arrowstack `database-per-tenant` deployments, the restricted application runtime identity owns DDL inside its dedicated database. The API applies versioned migrations at startup under a PostgreSQL advisory lock before it begins listening; applied migration filenames and checksums are recorded so startup is idempotent and changed historical migrations fail closed.
|
||||
|
||||
70
apps/api/src/migrations.ts
Normal file
70
apps/api/src/migrations.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { pool } from './db.js'
|
||||
import { log } from './log.js'
|
||||
|
||||
const MIGRATION_LOCK_ID = 884251031
|
||||
const migrationsDir = process.env.MIGRATIONS_DIR?.trim() || path.resolve(process.cwd(), 'migrations')
|
||||
|
||||
function checksum(sql: string) {
|
||||
return createHash('sha256').update(sql).digest('hex')
|
||||
}
|
||||
|
||||
export async function runMigrations() {
|
||||
const client = await pool.connect()
|
||||
try {
|
||||
await client.query('SELECT pg_advisory_lock($1)', [MIGRATION_LOCK_ID])
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS fieldlens_schema_migrations (
|
||||
filename text PRIMARY KEY,
|
||||
checksum text NOT NULL,
|
||||
applied_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
`)
|
||||
|
||||
const files = (await readdir(migrationsDir))
|
||||
.filter((name) => /^\d{3}_.+\.sql$/.test(name))
|
||||
.sort()
|
||||
|
||||
if (files.length === 0) throw new Error('No FieldLens migrations found')
|
||||
|
||||
for (const filename of files) {
|
||||
const sql = await readFile(path.join(migrationsDir, filename), 'utf8')
|
||||
const digest = checksum(sql)
|
||||
const applied = await client.query<{ checksum: string }>(
|
||||
'SELECT checksum FROM fieldlens_schema_migrations WHERE filename = $1',
|
||||
[filename],
|
||||
)
|
||||
|
||||
if (applied.rows[0]) {
|
||||
if (applied.rows[0].checksum !== digest) {
|
||||
throw new Error(`Applied migration checksum mismatch: ${filename}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
await client.query('BEGIN')
|
||||
try {
|
||||
await client.query(sql)
|
||||
await client.query(
|
||||
'INSERT INTO fieldlens_schema_migrations (filename, checksum) VALUES ($1, $2)',
|
||||
[filename, digest],
|
||||
)
|
||||
await client.query('COMMIT')
|
||||
log('info', 'migration_applied', { filename })
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
log('info', 'migrations_ready', { count: files.length })
|
||||
} finally {
|
||||
try {
|
||||
await client.query('SELECT pg_advisory_unlock($1)', [MIGRATION_LOCK_ID])
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { config } from './config.js'
|
||||
import { loadDeviceContext } from './context.js'
|
||||
import { databaseReady, pool } from './db.js'
|
||||
import { log } from './log.js'
|
||||
import { runMigrations } from './migrations.js'
|
||||
import { processSyncEvent } from './sync.js'
|
||||
|
||||
const app = express()
|
||||
@ -259,6 +260,8 @@ if (config.staticDir) {
|
||||
})
|
||||
}
|
||||
|
||||
await runMigrations()
|
||||
|
||||
const server = app.listen(config.port, '0.0.0.0', () => {
|
||||
log('info', 'server_started', {
|
||||
port: config.port,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user