22359be40e
Close billing, tenancy, migration, build, and CI/CD gaps identified in the audit: wallet/gateway guards, full-UUID namespaces, idempotent migrations with base schema, stateful service stability, safer Dockerfiles/git builds, and platform chart hardening (Redis auth, RollingUpdate, backups, Swagger off). Co-authored-by: Cursor <cursoragent@cursor.com>
95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generate the greenfield base schema (000_base_schema.sql) by letting TypeORM
|
|
* `synchronize` build every table from the entities against a throwaway
|
|
* Postgres, then dumping the schema. Run when entities change materially:
|
|
*
|
|
* docker run -d --name ch-schemagen -e POSTGRES_PASSWORD=pass \
|
|
* -e POSTGRES_USER=cloudhost -e POSTGRES_DB=cloudhost \
|
|
* -p 55432:5432 postgres:16-alpine
|
|
* node scripts/generate-base-schema.mjs
|
|
*
|
|
* The output is wrapped so it is safe to run on an already-populated database
|
|
* (every statement uses IF NOT EXISTS / duplicate_object guards where possible;
|
|
* the migration runner also records it in schema_migrations so it runs once).
|
|
*/
|
|
import 'reflect-metadata';
|
|
import { DataSource } from 'typeorm';
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const backendRoot = path.resolve(__dirname, '..');
|
|
const outPath = path.join(backendRoot, 'migrations', '000_base_schema.sql');
|
|
|
|
// Use the COMPILED entities (run `npm run build` first) — union-typed columns
|
|
// only carry correct decorator metadata through the project's tsc build.
|
|
const ds = new DataSource({
|
|
type: 'postgres',
|
|
host: process.env.SCHEMA_DB_HOST || '127.0.0.1',
|
|
port: parseInt(process.env.SCHEMA_DB_PORT || '55432', 10),
|
|
username: 'cloudhost',
|
|
password: 'pass',
|
|
database: 'cloudhost',
|
|
entities: [path.join(backendRoot, 'dist/**/*.entity.js')],
|
|
synchronize: true,
|
|
});
|
|
|
|
await ds.initialize();
|
|
await ds.destroy();
|
|
|
|
// Dump schema-only from the container, then strip owner/ACL noise.
|
|
const dumped = execFileSync('docker', [
|
|
'exec', 'ch-schemagen',
|
|
'pg_dump', '-U', 'cloudhost', '-d', 'cloudhost',
|
|
'--schema-only', '--no-owner', '--no-privileges',
|
|
], { maxBuffer: 32 * 1024 * 1024 }).toString();
|
|
|
|
// Strip:
|
|
// - psql client meta-commands that are version-specific (\restrict is
|
|
// pg_dump 16.13+ only) and would break on the migrations image's psql;
|
|
// - the `search_path = ''` reset, which otherwise persists into the trailing
|
|
// `INSERT INTO schema_migrations` the runner appends (unqualified) and the
|
|
// footer below, causing "no schema has been selected to create in".
|
|
const raw = dumped
|
|
.split('\n')
|
|
.filter(
|
|
(line) =>
|
|
!/^\\(restrict|unrestrict)\b/.test(line) &&
|
|
!/set_config\('search_path'/.test(line),
|
|
)
|
|
.join('\n');
|
|
|
|
const header = `-- 000_base_schema.sql — greenfield base schema (generated from TypeORM entities).
|
|
-- Auto-generated by scripts/generate-base-schema.mjs. Do not edit by hand.
|
|
-- Incremental migrations (001+) run afterwards on top of this schema.
|
|
|
|
`;
|
|
|
|
// The legacy pricing-catalog migrations (004-009) target a superseded
|
|
// snake_case pricing schema that is incompatible with the current entities.
|
|
// On greenfield the base schema already creates the entity-shaped pricing
|
|
// tables and the app self-seeds their rows (PricingCatalogService.ensureDefaults
|
|
// on boot), so mark those migrations as already applied to skip them.
|
|
const supersededPricingMigrations = [
|
|
'004_pricing_catalog.sql',
|
|
'005_pricing_catalog_all_runtimes.sql',
|
|
'006_addon_rate_resources.sql',
|
|
'007_optional_service_pricing_matrix.sql',
|
|
'008_application_optional_service_resources.sql',
|
|
'009_optional_service_deploy_defaults.sql',
|
|
];
|
|
const footer = `
|
|
|
|
-- Mark superseded legacy pricing migrations as applied (see generator note).
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
|
|
INSERT INTO schema_migrations (filename) VALUES
|
|
${supersededPricingMigrations.map((m) => ` ('${m}')`).join(',\n')}
|
|
ON CONFLICT (filename) DO NOTHING;
|
|
`;
|
|
|
|
fs.writeFileSync(outPath, header + raw + footer);
|
|
console.log(`Wrote ${outPath} (${raw.length} bytes)`);
|