init: M1 scaffolding + M2 organization/clients/services CRUD
- monorepo (npm workspaces): apps/api (Fastify+Prisma+TS), apps/web (Vite+React+TS), packages/shared (zod schemas) - SSO via auth.queo.ru: jose+JWKS plugin, requireDocPermission(viewer|user|admin) - DEV_BYPASS_AUTH for local development (hard-checked off in production) - M2: organization upsert, clients CRUD with search, services catalog with soft-delete - BigInt -> Number serializer for Prisma money columns - Embedded Postgres + npm run dev:demo for one-command local boot - Docker compose for queoserver: postgres + api + web (nginx as ingress proxying /api -> api:3030) - First migration 0_init committed (prisma migrate diff) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Dev orchestrator: embedded Postgres → prisma db push → seed → API server.
|
||||
* Запуск: npm run dev:demo (в корне) или tsx scripts/dev-server.ts (в apps/api).
|
||||
*
|
||||
* Использовать ТОЛЬКО для локальной разработки. Защита: процесс выходит,
|
||||
* если NODE_ENV=production или DEV_BYPASS_AUTH не включён (см. ниже).
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import EmbeddedPostgres from 'embedded-postgres';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const apiRoot = resolve(__dirname, '..');
|
||||
|
||||
const PG_PORT = 5433;
|
||||
const PG_USER = 'postgres';
|
||||
const PG_PASSWORD = 'postgres';
|
||||
const PG_DB = 'docmanager';
|
||||
const PG_DATA = resolve(apiRoot, '../../data/embedded-pg');
|
||||
const DATABASE_URL = `postgresql://${PG_USER}:${PG_PASSWORD}@localhost:${PG_PORT}/${PG_DB}?schema=public`;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.error('FATAL: dev-server.ts запрещён в production');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(dirname(PG_DATA), { recursive: true });
|
||||
|
||||
const pg = new EmbeddedPostgres({
|
||||
databaseDir: PG_DATA,
|
||||
user: PG_USER,
|
||||
password: PG_PASSWORD,
|
||||
port: PG_PORT,
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
async function exec(cmd: string, args: string[], env: NodeJS.ProcessEnv): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
const child = spawn(cmd, args, {
|
||||
stdio: 'inherit',
|
||||
cwd: apiRoot,
|
||||
env: { ...process.env, ...env },
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
child.on('exit', (code) => (code === 0 ? res() : rej(new Error(`${cmd} ${args.join(' ')} exited ${code}`))));
|
||||
child.on('error', rej);
|
||||
});
|
||||
}
|
||||
|
||||
let serverChild: ReturnType<typeof spawn> | null = null;
|
||||
let stopping = false;
|
||||
|
||||
async function shutdown(reason: string) {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
console.log(`\n[dev-server] shutdown: ${reason}`);
|
||||
try {
|
||||
if (serverChild && !serverChild.killed) {
|
||||
serverChild.kill('SIGTERM');
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[dev-server] error stopping API:', e);
|
||||
}
|
||||
try {
|
||||
await pg.stop();
|
||||
} catch (e) {
|
||||
console.warn('[dev-server] error stopping pg:', e);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => void shutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
||||
|
||||
async function main() {
|
||||
const dataInitialised = existsSync(resolve(PG_DATA, 'PG_VERSION'));
|
||||
|
||||
if (!dataInitialised) {
|
||||
console.log('[dev-server] initialising embedded Postgres (~80MB binaries on first run)…');
|
||||
await pg.initialise();
|
||||
}
|
||||
|
||||
console.log(`[dev-server] starting Postgres on :${PG_PORT}…`);
|
||||
await pg.start();
|
||||
|
||||
if (!dataInitialised) {
|
||||
console.log(`[dev-server] creating database "${PG_DB}"…`);
|
||||
try {
|
||||
await pg.createDatabase(PG_DB);
|
||||
} catch (e) {
|
||||
console.warn('[dev-server] createDatabase warning:', (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[dev-server] applying schema (prisma db push)…');
|
||||
await exec('npx', ['prisma', 'db', 'push', '--skip-generate', '--accept-data-loss'], { DATABASE_URL });
|
||||
|
||||
console.log('[dev-server] seeding default organization…');
|
||||
await exec('npx', ['tsx', 'prisma/seed.ts'], { DATABASE_URL });
|
||||
|
||||
console.log('[dev-server] starting API server…');
|
||||
serverChild = spawn('npx', ['tsx', 'watch', 'src/server.ts'], {
|
||||
stdio: 'inherit',
|
||||
cwd: apiRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL,
|
||||
DEV_BYPASS_AUTH: '1',
|
||||
PORT: '3030',
|
||||
HOST: '127.0.0.1',
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
serverChild.on('exit', (code) => {
|
||||
if (!stopping) {
|
||||
console.error(`[dev-server] API exited unexpectedly (${code})`);
|
||||
void shutdown('api-exit');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[dev-server] fatal:', err);
|
||||
void shutdown('fatal');
|
||||
});
|
||||
Reference in New Issue
Block a user