Skip to content

Commit 2b57f99

Browse files
authored
Merge pull request #836 from constructive-io/devin/1773717246-export-use-migrate-client
refactor(export): use @pgpmjs/migrate-client ORM for sql_actions fetching
2 parents 50a5043 + c199353 commit 2b57f99

28 files changed

Lines changed: 6653 additions & 8736 deletions

pgpm/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"dependencies": {
4848
"@inquirerer/utils": "^3.3.4",
4949
"@pgpmjs/core": "workspace:^",
50+
"@pgpmjs/export": "workspace:^",
5051
"@pgpmjs/env": "workspace:^",
5152
"@pgpmjs/logger": "workspace:^",
5253
"@pgpmjs/types": "workspace:^",

pgpm/cli/src/commands/export.ts

Lines changed: 266 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { exportMigrations,PgpmPackage } from '@pgpmjs/core';
1+
import { PgpmPackage } from '@pgpmjs/core';
2+
import { exportMigrations, exportGraphQL, GraphQLClient, graphqlRowToPostgresRow } from '@pgpmjs/export';
23
import { getEnvOptions } from '@pgpmjs/env';
34
import { getGitConfigInfo } from '@pgpmjs/types';
45
import { CLIOptions, Inquirerer } from 'inquirerer';
@@ -14,13 +15,19 @@ Export Command:
1415
1516
Options:
1617
--help, -h Show this help message
18+
--graphql-endpoint <url> GraphQL endpoint for meta/services data (enables GraphQL mode)
19+
--migrate-endpoint <url> GraphQL endpoint for db_migrate data (optional, for sql_actions)
20+
--migrate-host <host> Host header for migrate endpoint (e.g. db_migrate.localhost:3000)
21+
--migrate-headers <json> Extra headers for migrate endpoint as JSON (e.g. '{"X-Schemata":"db_migrate"}')
22+
--token <token> Bearer token for GraphQL authentication
1723
--author <name> Project author (default: from git config)
1824
--extensionName <name> Extension name
1925
--metaExtensionName <name> Meta extension name (default: svc)
2026
--cwd <directory> Working directory (default: current directory)
2127
2228
Examples:
23-
pgpm export Export migrations from selected database
29+
pgpm export Export migrations from selected database (SQL mode)
30+
pgpm export --graphql-endpoint 'http://[::1]:3002/graphql' --migrate-endpoint 'http://[::1]:3000/graphql' --migrate-host db_migrate.localhost:3000
2431
`;
2532

2633
export default async (
@@ -40,106 +47,267 @@ export default async (
4047
project.ensureWorkspace();
4148
project.resetCwd(project.workspacePath);
4249

43-
const options = getEnvOptions();
44-
45-
const db = await getPgPool({
46-
database: 'postgres'
47-
});
48-
49-
const databasesResult = await db.query(`
50-
SELECT datname FROM pg_catalog.pg_database
51-
WHERE datistemplate = FALSE AND datname NOT IN ('postgres')
52-
AND datname !~ '^pg_';
53-
`);
54-
55-
const { databases: dbname } = await prompter.prompt(argv, [
56-
{
57-
type: 'list',
58-
name: 'databases',
59-
message: 'Select a database',
60-
options: databasesResult.rows.map(row => row.datname),
61-
required: true
50+
const graphqlEndpoint = argv['graphql-endpoint'] || argv.graphqlEndpoint;
51+
const migrateEndpoint = argv['migrate-endpoint'] || argv.migrateEndpoint;
52+
const migrateHost = argv['migrate-host'] || argv.migrateHost;
53+
const migrateHeadersRaw = argv['migrate-headers'] || argv.migrateHeaders;
54+
const token = argv.token;
55+
56+
if (graphqlEndpoint) {
57+
// =========================================================================
58+
// GraphQL export mode
59+
// =========================================================================
60+
console.log(`GraphQL export mode: ${graphqlEndpoint}`);
61+
62+
const metaClient = new GraphQLClient({
63+
endpoint: graphqlEndpoint,
64+
token,
65+
headers: { 'X-Meta-Schema': 'true' }
66+
});
67+
68+
// Fetch databases via GraphQL
69+
const dbRows = await metaClient.fetchAllNodes<{ id: string; name: string }>(
70+
'databases',
71+
'id\nname'
72+
);
73+
74+
if (!dbRows.length) {
75+
console.log('No databases found via GraphQL.');
76+
prompter.close();
77+
return;
6278
}
63-
]);
64-
const selectedDb = await getPgPool({
65-
database: dbname
66-
});
67-
68-
const dbsResult = await selectedDb.query(`
69-
SELECT id, name FROM metaschema_public.database;
70-
`);
71-
72-
const { database_ids: selectedDatabaseName } = await prompter.prompt({} as any, [
73-
{
74-
type: 'list',
75-
name: 'database_ids',
76-
message: 'Select database_id',
77-
options: dbsResult.rows.map(db => db.name),
78-
required: true
79+
80+
const { database_ids: selectedDatabaseName } = await prompter.prompt(argv, [
81+
{
82+
type: 'list',
83+
name: 'database_ids',
84+
message: 'Select database',
85+
options: dbRows.map(db => db.name),
86+
required: true
87+
}
88+
]);
89+
90+
const selectedDatabase = dbRows.find(db => db.name === selectedDatabaseName);
91+
if (!selectedDatabase) {
92+
console.log('Database not found.');
93+
prompter.close();
94+
return;
7995
}
80-
]);
81-
82-
const selectedDatabase = dbsResult.rows.find(db => db.name === selectedDatabaseName);
83-
84-
const dbInfo = {
85-
dbname,
86-
databaseName: selectedDatabaseName,
87-
database_ids: [selectedDatabase!.id]
88-
};
89-
90-
const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
91-
{
92-
type: 'text',
93-
name: 'author',
94-
message: 'Project author',
95-
default: `${username} <${email}>`,
96-
required: true
97-
},
98-
{
99-
type: 'text',
100-
name: 'extensionName',
101-
message: 'Extension name',
102-
default: selectedDatabaseName || dbname,
103-
required: true
104-
},
105-
{
106-
type: 'text',
107-
name: 'metaExtensionName',
108-
message: 'Meta extension name',
109-
default: `${selectedDatabaseName || dbname}-service`,
110-
required: true
96+
97+
const databaseId = selectedDatabase.id;
98+
99+
const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
100+
{
101+
type: 'text',
102+
name: 'author',
103+
message: 'Project author',
104+
default: `${username} <${email}>`,
105+
required: true
106+
},
107+
{
108+
type: 'text',
109+
name: 'extensionName',
110+
message: 'Extension name',
111+
default: selectedDatabaseName,
112+
required: true
113+
},
114+
{
115+
type: 'text',
116+
name: 'metaExtensionName',
117+
message: 'Meta extension name',
118+
default: `${selectedDatabaseName}-service`,
119+
required: true
120+
}
121+
]);
122+
123+
// Fetch schemas via GraphQL
124+
const schemaRows = await metaClient.fetchAllNodes<{ id: string; schemaName: string; name: string }>(
125+
'schemas',
126+
'id\nschemaName\nname',
127+
{ databaseId }
128+
);
129+
130+
// Convert camelCase to snake_case for schema rows
131+
const pgSchemaRows = schemaRows.map(s => graphqlRowToPostgresRow(s)) as Array<{ id: string; schema_name: string; name: string }>;
132+
133+
// Normalize comma-separated schema_names string into an array for checkbox override
134+
if (typeof argv.schema_names === 'string') {
135+
argv.schema_names = argv.schema_names.split(',').map((s: string) => s.trim()).filter(Boolean);
111136
}
112-
]);
113-
114-
const schemasResult = await selectedDb.query(
115-
`SELECT * FROM metaschema_public.schema WHERE database_id = $1`,
116-
[dbInfo.database_ids[0]]
117-
);
118-
119-
const { schema_names } = await prompter.prompt({} as any, [
120-
{
121-
type: 'checkbox',
122-
name: 'schema_names',
123-
message: 'Select schema_name(s)',
124-
options: schemasResult.rows.map(s => s.schema_name),
125-
default: schemasResult.rows.map(s => s.schema_name),
126-
required: true
137+
138+
const { schema_names } = await prompter.prompt(argv, [
139+
{
140+
type: 'checkbox',
141+
name: 'schema_names',
142+
message: 'Select schema_name(s)',
143+
options: pgSchemaRows.map(s => s.schema_name),
144+
default: pgSchemaRows.map(s => s.schema_name),
145+
required: true
146+
}
147+
]);
148+
149+
const outdir = resolve(project.workspacePath, 'packages/');
150+
151+
await exportGraphQL({
152+
project,
153+
metaEndpoint: graphqlEndpoint,
154+
migrateEndpoint,
155+
migrateHeaders: {
156+
...(migrateHost ? { Host: migrateHost } : {}),
157+
...(migrateHeadersRaw ? JSON.parse(migrateHeadersRaw) : {})
158+
},
159+
token,
160+
headers: { 'X-Meta-Schema': 'true' },
161+
databaseId,
162+
databaseName: selectedDatabaseName,
163+
schema_names,
164+
schemas: pgSchemaRows,
165+
author,
166+
outdir,
167+
extensionName,
168+
metaExtensionName,
169+
prompter,
170+
argv,
171+
username
172+
});
173+
} else {
174+
// =========================================================================
175+
// SQL export mode (original behavior)
176+
// =========================================================================
177+
const options = getEnvOptions();
178+
179+
const db = await getPgPool({
180+
database: 'postgres'
181+
});
182+
183+
const databasesResult = await db.query(`
184+
SELECT datname FROM pg_catalog.pg_database
185+
WHERE datistemplate = FALSE AND datname NOT IN ('postgres')
186+
AND datname !~ '^pg_';
187+
`);
188+
189+
// If --database_ids is provided but --databases is not, auto-detect
190+
// which postgres database contains the matching metaschema database name.
191+
if (argv.database_ids && !argv.databases) {
192+
const targetName = argv.database_ids;
193+
for (const row of databasesResult.rows) {
194+
try {
195+
const candidatePool = await getPgPool({ database: row.datname });
196+
const check = await candidatePool.query(
197+
`SELECT name FROM metaschema_public.database WHERE name = $1 LIMIT 1`,
198+
[targetName]
199+
);
200+
if (check.rows.length > 0) {
201+
argv.databases = row.datname;
202+
break;
203+
}
204+
} catch {
205+
// Skip databases that don't have metaschema_public
206+
}
207+
}
208+
if (!argv.databases) {
209+
console.error(`Could not find database "${targetName}" in any postgres database.`);
210+
prompter.close();
211+
return;
212+
}
213+
}
214+
215+
const { databases: dbname } = await prompter.prompt(argv, [
216+
{
217+
type: 'list',
218+
name: 'databases',
219+
message: 'Select a database',
220+
options: databasesResult.rows.map(row => row.datname),
221+
required: true
222+
}
223+
]);
224+
const selectedDb = await getPgPool({
225+
database: dbname
226+
});
227+
228+
const dbsResult = await selectedDb.query(`
229+
SELECT id, name FROM metaschema_public.database;
230+
`);
231+
232+
const { database_ids: selectedDatabaseName } = await prompter.prompt(argv, [
233+
{
234+
type: 'list',
235+
name: 'database_ids',
236+
message: 'Select database_id',
237+
options: dbsResult.rows.map(db => db.name),
238+
required: true
239+
}
240+
]);
241+
242+
const selectedDatabase = dbsResult.rows.find(db => db.name === selectedDatabaseName);
243+
244+
const dbInfo = {
245+
dbname,
246+
databaseName: selectedDatabaseName,
247+
database_ids: [selectedDatabase!.id]
248+
};
249+
250+
const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
251+
{
252+
type: 'text',
253+
name: 'author',
254+
message: 'Project author',
255+
default: `${username} <${email}>`,
256+
required: true
257+
},
258+
{
259+
type: 'text',
260+
name: 'extensionName',
261+
message: 'Extension name',
262+
default: selectedDatabaseName || dbname,
263+
required: true
264+
},
265+
{
266+
type: 'text',
267+
name: 'metaExtensionName',
268+
message: 'Meta extension name',
269+
default: `${selectedDatabaseName || dbname}-service`,
270+
required: true
271+
}
272+
]);
273+
274+
const schemasResult = await selectedDb.query(
275+
`SELECT * FROM metaschema_public.schema WHERE database_id = $1`,
276+
[dbInfo.database_ids[0]]
277+
);
278+
279+
// Normalize comma-separated schema_names string into an array for checkbox override
280+
if (typeof argv.schema_names === 'string') {
281+
argv.schema_names = argv.schema_names.split(',').map((s: string) => s.trim()).filter(Boolean);
127282
}
128-
]);
129-
130-
const outdir = resolve(project.workspacePath, 'packages/');
131-
132-
await exportMigrations({
133-
project,
134-
options,
135-
dbInfo,
136-
author,
137-
schema_names,
138-
outdir,
139-
extensionName,
140-
metaExtensionName,
141-
prompter
142-
});
283+
284+
const { schema_names } = await prompter.prompt(argv, [
285+
{
286+
type: 'checkbox',
287+
name: 'schema_names',
288+
message: 'Select schema_name(s)',
289+
options: schemasResult.rows.map(s => s.schema_name),
290+
default: schemasResult.rows.map(s => s.schema_name),
291+
required: true
292+
}
293+
]);
294+
295+
const outdir = resolve(project.workspacePath, 'packages/');
296+
297+
await exportMigrations({
298+
project,
299+
options,
300+
dbInfo,
301+
author,
302+
schema_names,
303+
outdir,
304+
extensionName,
305+
metaExtensionName,
306+
prompter,
307+
argv,
308+
username
309+
});
310+
}
143311

144312
prompter.close();
145313

0 commit comments

Comments
 (0)