@pgpmjs/export 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +86 -0
- package/esm/export-graphql-meta.js +151 -0
- package/esm/export-graphql.js +176 -0
- package/esm/export-meta.js +162 -0
- package/esm/export-migrations.js +171 -0
- package/esm/export-utils.js +1159 -0
- package/esm/graphql-client.js +139 -0
- package/esm/graphql-naming.js +76 -0
- package/esm/index.js +7 -0
- package/export-graphql-meta.d.ts +13 -0
- package/export-graphql-meta.js +155 -0
- package/export-graphql.d.ts +52 -0
- package/export-graphql.js +180 -0
- package/export-meta.d.ts +9 -0
- package/export-meta.js +166 -0
- package/export-migrations.d.ts +35 -0
- package/export-migrations.js +175 -0
- package/export-utils.d.ts +127 -0
- package/export-utils.js +1170 -0
- package/graphql-client.d.ts +25 -0
- package/graphql-client.js +143 -0
- package/graphql-naming.d.ts +24 -0
- package/graphql-naming.js +83 -0
- package/index.d.ts +8 -0
- package/index.js +40 -0
- package/package.json +60 -0
package/export-meta.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportMeta = void 0;
|
|
4
|
+
const csv_to_pg_1 = require("csv-to-pg");
|
|
5
|
+
const pg_cache_1 = require("pg-cache");
|
|
6
|
+
const export_utils_1 = require("./export-utils");
|
|
7
|
+
/**
|
|
8
|
+
* Query actual columns from information_schema for a given table.
|
|
9
|
+
* Returns a map of column_name -> udt_name (PostgreSQL type).
|
|
10
|
+
*/
|
|
11
|
+
const getTableColumns = async (pool, schemaName, tableName) => {
|
|
12
|
+
const result = await pool.query(`
|
|
13
|
+
SELECT column_name, udt_name
|
|
14
|
+
FROM information_schema.columns
|
|
15
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
16
|
+
ORDER BY ordinal_position
|
|
17
|
+
`, [schemaName, tableName]);
|
|
18
|
+
const columns = new Map();
|
|
19
|
+
for (const row of result.rows) {
|
|
20
|
+
columns.set(row.column_name, row.udt_name);
|
|
21
|
+
}
|
|
22
|
+
return columns;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Build dynamic fields config by intersecting the hardcoded config with actual database columns.
|
|
26
|
+
* - Only includes columns that exist in the database
|
|
27
|
+
* - Preserves special type hints from config (image, upload, url) for columns that exist
|
|
28
|
+
* - Infers types from PostgreSQL for columns not in config
|
|
29
|
+
*/
|
|
30
|
+
const buildDynamicFields = async (pool, tableConfig) => {
|
|
31
|
+
const actualColumns = await getTableColumns(pool, tableConfig.schema, tableConfig.table);
|
|
32
|
+
if (actualColumns.size === 0) {
|
|
33
|
+
// Table doesn't exist, return empty fields
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
const dynamicFields = {};
|
|
37
|
+
// For each column in the hardcoded config, check if it exists in the database
|
|
38
|
+
for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
|
|
39
|
+
if (actualColumns.has(fieldName)) {
|
|
40
|
+
// Column exists - use the config's type hint (preserves special types like 'image', 'upload', 'url')
|
|
41
|
+
dynamicFields[fieldName] = fieldType;
|
|
42
|
+
}
|
|
43
|
+
// If column doesn't exist in database, skip it (this fixes the bug)
|
|
44
|
+
}
|
|
45
|
+
return dynamicFields;
|
|
46
|
+
};
|
|
47
|
+
const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
48
|
+
const pool = (0, pg_cache_1.getPgPool)({
|
|
49
|
+
...opts.pg,
|
|
50
|
+
database: dbname
|
|
51
|
+
});
|
|
52
|
+
const sql = {};
|
|
53
|
+
// Cache for dynamically built parsers
|
|
54
|
+
const parsers = {};
|
|
55
|
+
// Build parser dynamically by querying actual columns from the database
|
|
56
|
+
const getParser = async (key) => {
|
|
57
|
+
if (parsers[key]) {
|
|
58
|
+
return parsers[key];
|
|
59
|
+
}
|
|
60
|
+
const tableConfig = export_utils_1.META_TABLE_CONFIG[key];
|
|
61
|
+
if (!tableConfig) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Build fields dynamically based on actual database columns
|
|
65
|
+
const dynamicFields = await buildDynamicFields(pool, tableConfig);
|
|
66
|
+
if (Object.keys(dynamicFields).length === 0) {
|
|
67
|
+
// No columns found (table doesn't exist or no matching columns)
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const parser = new csv_to_pg_1.Parser({
|
|
71
|
+
schema: tableConfig.schema,
|
|
72
|
+
table: tableConfig.table,
|
|
73
|
+
conflictDoNothing: tableConfig.conflictDoNothing,
|
|
74
|
+
fields: dynamicFields
|
|
75
|
+
});
|
|
76
|
+
parsers[key] = parser;
|
|
77
|
+
return parser;
|
|
78
|
+
};
|
|
79
|
+
const queryAndParse = async (key, query) => {
|
|
80
|
+
try {
|
|
81
|
+
const parser = await getParser(key);
|
|
82
|
+
if (!parser) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const result = await pool.query(query, [database_id]);
|
|
86
|
+
if (result.rows.length) {
|
|
87
|
+
const parsed = await parser.parse(result.rows);
|
|
88
|
+
if (parsed) {
|
|
89
|
+
sql[key] = parsed;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const pgError = err;
|
|
95
|
+
if (pgError.code === '42P01') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// metaschema_public tables
|
|
103
|
+
// =============================================================================
|
|
104
|
+
await queryAndParse('database', `SELECT * FROM metaschema_public.database WHERE id = $1 ORDER BY id`);
|
|
105
|
+
await queryAndParse('database_extension', `SELECT * FROM metaschema_public.database_extension WHERE database_id = $1 ORDER BY id`);
|
|
106
|
+
await queryAndParse('schema', `SELECT * FROM metaschema_public.schema WHERE database_id = $1 ORDER BY id`);
|
|
107
|
+
await queryAndParse('table', `SELECT * FROM metaschema_public.table WHERE database_id = $1 ORDER BY id`);
|
|
108
|
+
await queryAndParse('field', `SELECT * FROM metaschema_public.field WHERE database_id = $1 ORDER BY id`);
|
|
109
|
+
await queryAndParse('policy', `SELECT * FROM metaschema_public.policy WHERE database_id = $1 ORDER BY id`);
|
|
110
|
+
await queryAndParse('index', `SELECT * FROM metaschema_public.index WHERE database_id = $1 ORDER BY id`);
|
|
111
|
+
await queryAndParse('trigger', `SELECT * FROM metaschema_public.trigger WHERE database_id = $1 ORDER BY id`);
|
|
112
|
+
await queryAndParse('trigger_function', `SELECT * FROM metaschema_public.trigger_function WHERE database_id = $1 ORDER BY id`);
|
|
113
|
+
await queryAndParse('rls_function', `SELECT * FROM metaschema_public.rls_function WHERE database_id = $1 ORDER BY id`);
|
|
114
|
+
await queryAndParse('foreign_key_constraint', `SELECT * FROM metaschema_public.foreign_key_constraint WHERE database_id = $1 ORDER BY id`);
|
|
115
|
+
await queryAndParse('primary_key_constraint', `SELECT * FROM metaschema_public.primary_key_constraint WHERE database_id = $1 ORDER BY id`);
|
|
116
|
+
await queryAndParse('unique_constraint', `SELECT * FROM metaschema_public.unique_constraint WHERE database_id = $1 ORDER BY id`);
|
|
117
|
+
await queryAndParse('check_constraint', `SELECT * FROM metaschema_public.check_constraint WHERE database_id = $1 ORDER BY id`);
|
|
118
|
+
await queryAndParse('full_text_search', `SELECT * FROM metaschema_public.full_text_search WHERE database_id = $1 ORDER BY id`);
|
|
119
|
+
await queryAndParse('schema_grant', `SELECT * FROM metaschema_public.schema_grant WHERE database_id = $1 ORDER BY id`);
|
|
120
|
+
await queryAndParse('table_grant', `SELECT * FROM metaschema_public.table_grant WHERE database_id = $1 ORDER BY id`);
|
|
121
|
+
await queryAndParse('default_privilege', `SELECT * FROM metaschema_public.default_privilege WHERE database_id = $1 ORDER BY id`);
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// services_public tables
|
|
124
|
+
// =============================================================================
|
|
125
|
+
await queryAndParse('domains', `SELECT * FROM services_public.domains WHERE database_id = $1 ORDER BY id`);
|
|
126
|
+
await queryAndParse('sites', `SELECT * FROM services_public.sites WHERE database_id = $1 ORDER BY id`);
|
|
127
|
+
await queryAndParse('apis', `SELECT * FROM services_public.apis WHERE database_id = $1 ORDER BY id`);
|
|
128
|
+
await queryAndParse('apps', `SELECT * FROM services_public.apps WHERE database_id = $1 ORDER BY id`);
|
|
129
|
+
await queryAndParse('site_modules', `SELECT * FROM services_public.site_modules WHERE database_id = $1 ORDER BY id`);
|
|
130
|
+
await queryAndParse('site_themes', `SELECT * FROM services_public.site_themes WHERE database_id = $1 ORDER BY id`);
|
|
131
|
+
await queryAndParse('site_metadata', `SELECT * FROM services_public.site_metadata WHERE database_id = $1 ORDER BY id`);
|
|
132
|
+
await queryAndParse('api_modules', `SELECT * FROM services_public.api_modules WHERE database_id = $1 ORDER BY id`);
|
|
133
|
+
await queryAndParse('api_extensions', `SELECT * FROM services_public.api_extensions WHERE database_id = $1 ORDER BY id`);
|
|
134
|
+
await queryAndParse('api_schemas', `SELECT * FROM services_public.api_schemas WHERE database_id = $1 ORDER BY id`);
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// metaschema_modules_public tables
|
|
137
|
+
// =============================================================================
|
|
138
|
+
await queryAndParse('rls_module', `SELECT * FROM metaschema_modules_public.rls_module WHERE database_id = $1 ORDER BY id`);
|
|
139
|
+
await queryAndParse('user_auth_module', `SELECT * FROM metaschema_modules_public.user_auth_module WHERE database_id = $1 ORDER BY id`);
|
|
140
|
+
await queryAndParse('memberships_module', `SELECT * FROM metaschema_modules_public.memberships_module WHERE database_id = $1 ORDER BY id`);
|
|
141
|
+
await queryAndParse('permissions_module', `SELECT * FROM metaschema_modules_public.permissions_module WHERE database_id = $1 ORDER BY id`);
|
|
142
|
+
await queryAndParse('limits_module', `SELECT * FROM metaschema_modules_public.limits_module WHERE database_id = $1 ORDER BY id`);
|
|
143
|
+
await queryAndParse('levels_module', `SELECT * FROM metaschema_modules_public.levels_module WHERE database_id = $1 ORDER BY id`);
|
|
144
|
+
await queryAndParse('users_module', `SELECT * FROM metaschema_modules_public.users_module WHERE database_id = $1 ORDER BY id`);
|
|
145
|
+
await queryAndParse('hierarchy_module', `SELECT * FROM metaschema_modules_public.hierarchy_module WHERE database_id = $1 ORDER BY id`);
|
|
146
|
+
await queryAndParse('membership_types_module', `SELECT * FROM metaschema_modules_public.membership_types_module WHERE database_id = $1 ORDER BY id`);
|
|
147
|
+
await queryAndParse('invites_module', `SELECT * FROM metaschema_modules_public.invites_module WHERE database_id = $1 ORDER BY id`);
|
|
148
|
+
await queryAndParse('emails_module', `SELECT * FROM metaschema_modules_public.emails_module WHERE database_id = $1 ORDER BY id`);
|
|
149
|
+
await queryAndParse('sessions_module', `SELECT * FROM metaschema_modules_public.sessions_module WHERE database_id = $1 ORDER BY id`);
|
|
150
|
+
await queryAndParse('secrets_module', `SELECT * FROM metaschema_modules_public.secrets_module WHERE database_id = $1 ORDER BY id`);
|
|
151
|
+
await queryAndParse('profiles_module', `SELECT * FROM metaschema_modules_public.profiles_module WHERE database_id = $1 ORDER BY id`);
|
|
152
|
+
await queryAndParse('encrypted_secrets_module', `SELECT * FROM metaschema_modules_public.encrypted_secrets_module WHERE database_id = $1 ORDER BY id`);
|
|
153
|
+
await queryAndParse('connected_accounts_module', `SELECT * FROM metaschema_modules_public.connected_accounts_module WHERE database_id = $1 ORDER BY id`);
|
|
154
|
+
await queryAndParse('phone_numbers_module', `SELECT * FROM metaschema_modules_public.phone_numbers_module WHERE database_id = $1 ORDER BY id`);
|
|
155
|
+
await queryAndParse('crypto_addresses_module', `SELECT * FROM metaschema_modules_public.crypto_addresses_module WHERE database_id = $1 ORDER BY id`);
|
|
156
|
+
await queryAndParse('crypto_auth_module', `SELECT * FROM metaschema_modules_public.crypto_auth_module WHERE database_id = $1 ORDER BY id`);
|
|
157
|
+
await queryAndParse('field_module', `SELECT * FROM metaschema_modules_public.field_module WHERE database_id = $1 ORDER BY id`);
|
|
158
|
+
await queryAndParse('table_module', `SELECT * FROM metaschema_modules_public.table_module WHERE database_id = $1 ORDER BY id`);
|
|
159
|
+
await queryAndParse('table_template_module', `SELECT * FROM metaschema_modules_public.table_template_module WHERE database_id = $1 ORDER BY id`);
|
|
160
|
+
await queryAndParse('secure_table_provision', `SELECT * FROM metaschema_modules_public.secure_table_provision WHERE database_id = $1 ORDER BY id`);
|
|
161
|
+
await queryAndParse('uuid_module', `SELECT * FROM metaschema_modules_public.uuid_module WHERE database_id = $1 ORDER BY id`);
|
|
162
|
+
await queryAndParse('default_ids_module', `SELECT * FROM metaschema_modules_public.default_ids_module WHERE database_id = $1 ORDER BY id`);
|
|
163
|
+
await queryAndParse('denormalized_table_field', `SELECT * FROM metaschema_modules_public.denormalized_table_field WHERE database_id = $1 ORDER BY id`);
|
|
164
|
+
return sql;
|
|
165
|
+
};
|
|
166
|
+
exports.exportMeta = exportMeta;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
|
+
import { Inquirerer } from 'inquirerer';
|
|
3
|
+
import { PgpmPackage } from '@pgpmjs/core';
|
|
4
|
+
interface ExportOptions {
|
|
5
|
+
project: PgpmPackage;
|
|
6
|
+
options: PgpmOptions;
|
|
7
|
+
dbInfo: {
|
|
8
|
+
dbname: string;
|
|
9
|
+
databaseName: string;
|
|
10
|
+
database_ids: string[];
|
|
11
|
+
};
|
|
12
|
+
author: string;
|
|
13
|
+
outdir: string;
|
|
14
|
+
schema_names: string[];
|
|
15
|
+
extensionName?: string;
|
|
16
|
+
extensionDesc?: string;
|
|
17
|
+
metaExtensionName: string;
|
|
18
|
+
metaExtensionDesc?: string;
|
|
19
|
+
prompter?: Inquirerer;
|
|
20
|
+
argv?: Record<string, any>;
|
|
21
|
+
/** Repository name for module scaffolding. Defaults to module name if not provided. */
|
|
22
|
+
repoName?: string;
|
|
23
|
+
/** GitHub username/org for module scaffolding. Required for non-interactive use. */
|
|
24
|
+
username?: string;
|
|
25
|
+
/** Output directory for service/meta module. Defaults to outdir if not provided. */
|
|
26
|
+
serviceOutdir?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Skip schema name replacement for infrastructure schemas.
|
|
29
|
+
* When true, schema names like metaschema_public, services_public will not be renamed.
|
|
30
|
+
* Useful for self-referential introspection where you want to apply policies to real schemas.
|
|
31
|
+
*/
|
|
32
|
+
skipSchemaRenaming?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const exportMigrations: ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, argv, repoName, username, serviceOutdir, skipSchemaRenaming }: ExportOptions) => Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportMigrations = void 0;
|
|
4
|
+
const pg_cache_1 = require("pg-cache");
|
|
5
|
+
const core_1 = require("@pgpmjs/core");
|
|
6
|
+
const export_meta_1 = require("./export-meta");
|
|
7
|
+
const export_utils_1 = require("./export-utils");
|
|
8
|
+
const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, argv, repoName, username, serviceOutdir, skipSchemaRenaming = false }) => {
|
|
9
|
+
const normalizedOutdir = (0, export_utils_1.normalizeOutdir)(outdir);
|
|
10
|
+
// Use serviceOutdir for service module, defaulting to outdir if not provided
|
|
11
|
+
const svcOutdir = (0, export_utils_1.normalizeOutdir)(serviceOutdir || outdir);
|
|
12
|
+
const pgPool = (0, pg_cache_1.getPgPool)({
|
|
13
|
+
...options.pg,
|
|
14
|
+
database
|
|
15
|
+
});
|
|
16
|
+
const db = await pgPool.query(`select * from metaschema_public.database where id=$1`, [databaseId]);
|
|
17
|
+
const schemas = await pgPool.query(`select * from metaschema_public.schema where database_id=$1`, [databaseId]);
|
|
18
|
+
if (!db?.rows?.length) {
|
|
19
|
+
console.log('NO DATABASES.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!schemas?.rows?.length) {
|
|
23
|
+
console.log('NO SCHEMAS.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const name = extensionName || db.rows[0].name;
|
|
27
|
+
// When skipSchemaRenaming is true, pass empty schemas array to avoid renaming
|
|
28
|
+
// This is useful for self-referential introspection where you want to apply
|
|
29
|
+
// policies to real infrastructure schemas (metaschema_public, services_public, etc.)
|
|
30
|
+
const schemasForReplacement = skipSchemaRenaming
|
|
31
|
+
? []
|
|
32
|
+
: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name));
|
|
33
|
+
const { replace, replacer } = (0, export_utils_1.makeReplacer)({
|
|
34
|
+
schemas: schemasForReplacement,
|
|
35
|
+
name
|
|
36
|
+
});
|
|
37
|
+
// Filter sql_actions by database_id to avoid cross-database pollution
|
|
38
|
+
// Previously this query had no WHERE clause, which could export actions
|
|
39
|
+
// from unrelated databases in a persistent database environment
|
|
40
|
+
const results = await pgPool.query(`select * from db_migrate.sql_actions where database_id = $1 order by id`, [databaseId]);
|
|
41
|
+
const opts = {
|
|
42
|
+
name,
|
|
43
|
+
replacer,
|
|
44
|
+
outdir: normalizedOutdir,
|
|
45
|
+
author
|
|
46
|
+
};
|
|
47
|
+
// Build description for the database extension package
|
|
48
|
+
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
|
|
49
|
+
if (results?.rows?.length > 0) {
|
|
50
|
+
// Detect missing modules at workspace level and prompt user
|
|
51
|
+
const dbMissingResult = await (0, export_utils_1.detectMissingModules)(project, [...export_utils_1.DB_REQUIRED_EXTENSIONS], prompter, argv);
|
|
52
|
+
// Create/prepare the module directory
|
|
53
|
+
const dbModuleDir = await (0, export_utils_1.preparePackage)({
|
|
54
|
+
project,
|
|
55
|
+
author,
|
|
56
|
+
outdir: normalizedOutdir,
|
|
57
|
+
name,
|
|
58
|
+
description: dbExtensionDesc,
|
|
59
|
+
extensions: [...export_utils_1.DB_REQUIRED_EXTENSIONS],
|
|
60
|
+
prompter,
|
|
61
|
+
repoName,
|
|
62
|
+
username
|
|
63
|
+
});
|
|
64
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
65
|
+
if (dbMissingResult.shouldInstall) {
|
|
66
|
+
await (0, export_utils_1.installMissingModules)(dbModuleDir, dbMissingResult.missingModules);
|
|
67
|
+
}
|
|
68
|
+
(0, core_1.writePgpmPlan)(results.rows, opts);
|
|
69
|
+
(0, core_1.writePgpmFiles)(results.rows, opts);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log('No sql_actions found — skipping database module. Meta/service module will still be exported.');
|
|
73
|
+
}
|
|
74
|
+
// =========================================================================
|
|
75
|
+
// Meta/service module export — runs independently of sql_actions
|
|
76
|
+
// =========================================================================
|
|
77
|
+
const metaResult = await (0, export_meta_1.exportMeta)({
|
|
78
|
+
opts: options,
|
|
79
|
+
dbname: database,
|
|
80
|
+
database_id: databaseId
|
|
81
|
+
});
|
|
82
|
+
// Build description for the meta/service extension package
|
|
83
|
+
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
|
|
84
|
+
// Detect missing modules at workspace level and prompt user
|
|
85
|
+
const svcMissingResult = await (0, export_utils_1.detectMissingModules)(project, [...export_utils_1.SERVICE_REQUIRED_EXTENSIONS], prompter, argv);
|
|
86
|
+
// Create/prepare the module directory (use serviceOutdir if provided)
|
|
87
|
+
const svcModuleDir = await (0, export_utils_1.preparePackage)({
|
|
88
|
+
project,
|
|
89
|
+
author,
|
|
90
|
+
outdir: svcOutdir,
|
|
91
|
+
name: metaExtensionName,
|
|
92
|
+
description: metaDesc,
|
|
93
|
+
extensions: [...export_utils_1.SERVICE_REQUIRED_EXTENSIONS],
|
|
94
|
+
prompter,
|
|
95
|
+
repoName,
|
|
96
|
+
username
|
|
97
|
+
});
|
|
98
|
+
// Install missing modules if user confirmed (now that module exists)
|
|
99
|
+
if (svcMissingResult.shouldInstall) {
|
|
100
|
+
await (0, export_utils_1.installMissingModules)(svcModuleDir, svcMissingResult.missingModules);
|
|
101
|
+
}
|
|
102
|
+
// Use same skipSchemaRenaming logic for meta replacer
|
|
103
|
+
const metaSchemasForReplacement = skipSchemaRenaming
|
|
104
|
+
? []
|
|
105
|
+
: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name));
|
|
106
|
+
const metaReplacer = (0, export_utils_1.makeReplacer)({
|
|
107
|
+
schemas: metaSchemasForReplacement,
|
|
108
|
+
name: metaExtensionName,
|
|
109
|
+
// Use extensionName for schema prefix — the services metadata references
|
|
110
|
+
// schemas owned by the application package (e.g. agent_db_auth_public),
|
|
111
|
+
// not the services package (agent_db_services_auth_public)
|
|
112
|
+
schemaPrefix: name
|
|
113
|
+
});
|
|
114
|
+
// Create separate files for each table type
|
|
115
|
+
const metaPackage = [];
|
|
116
|
+
// Track which tables have content for dependency resolution
|
|
117
|
+
const tablesWithContent = [];
|
|
118
|
+
// Create a file for each table type that has content
|
|
119
|
+
for (const tableName of export_utils_1.META_TABLE_ORDER) {
|
|
120
|
+
const tableSql = metaResult[tableName];
|
|
121
|
+
if (tableSql) {
|
|
122
|
+
const replacedSql = metaReplacer.replacer(tableSql);
|
|
123
|
+
// Determine dependencies - each table depends on the previous tables that have content
|
|
124
|
+
// This ensures proper ordering during deployment
|
|
125
|
+
const deps = tableName === 'database'
|
|
126
|
+
? []
|
|
127
|
+
: tablesWithContent.length > 0
|
|
128
|
+
? [`migrate/${tablesWithContent[tablesWithContent.length - 1]}`]
|
|
129
|
+
: [];
|
|
130
|
+
metaPackage.push({
|
|
131
|
+
deps,
|
|
132
|
+
deploy: `migrate/${tableName}`,
|
|
133
|
+
content: `${export_utils_1.META_COMMON_HEADER}
|
|
134
|
+
|
|
135
|
+
${replacedSql}
|
|
136
|
+
|
|
137
|
+
${export_utils_1.META_COMMON_FOOTER}
|
|
138
|
+
`
|
|
139
|
+
});
|
|
140
|
+
tablesWithContent.push(tableName);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
opts.replacer = metaReplacer.replacer;
|
|
144
|
+
opts.name = metaExtensionName;
|
|
145
|
+
opts.outdir = svcOutdir;
|
|
146
|
+
(0, core_1.writePgpmPlan)(metaPackage, opts);
|
|
147
|
+
(0, core_1.writePgpmFiles)(metaPackage, opts);
|
|
148
|
+
pgPool.end();
|
|
149
|
+
};
|
|
150
|
+
const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, argv, repoName, username, serviceOutdir, skipSchemaRenaming }) => {
|
|
151
|
+
for (let v = 0; v < dbInfo.database_ids.length; v++) {
|
|
152
|
+
const databaseId = dbInfo.database_ids[v];
|
|
153
|
+
await exportMigrationsToDisk({
|
|
154
|
+
project,
|
|
155
|
+
options,
|
|
156
|
+
extensionName,
|
|
157
|
+
extensionDesc,
|
|
158
|
+
metaExtensionName,
|
|
159
|
+
metaExtensionDesc,
|
|
160
|
+
database: dbInfo.dbname,
|
|
161
|
+
databaseName: dbInfo.databaseName,
|
|
162
|
+
databaseId,
|
|
163
|
+
schema_names,
|
|
164
|
+
author,
|
|
165
|
+
outdir,
|
|
166
|
+
prompter,
|
|
167
|
+
argv,
|
|
168
|
+
repoName,
|
|
169
|
+
username,
|
|
170
|
+
serviceOutdir,
|
|
171
|
+
skipSchemaRenaming
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
exports.exportMigrations = exportMigrations;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Inquirerer } from 'inquirerer';
|
|
2
|
+
import { PgpmPackage } from '@pgpmjs/core';
|
|
3
|
+
/**
|
|
4
|
+
* Required extensions for database schema exports.
|
|
5
|
+
* Includes native PostgreSQL extensions and pgpm modules.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DB_REQUIRED_EXTENSIONS: readonly ["plpgsql", "uuid-ossp", "citext", "pgcrypto", "btree_gin", "btree_gist", "pg_textsearch", "pg_trgm", "postgis", "hstore", "vector", "metaschema-schema", "pgpm-inflection", "pgpm-uuid", "pgpm-utils", "pgpm-database-jobs", "pgpm-jwt-claims", "pgpm-stamps", "pgpm-base32", "pgpm-totp", "pgpm-types"];
|
|
8
|
+
/**
|
|
9
|
+
* Required extensions for service/meta exports.
|
|
10
|
+
* Includes native PostgreSQL extensions and pgpm modules for metadata management.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SERVICE_REQUIRED_EXTENSIONS: readonly ["plpgsql", "metaschema-schema", "metaschema-modules", "services"];
|
|
13
|
+
/**
|
|
14
|
+
* Common SQL header for meta export files.
|
|
15
|
+
* Sets session_replication_role and grants necessary permissions.
|
|
16
|
+
*/
|
|
17
|
+
export declare const META_COMMON_HEADER = "SET session_replication_role TO replica;\n-- using replica in case we are deploying triggers to metaschema_public\n\n-- unaccent, postgis affected and require grants\nGRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public to public;\n\nDO $LQLMIGRATION$\n DECLARE\n BEGIN\n\n EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_user');\n EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_admin');\n\n END;\n$LQLMIGRATION$;";
|
|
18
|
+
/**
|
|
19
|
+
* Common SQL footer for meta export files.
|
|
20
|
+
*/
|
|
21
|
+
export declare const META_COMMON_FOOTER = "\nSET session_replication_role TO DEFAULT;";
|
|
22
|
+
/**
|
|
23
|
+
* Ordered list of meta tables for export.
|
|
24
|
+
* Tables are processed in this order to satisfy foreign key dependencies.
|
|
25
|
+
*/
|
|
26
|
+
export declare const META_TABLE_ORDER: readonly ["database", "schema", "table", "field", "policy", "index", "trigger", "trigger_function", "rls_function", "foreign_key_constraint", "primary_key_constraint", "unique_constraint", "check_constraint", "full_text_search", "schema_grant", "table_grant", "default_privilege", "domains", "sites", "apis", "apps", "site_modules", "site_themes", "site_metadata", "api_modules", "api_extensions", "api_schemas", "rls_module", "user_auth_module", "memberships_module", "permissions_module", "limits_module", "levels_module", "users_module", "hierarchy_module", "membership_types_module", "invites_module", "emails_module", "sessions_module", "secrets_module", "profiles_module", "encrypted_secrets_module", "connected_accounts_module", "phone_numbers_module", "crypto_addresses_module", "crypto_auth_module", "field_module", "table_module", "secure_table_provision", "uuid_module", "default_ids_module", "denormalized_table_field", "table_template_module"];
|
|
27
|
+
export type FieldType = 'uuid' | 'uuid[]' | 'text' | 'text[]' | 'boolean' | 'image' | 'upload' | 'url' | 'jsonb' | 'jsonb[]' | 'int' | 'interval' | 'timestamptz';
|
|
28
|
+
export interface TableConfig {
|
|
29
|
+
schema: string;
|
|
30
|
+
table: string;
|
|
31
|
+
conflictDoNothing?: boolean;
|
|
32
|
+
fields: Record<string, FieldType>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shared metadata table configuration.
|
|
36
|
+
*
|
|
37
|
+
* This is the **superset** of fields needed by both the SQL export flow
|
|
38
|
+
* (export-meta.ts) and the GraphQL export flow (export-graphql-meta.ts).
|
|
39
|
+
* Each flow dynamically filters to only the fields that actually exist:
|
|
40
|
+
* - SQL flow: uses buildDynamicFields() to intersect with information_schema
|
|
41
|
+
* - GraphQL flow: filters to fields present in the returned data
|
|
42
|
+
*
|
|
43
|
+
* Adding a field here that doesn't exist in a particular environment is safe.
|
|
44
|
+
*/
|
|
45
|
+
export declare const META_TABLE_CONFIG: Record<string, TableConfig>;
|
|
46
|
+
export interface Schema {
|
|
47
|
+
name: string;
|
|
48
|
+
schema_name: string;
|
|
49
|
+
}
|
|
50
|
+
export interface MakeReplacerOptions {
|
|
51
|
+
schemas: Schema[];
|
|
52
|
+
name: string;
|
|
53
|
+
/**
|
|
54
|
+
* Optional prefix for schema name replacement.
|
|
55
|
+
* When provided, schema names are replaced using this prefix instead of `name`.
|
|
56
|
+
* This is needed for the services/meta package where `name` is the services
|
|
57
|
+
* extension name (e.g. "agent-db-services") but schemas should use the
|
|
58
|
+
* application extension prefix (e.g. "agent-db" → "agent_db_auth_public").
|
|
59
|
+
*/
|
|
60
|
+
schemaPrefix?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface ReplacerResult {
|
|
63
|
+
replacer: (str: string, n?: number) => string;
|
|
64
|
+
replace: [RegExp, string][];
|
|
65
|
+
}
|
|
66
|
+
export interface PreparePackageOptions {
|
|
67
|
+
project: PgpmPackage;
|
|
68
|
+
author: string;
|
|
69
|
+
outdir: string;
|
|
70
|
+
name: string;
|
|
71
|
+
description: string;
|
|
72
|
+
extensions: string[];
|
|
73
|
+
prompter?: Inquirerer;
|
|
74
|
+
/** Repository name for module scaffolding. Defaults to module name if not provided. */
|
|
75
|
+
repoName?: string;
|
|
76
|
+
/** GitHub username/org for module scaffolding. Required for non-interactive use. */
|
|
77
|
+
username?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Result of checking for missing modules at workspace level.
|
|
81
|
+
*/
|
|
82
|
+
export interface MissingModulesResult {
|
|
83
|
+
missingModules: {
|
|
84
|
+
controlName: string;
|
|
85
|
+
npmName: string;
|
|
86
|
+
}[];
|
|
87
|
+
shouldInstall: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks which pgpm modules from the extensions list are missing from the workspace
|
|
91
|
+
* and prompts the user if they want to install them.
|
|
92
|
+
*
|
|
93
|
+
* This function only does detection and prompting - it does NOT install.
|
|
94
|
+
* Use installMissingModules() after the module is created to do the actual installation.
|
|
95
|
+
*
|
|
96
|
+
* @param project - The PgpmPackage instance (only needs workspace context)
|
|
97
|
+
* @param extensions - List of extension names (control file names)
|
|
98
|
+
* @param prompter - Optional prompter for interactive confirmation
|
|
99
|
+
* @returns Object with missing modules and whether user wants to install them
|
|
100
|
+
*/
|
|
101
|
+
export declare const detectMissingModules: (project: PgpmPackage, extensions: string[], prompter?: Inquirerer, argv?: Record<string, any>) => Promise<MissingModulesResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Installs missing modules into a specific module directory.
|
|
104
|
+
* Must be called after the module has been created.
|
|
105
|
+
*
|
|
106
|
+
* @param moduleDir - The directory of the module to install into
|
|
107
|
+
* @param missingModules - Array of missing modules to install
|
|
108
|
+
*/
|
|
109
|
+
export declare const installMissingModules: (moduleDir: string, missingModules: {
|
|
110
|
+
controlName: string;
|
|
111
|
+
npmName: string;
|
|
112
|
+
}[]) => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Generates a function for replacing schema names and extension names in strings.
|
|
115
|
+
*/
|
|
116
|
+
export declare const makeReplacer: ({ schemas, name, schemaPrefix }: MakeReplacerOptions) => ReplacerResult;
|
|
117
|
+
/**
|
|
118
|
+
* Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
|
|
119
|
+
* If the module already exists and a prompter is provided, prompts the user for confirmation.
|
|
120
|
+
*
|
|
121
|
+
* @returns The absolute path to the created/prepared module directory
|
|
122
|
+
*/
|
|
123
|
+
export declare const preparePackage: ({ project, author, outdir, name, description, extensions, prompter, repoName, username }: PreparePackageOptions) => Promise<string>;
|
|
124
|
+
/**
|
|
125
|
+
* Normalizes an output directory path to ensure it ends with a path separator.
|
|
126
|
+
*/
|
|
127
|
+
export declare const normalizeOutdir: (outdir: string) => string;
|