Skip to content

Commit d05e763

Browse files
committed
feat(codegen): add shared PGPM ephemeral DB, multi-target cnc codegen, and export-schema command
- Shared ephemeral DB: When multiple targets in generateMulti() share the same PGPM source, deploy once to a single ephemeral DB and reuse it across targets instead of creating N separate databases. Automatic teardown after all targets. - Multi-target cnc codegen: Add multi-target config detection to cnc codegen command, mirroring graphql-codegen CLI behavior. Supports --target flag. - Export schema from PGPM: New exportSchema()/exportSchemaSimple() functions and CLI commands (graphql-codegen export-schema, cnc export-schema) that deploy a PGPM module to an ephemeral DB, introspect via PostGraphile, and write .graphql schema files. No server required. Supports --api-names and --schemas flags.
1 parent ef638ad commit d05e763

8 files changed

Lines changed: 575 additions & 3 deletions

File tree

graphql/codegen/src/cli/index.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { CLI, CLIOptions, getPackageJson, Inquirerer } from 'inquirerer';
99

1010
import { findConfigFile, loadConfigFile } from '../core/config';
11+
import { exportSchemaSimple } from '../core/export-schema';
1112
import { generate, generateMulti } from '../core/generate';
1213
import { mergeConfig, type GraphQLSDKConfigTarget } from '../types/config';
1314
import {
@@ -26,6 +27,7 @@ graphql-codegen - GraphQL SDK generator for Constructive databases
2627
2728
Usage:
2829
graphql-codegen [options]
30+
graphql-codegen export-schema [options]
2931
3032
Source Options (choose one):
3133
-c, --config <path> Path to config file
@@ -47,8 +49,78 @@ Generator Options:
4749
4850
-h, --help Show this help message
4951
--version Show version number
52+
53+
Export Schema (from PGPM module, no server required):
54+
graphql-codegen export-schema --pgpm-module-path <path> --api-names <list> -o <dir>
55+
graphql-codegen export-schema --pgpm-workspace-path <path> --pgpm-module-name <name> --api-names <list> -o <dir>
56+
--pgpm-module-path <path> Path to PGPM module directory
57+
--pgpm-workspace-path <path> Path to PGPM workspace directory
58+
--pgpm-module-name <name> Module name within workspace
59+
--keep-db Keep ephemeral database after export (debug)
5060
`;
5161

62+
async function handleExportSchema(
63+
argv: Record<string, unknown>,
64+
): Promise<void> {
65+
const pgpmModulePath = argv['pgpm-module-path'] as string | undefined;
66+
const pgpmWorkspacePath = argv['pgpm-workspace-path'] as string | undefined;
67+
const pgpmModuleName = argv['pgpm-module-name'] as string | undefined;
68+
const output = (argv.output || argv.o || '.') as string;
69+
const filename = argv.filename as string | undefined;
70+
const verbose = Boolean(argv.verbose || argv.v);
71+
const keepDb = Boolean(argv['keep-db']);
72+
73+
const rawSchemas = argv.schemas as string | undefined;
74+
const rawApiNames = argv['api-names'] as string | undefined;
75+
const schemas = rawSchemas
76+
? rawSchemas.split(',').map((s) => s.trim()).filter(Boolean)
77+
: undefined;
78+
const apiNames = rawApiNames
79+
? rawApiNames.split(',').map((s) => s.trim()).filter(Boolean)
80+
: undefined;
81+
82+
if (!pgpmModulePath && !(pgpmWorkspacePath && pgpmModuleName)) {
83+
console.error(
84+
'x',
85+
'export-schema requires --pgpm-module-path or both --pgpm-workspace-path and --pgpm-module-name',
86+
);
87+
process.exit(1);
88+
}
89+
90+
if (!schemas?.length && !apiNames?.length) {
91+
console.error(
92+
'x',
93+
'export-schema requires --schemas or --api-names',
94+
);
95+
process.exit(1);
96+
}
97+
98+
const pgpm = pgpmModulePath
99+
? { modulePath: pgpmModulePath }
100+
: { workspacePath: pgpmWorkspacePath!, moduleName: pgpmModuleName! };
101+
102+
const result = await exportSchemaSimple({
103+
pgpm,
104+
apiNames,
105+
schemas,
106+
output,
107+
filename,
108+
keepDb,
109+
verbose,
110+
});
111+
112+
if (result.success) {
113+
console.log('[ok]', result.message);
114+
for (const file of result.files ?? []) {
115+
console.log(` ${file.target}: ${file.path} (schemas: ${file.schemas.join(', ')})`);
116+
}
117+
} else {
118+
console.error('x', result.message);
119+
result.errors?.forEach((e) => console.error(' -', e));
120+
process.exit(1);
121+
}
122+
}
123+
52124
export const commands = async (
53125
argv: Record<string, unknown>,
54126
prompter: Inquirerer,
@@ -65,6 +137,13 @@ export const commands = async (
65137
process.exit(0);
66138
}
67139

140+
const positionalArgs = (argv._ as string[]) ?? [];
141+
if (positionalArgs.includes('export-schema')) {
142+
await handleExportSchema(argv);
143+
prompter.close();
144+
return argv;
145+
}
146+
68147
const hasSourceFlags = Boolean(
69148
argv.endpoint ||
70149
argv.e ||
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
4+
import { PgpmPackage } from '@pgpmjs/core';
5+
import { createEphemeralDb, type EphemeralDbResult } from 'pgsql-client';
6+
import { deployPgpm } from 'pgsql-seed';
7+
8+
import type { PgpmConfig } from '../types/config';
9+
import { buildSchemaSDLFromDatabase } from './database';
10+
import { resolveApiSchemas, validateServicesSchemas } from './introspect/source/api-schemas';
11+
12+
export interface ExportSchemaTarget {
13+
apiNames?: string[];
14+
schemas?: string[];
15+
output: string;
16+
filename?: string;
17+
}
18+
19+
export interface ExportSchemaOptions {
20+
pgpm: PgpmConfig;
21+
targets: ExportSchemaTarget[];
22+
keepDb?: boolean;
23+
verbose?: boolean;
24+
}
25+
26+
export interface ExportSchemaResult {
27+
success: boolean;
28+
message: string;
29+
files?: Array<{ target: string; path: string; schemas: string[] }>;
30+
errors?: string[];
31+
}
32+
33+
function resolveModulePath(pgpm: PgpmConfig): string {
34+
if (pgpm.modulePath) return pgpm.modulePath;
35+
if (pgpm.workspacePath && pgpm.moduleName) {
36+
const workspace = new PgpmPackage(pgpm.workspacePath);
37+
const moduleProject = workspace.getModuleProject(pgpm.moduleName);
38+
const modulePath = moduleProject.getModulePath();
39+
if (!modulePath) {
40+
throw new Error(`Module "${pgpm.moduleName}" not found in workspace`);
41+
}
42+
return modulePath;
43+
}
44+
throw new Error(
45+
'Invalid PGPM config: requires modulePath or both workspacePath and moduleName',
46+
);
47+
}
48+
49+
export async function exportSchema(
50+
options: ExportSchemaOptions,
51+
): Promise<ExportSchemaResult> {
52+
const { pgpm, targets, keepDb = false, verbose = false } = options;
53+
54+
if (targets.length === 0) {
55+
return {
56+
success: false,
57+
message: 'No export targets specified.',
58+
};
59+
}
60+
61+
let modulePath: string;
62+
try {
63+
modulePath = resolveModulePath(pgpm);
64+
} catch (err) {
65+
return {
66+
success: false,
67+
message: `Failed to resolve module path: ${err instanceof Error ? err.message : 'Unknown error'}`,
68+
};
69+
}
70+
71+
const pkg = new PgpmPackage(modulePath);
72+
if (!pkg.isInModule()) {
73+
return {
74+
success: false,
75+
message: `Not a valid PGPM module: ${modulePath}. Directory must contain pgpm.plan and .control files.`,
76+
};
77+
}
78+
79+
let ephemeralDb: EphemeralDbResult;
80+
try {
81+
ephemeralDb = createEphemeralDb({
82+
prefix: 'codegen_export_',
83+
verbose,
84+
});
85+
} catch (err) {
86+
return {
87+
success: false,
88+
message: `Failed to create ephemeral database: ${err instanceof Error ? err.message : 'Unknown error'}`,
89+
};
90+
}
91+
92+
const { config: dbConfig, teardown } = ephemeralDb;
93+
94+
try {
95+
if (verbose) {
96+
console.log(`Deploying PGPM module: ${modulePath}`);
97+
}
98+
await deployPgpm(dbConfig, modulePath, false);
99+
100+
const { getPgPool } = await import('pg-cache');
101+
const pool = getPgPool(dbConfig);
102+
103+
const exportedFiles: Array<{
104+
target: string;
105+
path: string;
106+
schemas: string[];
107+
}> = [];
108+
const errors: string[] = [];
109+
110+
for (const target of targets) {
111+
const targetName =
112+
target.apiNames?.join(',') || target.schemas?.join(',') || 'default';
113+
114+
try {
115+
let schemas: string[];
116+
if (target.apiNames && target.apiNames.length > 0) {
117+
const validation = await validateServicesSchemas(pool);
118+
if (!validation.valid) {
119+
errors.push(`[${targetName}] ${validation.error}`);
120+
continue;
121+
}
122+
schemas = await resolveApiSchemas(pool, target.apiNames);
123+
} else if (target.schemas && target.schemas.length > 0) {
124+
schemas = target.schemas;
125+
} else {
126+
schemas = ['public'];
127+
}
128+
129+
if (verbose) {
130+
console.log(
131+
`[${targetName}] Introspecting schemas: ${schemas.join(', ')}`,
132+
);
133+
}
134+
135+
const sdl = await buildSchemaSDLFromDatabase({
136+
database: dbConfig.database,
137+
schemas,
138+
});
139+
140+
if (!sdl.trim()) {
141+
errors.push(`[${targetName}] Introspection returned empty schema`);
142+
continue;
143+
}
144+
145+
const outDir = path.resolve(target.output);
146+
await fs.promises.mkdir(outDir, { recursive: true });
147+
148+
const filename = target.filename ?? 'schema.graphql';
149+
const filePath = path.join(outDir, filename);
150+
await fs.promises.writeFile(filePath, sdl, 'utf-8');
151+
152+
exportedFiles.push({
153+
target: targetName,
154+
path: filePath,
155+
schemas,
156+
});
157+
158+
if (verbose) {
159+
console.log(`[${targetName}] Wrote ${filePath}`);
160+
}
161+
} catch (err) {
162+
errors.push(
163+
`[${targetName}] ${err instanceof Error ? err.message : 'Unknown error'}`,
164+
);
165+
}
166+
}
167+
168+
if (exportedFiles.length === 0) {
169+
return {
170+
success: false,
171+
message: 'No schema files exported.',
172+
errors,
173+
};
174+
}
175+
176+
return {
177+
success: true,
178+
message: `Exported ${exportedFiles.length} schema file(s) from PGPM module.`,
179+
files: exportedFiles,
180+
errors: errors.length > 0 ? errors : undefined,
181+
};
182+
} catch (err) {
183+
return {
184+
success: false,
185+
message: `Failed to deploy PGPM module: ${err instanceof Error ? err.message : 'Unknown error'}`,
186+
};
187+
} finally {
188+
teardown({ keepDb });
189+
if (keepDb) {
190+
console.log(`Kept ephemeral database: ${dbConfig.database}`);
191+
}
192+
}
193+
}
194+
195+
export interface ExportSchemaSimpleOptions {
196+
pgpm: PgpmConfig;
197+
apiNames?: string[];
198+
schemas?: string[];
199+
output: string;
200+
filename?: string;
201+
keepDb?: boolean;
202+
verbose?: boolean;
203+
}
204+
205+
export async function exportSchemaSimple(
206+
options: ExportSchemaSimpleOptions,
207+
): Promise<ExportSchemaResult> {
208+
const { pgpm, apiNames, schemas, output, filename, keepDb, verbose } =
209+
options;
210+
211+
return exportSchema({
212+
pgpm,
213+
targets: [{ apiNames, schemas, output, filename }],
214+
keepDb,
215+
verbose,
216+
});
217+
}

0 commit comments

Comments
 (0)