Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3074325
Fixed few bugs for pgpm and make both export flow work
NorOldBurden Mar 2, 2026
ad4bf65
Merge branch 'main' into feature/pgpm-export-graphql
NorOldBurden Mar 3, 2026
b1c70af
Fix minor bugs and cli issues
NorOldBurden Mar 4, 2026
15e25d5
Fix flow output diff and added inflekt to deal with cases
NorOldBurden Mar 5, 2026
a824202
Merge main
NorOldBurden Mar 5, 2026
ba9e8b9
Replace hardcode with inflekt
NorOldBurden Mar 5, 2026
9e3f0e2
Merge branch 'main' into feature/pgpm-export-graphql
NorOldBurden Mar 6, 2026
c53e846
Update lock
NorOldBurden Mar 6, 2026
d2b65b0
Fix issue for both export flows
NorOldBurden Mar 16, 2026
0822c26
Merge branch 'main' into feature/pgpm-export-graphql
NorOldBurden Mar 16, 2026
632e1c8
Extract common part for both export flow
NorOldBurden Mar 16, 2026
addcb92
Update export tests
NorOldBurden Mar 16, 2026
ac7215d
Merge main into export branch to get @pgpmjs/migrate-client
pyramation Mar 17, 2026
6fd8458
refactor(export): use @pgpmjs/migrate-client ORM for sql_actions fetc…
pyramation Mar 17, 2026
26f9fb2
chore: update pnpm-lock.yaml after merge
pyramation Mar 17, 2026
b00160f
test: update export-parity test to mock @pgpmjs/migrate-client ORM
pyramation Mar 17, 2026
5c878ae
fix(test): resolve migrate-client mock by absolute path for CI compat…
pyramation Mar 17, 2026
681d3b9
fix(export): use extensionName for schema prefix in meta replacer (pa…
pyramation Mar 17, 2026
7fdce33
Update test to use pgsql-test
NorOldBurden Mar 18, 2026
62a26c6
Use moduleNameMapper to avoid circular deps
NorOldBurden Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
498 changes: 498 additions & 0 deletions pgpm/cli/__tests__/export-parity.test.ts

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pgpm/cli/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ module.exports = {
'ts-jest',
{
babelConfig: false,
tsconfig: 'tsconfig.json',
tsconfig: 'tsconfig.test.json',
},
],
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^pgsql-test$': '<rootDir>/../../postgres/pgsql-test/src',
},
modulePathIgnorePatterns: ['dist/*'],
};
332 changes: 234 additions & 98 deletions pgpm/cli/src/commands/export.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exportMigrations,PgpmPackage } from '@pgpmjs/core';
import { exportMigrations, exportGraphQL, GraphQLClient, PgpmPackage, graphqlRowToPostgresRow } from '@pgpmjs/core';
import { getEnvOptions } from '@pgpmjs/env';
import { getGitConfigInfo } from '@pgpmjs/types';
import { CLIOptions, Inquirerer } from 'inquirerer';
Expand All @@ -14,13 +14,18 @@ Export Command:

Options:
--help, -h Show this help message
--graphql-endpoint <url> GraphQL endpoint for meta/services data (enables GraphQL mode)
--migrate-endpoint <url> GraphQL endpoint for db_migrate data (optional, for sql_actions)
--migrate-host <host> Host header for migrate endpoint (e.g. db_migrate.localhost:3000)
--token <token> Bearer token for GraphQL authentication
--author <name> Project author (default: from git config)
--extensionName <name> Extension name
--metaExtensionName <name> Meta extension name (default: svc)
--cwd <directory> Working directory (default: current directory)

Examples:
pgpm export Export migrations from selected database
pgpm export Export migrations from selected database (SQL mode)
pgpm export --graphql-endpoint 'http://[::1]:3002/graphql' --migrate-endpoint 'http://[::1]:3000/graphql' --migrate-host db_migrate.localhost:3000
`;

export default async (
Expand All @@ -40,106 +45,237 @@ export default async (
project.ensureWorkspace();
project.resetCwd(project.workspacePath);

const options = getEnvOptions();

const db = await getPgPool({
database: 'postgres'
});

const databasesResult = await db.query(`
SELECT datname FROM pg_catalog.pg_database
WHERE datistemplate = FALSE AND datname NOT IN ('postgres')
AND datname !~ '^pg_';
`);

const { databases: dbname } = await prompter.prompt(argv, [
{
type: 'list',
name: 'databases',
message: 'Select a database',
options: databasesResult.rows.map(row => row.datname),
required: true
const graphqlEndpoint = argv['graphql-endpoint'] || argv.graphqlEndpoint;
const migrateEndpoint = argv['migrate-endpoint'] || argv.migrateEndpoint;
const migrateHost = argv['migrate-host'] || argv.migrateHost;
const token = argv.token;

if (graphqlEndpoint) {
// =========================================================================
// GraphQL export mode
// =========================================================================
console.log(`GraphQL export mode: ${graphqlEndpoint}`);

const metaClient = new GraphQLClient({
endpoint: graphqlEndpoint,
token,
headers: { 'X-Meta-Schema': 'true' }
});

// Fetch databases via GraphQL
const dbRows = await metaClient.fetchAllNodes<{ id: string; name: string }>(
'databases',
'id\nname'
);

if (!dbRows.length) {
console.log('No databases found via GraphQL.');
prompter.close();
return;
}
]);
const selectedDb = await getPgPool({
database: dbname
});

const dbsResult = await selectedDb.query(`
SELECT id, name FROM metaschema_public.database;
`);

const { database_ids: selectedDatabaseName } = await prompter.prompt({} as any, [
{
type: 'list',
name: 'database_ids',
message: 'Select database_id',
options: dbsResult.rows.map(db => db.name),
required: true

const { database_ids: selectedDatabaseName } = await prompter.prompt(argv, [
{
type: 'list',
name: 'database_ids',
message: 'Select database',
options: dbRows.map(db => db.name),
required: true
}
]);

const selectedDatabase = dbRows.find(db => db.name === selectedDatabaseName);
if (!selectedDatabase) {
console.log('Database not found.');
prompter.close();
return;
}
]);

const selectedDatabase = dbsResult.rows.find(db => db.name === selectedDatabaseName);

const dbInfo = {
dbname,
databaseName: selectedDatabaseName,
database_ids: [selectedDatabase!.id]
};

const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
{
type: 'text',
name: 'author',
message: 'Project author',
default: `${username} <${email}>`,
required: true
},
{
type: 'text',
name: 'extensionName',
message: 'Extension name',
default: selectedDatabaseName || dbname,
required: true
},
{
type: 'text',
name: 'metaExtensionName',
message: 'Meta extension name',
default: `${selectedDatabaseName || dbname}-service`,
required: true

const databaseId = selectedDatabase.id;

const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
{
type: 'text',
name: 'author',
message: 'Project author',
default: `${username} <${email}>`,
required: true
},
{
type: 'text',
name: 'extensionName',
message: 'Extension name',
default: selectedDatabaseName,
required: true
},
{
type: 'text',
name: 'metaExtensionName',
message: 'Meta extension name',
default: `${selectedDatabaseName}-service`,
required: true
}
]);

// Fetch schemas via GraphQL
const schemaRows = await metaClient.fetchAllNodes<{ id: string; schemaName: string; name: string }>(
'schemas',
'id\nschemaName\nname',
{ databaseId }
);

// Convert camelCase to snake_case for schema rows
const pgSchemaRows = schemaRows.map(s => graphqlRowToPostgresRow(s)) as Array<{ id: string; schema_name: string; name: string }>;

// Normalize comma-separated schema_names string into an array for checkbox override
if (typeof argv.schema_names === 'string') {
argv.schema_names = argv.schema_names.split(',').map((s: string) => s.trim()).filter(Boolean);
}
]);

const schemasResult = await selectedDb.query(
`SELECT * FROM metaschema_public.schema WHERE database_id = $1`,
[dbInfo.database_ids[0]]
);

const { schema_names } = await prompter.prompt({} as any, [
{
type: 'checkbox',
name: 'schema_names',
message: 'Select schema_name(s)',
options: schemasResult.rows.map(s => s.schema_name),
default: schemasResult.rows.map(s => s.schema_name),
required: true

const { schema_names } = await prompter.prompt(argv, [
{
type: 'checkbox',
name: 'schema_names',
message: 'Select schema_name(s)',
options: pgSchemaRows.map(s => s.schema_name),
default: pgSchemaRows.map(s => s.schema_name),
required: true
}
]);

const outdir = resolve(project.workspacePath, 'packages/');

await exportGraphQL({
project,
metaEndpoint: graphqlEndpoint,
migrateEndpoint,
migrateHeaders: migrateHost ? { Host: migrateHost } : undefined,
token,
headers: { 'X-Meta-Schema': 'true' },
databaseId,
databaseName: selectedDatabaseName,
schema_names,
schemas: pgSchemaRows,
author,
outdir,
extensionName,
metaExtensionName,
prompter,
argv,
username
});
} else {
// =========================================================================
// SQL export mode (original behavior)
// =========================================================================
const options = getEnvOptions();

const db = await getPgPool({
database: 'postgres'
});

const databasesResult = await db.query(`
SELECT datname FROM pg_catalog.pg_database
WHERE datistemplate = FALSE AND datname NOT IN ('postgres')
AND datname !~ '^pg_';
`);

const { databases: dbname } = await prompter.prompt(argv, [
{
type: 'list',
name: 'databases',
message: 'Select a database',
options: databasesResult.rows.map(row => row.datname),
required: true
}
]);
const selectedDb = await getPgPool({
database: dbname
});

const dbsResult = await selectedDb.query(`
SELECT id, name FROM metaschema_public.database;
`);

const { database_ids: selectedDatabaseName } = await prompter.prompt(argv, [
{
type: 'list',
name: 'database_ids',
message: 'Select database_id',
options: dbsResult.rows.map(db => db.name),
required: true
}
]);

const selectedDatabase = dbsResult.rows.find(db => db.name === selectedDatabaseName);

const dbInfo = {
dbname,
databaseName: selectedDatabaseName,
database_ids: [selectedDatabase!.id]
};

const { author, extensionName, metaExtensionName } = await prompter.prompt(argv, [
{
type: 'text',
name: 'author',
message: 'Project author',
default: `${username} <${email}>`,
required: true
},
{
type: 'text',
name: 'extensionName',
message: 'Extension name',
default: selectedDatabaseName || dbname,
required: true
},
{
type: 'text',
name: 'metaExtensionName',
message: 'Meta extension name',
default: `${selectedDatabaseName || dbname}-service`,
required: true
}
]);

const schemasResult = await selectedDb.query(
`SELECT * FROM metaschema_public.schema WHERE database_id = $1`,
[dbInfo.database_ids[0]]
);

// Normalize comma-separated schema_names string into an array for checkbox override
if (typeof argv.schema_names === 'string') {
argv.schema_names = argv.schema_names.split(',').map((s: string) => s.trim()).filter(Boolean);
}
]);

const outdir = resolve(project.workspacePath, 'packages/');

await exportMigrations({
project,
options,
dbInfo,
author,
schema_names,
outdir,
extensionName,
metaExtensionName,
prompter
});

const { schema_names } = await prompter.prompt(argv, [
{
type: 'checkbox',
name: 'schema_names',
message: 'Select schema_name(s)',
options: schemasResult.rows.map(s => s.schema_name),
default: schemasResult.rows.map(s => s.schema_name),
required: true
}
]);

const outdir = resolve(project.workspacePath, 'packages/');

await exportMigrations({
project,
options,
dbInfo,
author,
schema_names,
outdir,
extensionName,
metaExtensionName,
prompter,
argv,
username
});
}

prompter.close();

Expand Down
9 changes: 9 additions & 0 deletions pgpm/cli/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"pgsql-test": ["../../postgres/pgsql-test/src"]
}
},
"include": ["src", "__tests__"]
}
Loading