@pgpmjs/core 6.8.2 → 6.9.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/esm/index.js +1 -2
- package/index.d.ts +1 -2
- package/index.js +3 -3
- package/package.json +3 -3
- package/esm/export/export-meta.js +0 -1016
- package/esm/export/export-migrations.js +0 -411
- package/export/export-meta.d.ts +0 -9
- package/export/export-meta.js +0 -1020
- package/export/export-migrations.d.ts +0 -40
- package/export/export-migrations.js +0 -418
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, rmSync } from 'fs';
|
|
2
|
-
import { sync as glob } from 'glob';
|
|
3
|
-
import { toSnakeCase } from 'komoji';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { getPgPool } from 'pg-cache';
|
|
6
|
-
import { PgpmPackage } from '../core/class/pgpm';
|
|
7
|
-
import { writePgpmFiles, writePgpmPlan } from '../files';
|
|
8
|
-
import { getMissingInstallableModules } from '../modules/modules';
|
|
9
|
-
import { parseAuthor } from '../utils/author';
|
|
10
|
-
import { exportMeta } from './export-meta';
|
|
11
|
-
/**
|
|
12
|
-
* Required extensions for database schema exports.
|
|
13
|
-
* Includes native PostgreSQL extensions and pgpm modules.
|
|
14
|
-
*/
|
|
15
|
-
const DB_REQUIRED_EXTENSIONS = [
|
|
16
|
-
'plpgsql',
|
|
17
|
-
'uuid-ossp',
|
|
18
|
-
'citext',
|
|
19
|
-
'pgcrypto',
|
|
20
|
-
'btree_gin',
|
|
21
|
-
'btree_gist',
|
|
22
|
-
'pg_textsearch',
|
|
23
|
-
'pg_trgm',
|
|
24
|
-
'postgis',
|
|
25
|
-
'hstore',
|
|
26
|
-
'vector',
|
|
27
|
-
'metaschema-schema',
|
|
28
|
-
'pgpm-inflection',
|
|
29
|
-
'pgpm-utils',
|
|
30
|
-
'pgpm-database-jobs',
|
|
31
|
-
'pgpm-jwt-claims',
|
|
32
|
-
'pgpm-stamps',
|
|
33
|
-
'pgpm-base32',
|
|
34
|
-
'pgpm-totp',
|
|
35
|
-
'pgpm-types'
|
|
36
|
-
];
|
|
37
|
-
/**
|
|
38
|
-
* Required extensions for service/meta exports.
|
|
39
|
-
* Includes native PostgreSQL extensions and pgpm modules for metadata management.
|
|
40
|
-
*/
|
|
41
|
-
const SERVICE_REQUIRED_EXTENSIONS = [
|
|
42
|
-
'plpgsql',
|
|
43
|
-
'metaschema-schema',
|
|
44
|
-
'metaschema-modules',
|
|
45
|
-
'services'
|
|
46
|
-
];
|
|
47
|
-
/**
|
|
48
|
-
* Checks which pgpm modules from the extensions list are missing from the workspace
|
|
49
|
-
* and prompts the user if they want to install them.
|
|
50
|
-
*
|
|
51
|
-
* This function only does detection and prompting - it does NOT install.
|
|
52
|
-
* Use installMissingModules() after the module is created to do the actual installation.
|
|
53
|
-
*
|
|
54
|
-
* @param project - The PgpmPackage instance (only needs workspace context)
|
|
55
|
-
* @param extensions - List of extension names (control file names)
|
|
56
|
-
* @param prompter - Optional prompter for interactive confirmation
|
|
57
|
-
* @returns Object with missing modules and whether user wants to install them
|
|
58
|
-
*/
|
|
59
|
-
const detectMissingModules = async (project, extensions, prompter) => {
|
|
60
|
-
// Use workspace-level check - doesn't require being inside a module
|
|
61
|
-
const installed = project.getWorkspaceInstalledModules();
|
|
62
|
-
const missingModules = getMissingInstallableModules(extensions, installed);
|
|
63
|
-
if (missingModules.length === 0) {
|
|
64
|
-
return { missingModules: [], shouldInstall: false };
|
|
65
|
-
}
|
|
66
|
-
const missingNames = missingModules.map(m => m.npmName);
|
|
67
|
-
console.log(`\nMissing pgpm modules detected: ${missingNames.join(', ')}`);
|
|
68
|
-
if (prompter) {
|
|
69
|
-
const { install } = await prompter.prompt({}, [
|
|
70
|
-
{
|
|
71
|
-
type: 'confirm',
|
|
72
|
-
name: 'install',
|
|
73
|
-
message: `Install missing modules (${missingNames.join(', ')})?`,
|
|
74
|
-
default: true
|
|
75
|
-
}
|
|
76
|
-
]);
|
|
77
|
-
return { missingModules, shouldInstall: install };
|
|
78
|
-
}
|
|
79
|
-
return { missingModules, shouldInstall: false };
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* Installs missing modules into a specific module directory.
|
|
83
|
-
* Must be called after the module has been created.
|
|
84
|
-
*
|
|
85
|
-
* @param moduleDir - The directory of the module to install into
|
|
86
|
-
* @param missingModules - Array of missing modules to install
|
|
87
|
-
*/
|
|
88
|
-
const installMissingModules = async (moduleDir, missingModules) => {
|
|
89
|
-
if (missingModules.length === 0) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const missingNames = missingModules.map(m => m.npmName);
|
|
93
|
-
console.log('Installing missing modules...');
|
|
94
|
-
// Create a new PgpmPackage instance pointing to the module directory
|
|
95
|
-
const moduleProject = new PgpmPackage(moduleDir);
|
|
96
|
-
await moduleProject.installModules(...missingNames);
|
|
97
|
-
console.log('Modules installed successfully.');
|
|
98
|
-
};
|
|
99
|
-
const exportMigrationsToDisk = async ({ project, options, database, databaseId, databaseName, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, repoName, username, serviceOutdir, skipSchemaRenaming = false }) => {
|
|
100
|
-
outdir = outdir + '/';
|
|
101
|
-
// Use serviceOutdir for service module, defaulting to outdir if not provided
|
|
102
|
-
const svcOutdir = (serviceOutdir || outdir.slice(0, -1)) + '/';
|
|
103
|
-
const pgPool = getPgPool({
|
|
104
|
-
...options.pg,
|
|
105
|
-
database
|
|
106
|
-
});
|
|
107
|
-
const db = await pgPool.query(`select * from metaschema_public.database where id=$1`, [databaseId]);
|
|
108
|
-
const schemas = await pgPool.query(`select * from metaschema_public.schema where database_id=$1`, [databaseId]);
|
|
109
|
-
if (!db?.rows?.length) {
|
|
110
|
-
console.log('NO DATABASES.');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (!schemas?.rows?.length) {
|
|
114
|
-
console.log('NO SCHEMAS.');
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const name = extensionName || db.rows[0].name;
|
|
118
|
-
// When skipSchemaRenaming is true, pass empty schemas array to avoid renaming
|
|
119
|
-
// This is useful for self-referential introspection where you want to apply
|
|
120
|
-
// policies to real infrastructure schemas (metaschema_public, services_public, etc.)
|
|
121
|
-
const schemasForReplacement = skipSchemaRenaming
|
|
122
|
-
? []
|
|
123
|
-
: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name));
|
|
124
|
-
const { replace, replacer } = makeReplacer({
|
|
125
|
-
schemas: schemasForReplacement,
|
|
126
|
-
name
|
|
127
|
-
});
|
|
128
|
-
// Filter sql_actions by database_id to avoid cross-database pollution
|
|
129
|
-
// Previously this query had no WHERE clause, which could export actions
|
|
130
|
-
// from unrelated databases in a persistent database environment
|
|
131
|
-
const results = await pgPool.query(`select * from db_migrate.sql_actions where database_id = $1 order by id`, [databaseId]);
|
|
132
|
-
const opts = {
|
|
133
|
-
name,
|
|
134
|
-
replacer,
|
|
135
|
-
outdir,
|
|
136
|
-
author
|
|
137
|
-
};
|
|
138
|
-
// Build description for the database extension package
|
|
139
|
-
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
|
|
140
|
-
if (results?.rows?.length > 0) {
|
|
141
|
-
// Detect missing modules at workspace level and prompt user
|
|
142
|
-
const dbMissingResult = await detectMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter);
|
|
143
|
-
// Create/prepare the module directory
|
|
144
|
-
const dbModuleDir = await preparePackage({
|
|
145
|
-
project,
|
|
146
|
-
author,
|
|
147
|
-
outdir,
|
|
148
|
-
name,
|
|
149
|
-
description: dbExtensionDesc,
|
|
150
|
-
extensions: [...DB_REQUIRED_EXTENSIONS],
|
|
151
|
-
prompter,
|
|
152
|
-
repoName,
|
|
153
|
-
username
|
|
154
|
-
});
|
|
155
|
-
// Install missing modules if user confirmed (now that module exists)
|
|
156
|
-
if (dbMissingResult.shouldInstall) {
|
|
157
|
-
await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
|
|
158
|
-
}
|
|
159
|
-
writePgpmPlan(results.rows, opts);
|
|
160
|
-
writePgpmFiles(results.rows, opts);
|
|
161
|
-
const metaResult = await exportMeta({
|
|
162
|
-
opts: options,
|
|
163
|
-
dbname: database,
|
|
164
|
-
database_id: databaseId
|
|
165
|
-
});
|
|
166
|
-
// Build description for the meta/service extension package
|
|
167
|
-
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
|
|
168
|
-
// Detect missing modules at workspace level and prompt user
|
|
169
|
-
const svcMissingResult = await detectMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter);
|
|
170
|
-
// Create/prepare the module directory (use serviceOutdir if provided)
|
|
171
|
-
const svcModuleDir = await preparePackage({
|
|
172
|
-
project,
|
|
173
|
-
author,
|
|
174
|
-
outdir: svcOutdir,
|
|
175
|
-
name: metaExtensionName,
|
|
176
|
-
description: metaDesc,
|
|
177
|
-
extensions: [...SERVICE_REQUIRED_EXTENSIONS],
|
|
178
|
-
prompter,
|
|
179
|
-
repoName,
|
|
180
|
-
username
|
|
181
|
-
});
|
|
182
|
-
// Install missing modules if user confirmed (now that module exists)
|
|
183
|
-
if (svcMissingResult.shouldInstall) {
|
|
184
|
-
await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
|
|
185
|
-
}
|
|
186
|
-
// Use same skipSchemaRenaming logic for meta replacer
|
|
187
|
-
const metaSchemasForReplacement = skipSchemaRenaming
|
|
188
|
-
? []
|
|
189
|
-
: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name));
|
|
190
|
-
const metaReplacer = makeReplacer({
|
|
191
|
-
schemas: metaSchemasForReplacement,
|
|
192
|
-
name: metaExtensionName,
|
|
193
|
-
// Use extensionName for schema prefix — the services metadata references
|
|
194
|
-
// schemas owned by the application package (e.g. agent_db_auth_public),
|
|
195
|
-
// not the services package (agent_db_services_auth_public)
|
|
196
|
-
schemaPrefix: name
|
|
197
|
-
});
|
|
198
|
-
// Create separate files for each table type
|
|
199
|
-
const metaPackage = [];
|
|
200
|
-
// Common header for all meta files
|
|
201
|
-
const commonHeader = `SET session_replication_role TO replica;
|
|
202
|
-
-- using replica in case we are deploying triggers to metaschema_public
|
|
203
|
-
|
|
204
|
-
-- unaccent, postgis affected and require grants
|
|
205
|
-
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public to public;
|
|
206
|
-
|
|
207
|
-
DO $LQLMIGRATION$
|
|
208
|
-
DECLARE
|
|
209
|
-
BEGIN
|
|
210
|
-
|
|
211
|
-
EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_user');
|
|
212
|
-
EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_admin');
|
|
213
|
-
|
|
214
|
-
END;
|
|
215
|
-
$LQLMIGRATION$;`;
|
|
216
|
-
const commonFooter = `
|
|
217
|
-
SET session_replication_role TO DEFAULT;`;
|
|
218
|
-
// Define table ordering with dependencies
|
|
219
|
-
// Tables that depend on 'database' being inserted first
|
|
220
|
-
const tableOrder = [
|
|
221
|
-
'database',
|
|
222
|
-
'schema',
|
|
223
|
-
'table',
|
|
224
|
-
'field',
|
|
225
|
-
'policy',
|
|
226
|
-
'index',
|
|
227
|
-
'trigger',
|
|
228
|
-
'trigger_function',
|
|
229
|
-
'rls_function',
|
|
230
|
-
'foreign_key_constraint',
|
|
231
|
-
'primary_key_constraint',
|
|
232
|
-
'unique_constraint',
|
|
233
|
-
'check_constraint',
|
|
234
|
-
'full_text_search',
|
|
235
|
-
'schema_grant',
|
|
236
|
-
'table_grant',
|
|
237
|
-
'default_privilege',
|
|
238
|
-
'domains',
|
|
239
|
-
'sites',
|
|
240
|
-
'apis',
|
|
241
|
-
'apps',
|
|
242
|
-
'site_modules',
|
|
243
|
-
'site_themes',
|
|
244
|
-
'site_metadata',
|
|
245
|
-
'api_modules',
|
|
246
|
-
'api_schemas',
|
|
247
|
-
'rls_module',
|
|
248
|
-
'user_auth_module',
|
|
249
|
-
'memberships_module',
|
|
250
|
-
'permissions_module',
|
|
251
|
-
'limits_module',
|
|
252
|
-
'levels_module',
|
|
253
|
-
'users_module',
|
|
254
|
-
'hierarchy_module',
|
|
255
|
-
'membership_types_module',
|
|
256
|
-
'invites_module',
|
|
257
|
-
'emails_module',
|
|
258
|
-
'sessions_module',
|
|
259
|
-
'secrets_module',
|
|
260
|
-
'profiles_module',
|
|
261
|
-
'encrypted_secrets_module',
|
|
262
|
-
'connected_accounts_module',
|
|
263
|
-
'phone_numbers_module',
|
|
264
|
-
'crypto_addresses_module',
|
|
265
|
-
'crypto_auth_module',
|
|
266
|
-
'field_module',
|
|
267
|
-
'table_module',
|
|
268
|
-
'secure_table_provision',
|
|
269
|
-
'user_profiles_module',
|
|
270
|
-
'user_settings_module',
|
|
271
|
-
'organization_settings_module',
|
|
272
|
-
'uuid_module',
|
|
273
|
-
'default_ids_module',
|
|
274
|
-
'denormalized_table_field'
|
|
275
|
-
];
|
|
276
|
-
// Track which tables have content for dependency resolution
|
|
277
|
-
const tablesWithContent = [];
|
|
278
|
-
// Create a file for each table type that has content
|
|
279
|
-
for (const tableName of tableOrder) {
|
|
280
|
-
const tableSql = metaResult[tableName];
|
|
281
|
-
if (tableSql) {
|
|
282
|
-
const replacedSql = metaReplacer.replacer(tableSql);
|
|
283
|
-
// Determine dependencies - each table depends on the previous tables that have content
|
|
284
|
-
// This ensures proper ordering during deployment
|
|
285
|
-
const deps = tableName === 'database'
|
|
286
|
-
? []
|
|
287
|
-
: tablesWithContent.length > 0
|
|
288
|
-
? [`migrate/${tablesWithContent[tablesWithContent.length - 1]}`]
|
|
289
|
-
: [];
|
|
290
|
-
metaPackage.push({
|
|
291
|
-
deps,
|
|
292
|
-
deploy: `migrate/${tableName}`,
|
|
293
|
-
content: `${commonHeader}
|
|
294
|
-
|
|
295
|
-
${replacedSql}
|
|
296
|
-
|
|
297
|
-
${commonFooter}
|
|
298
|
-
`
|
|
299
|
-
});
|
|
300
|
-
tablesWithContent.push(tableName);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
opts.replacer = metaReplacer.replacer;
|
|
304
|
-
opts.name = metaExtensionName;
|
|
305
|
-
opts.outdir = svcOutdir;
|
|
306
|
-
writePgpmPlan(metaPackage, opts);
|
|
307
|
-
writePgpmFiles(metaPackage, opts);
|
|
308
|
-
}
|
|
309
|
-
pgPool.end();
|
|
310
|
-
};
|
|
311
|
-
export const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, repoName, username, serviceOutdir, skipSchemaRenaming }) => {
|
|
312
|
-
for (let v = 0; v < dbInfo.database_ids.length; v++) {
|
|
313
|
-
const databaseId = dbInfo.database_ids[v];
|
|
314
|
-
await exportMigrationsToDisk({
|
|
315
|
-
project,
|
|
316
|
-
options,
|
|
317
|
-
extensionName,
|
|
318
|
-
extensionDesc,
|
|
319
|
-
metaExtensionName,
|
|
320
|
-
metaExtensionDesc,
|
|
321
|
-
database: dbInfo.dbname,
|
|
322
|
-
databaseName: dbInfo.databaseName,
|
|
323
|
-
databaseId,
|
|
324
|
-
schema_names,
|
|
325
|
-
author,
|
|
326
|
-
outdir,
|
|
327
|
-
prompter,
|
|
328
|
-
repoName,
|
|
329
|
-
username,
|
|
330
|
-
serviceOutdir,
|
|
331
|
-
skipSchemaRenaming
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
/**
|
|
336
|
-
* Creates a PGPM package directory or resets the deploy/revert/verify directories if it exists.
|
|
337
|
-
* If the module already exists and a prompter is provided, prompts the user for confirmation.
|
|
338
|
-
*
|
|
339
|
-
* @returns The absolute path to the created/prepared module directory
|
|
340
|
-
*/
|
|
341
|
-
const preparePackage = async ({ project, author, outdir, name, description, extensions, prompter, repoName, username }) => {
|
|
342
|
-
const curDir = process.cwd();
|
|
343
|
-
const pgpmDir = path.resolve(path.join(outdir, name));
|
|
344
|
-
mkdirSync(pgpmDir, { recursive: true });
|
|
345
|
-
process.chdir(pgpmDir);
|
|
346
|
-
const plan = glob(path.join(pgpmDir, 'pgpm.plan'));
|
|
347
|
-
if (!plan.length) {
|
|
348
|
-
const { fullName, email } = parseAuthor(author);
|
|
349
|
-
await project.initModule({
|
|
350
|
-
name,
|
|
351
|
-
description,
|
|
352
|
-
author,
|
|
353
|
-
extensions,
|
|
354
|
-
// Use outputDir to create module directly in the specified location
|
|
355
|
-
outputDir: outdir,
|
|
356
|
-
answers: {
|
|
357
|
-
moduleName: name,
|
|
358
|
-
moduleDesc: description,
|
|
359
|
-
access: 'restricted',
|
|
360
|
-
license: 'CLOSED',
|
|
361
|
-
fullName,
|
|
362
|
-
...(email && { email }),
|
|
363
|
-
// Use provided values or sensible defaults
|
|
364
|
-
repoName: repoName || name,
|
|
365
|
-
...(username && { username })
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
if (prompter) {
|
|
371
|
-
const { overwrite } = await prompter.prompt({}, [
|
|
372
|
-
{
|
|
373
|
-
type: 'confirm',
|
|
374
|
-
name: 'overwrite',
|
|
375
|
-
message: `Module "${name}" already exists at ${pgpmDir}. Overwrite deploy/revert/verify directories?`,
|
|
376
|
-
default: false
|
|
377
|
-
}
|
|
378
|
-
]);
|
|
379
|
-
if (!overwrite) {
|
|
380
|
-
process.chdir(curDir);
|
|
381
|
-
throw new Error(`Export cancelled: Module "${name}" already exists.`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
rmSync(path.resolve(pgpmDir, 'deploy'), { recursive: true, force: true });
|
|
385
|
-
rmSync(path.resolve(pgpmDir, 'revert'), { recursive: true, force: true });
|
|
386
|
-
rmSync(path.resolve(pgpmDir, 'verify'), { recursive: true, force: true });
|
|
387
|
-
}
|
|
388
|
-
process.chdir(curDir);
|
|
389
|
-
return pgpmDir;
|
|
390
|
-
};
|
|
391
|
-
/**
|
|
392
|
-
* Generates a function for replacing schema names and extension names in strings.
|
|
393
|
-
*/
|
|
394
|
-
const makeReplacer = ({ schemas, name, schemaPrefix }) => {
|
|
395
|
-
const replacements = ['constructive-extension-name', name];
|
|
396
|
-
const prefix = schemaPrefix || name;
|
|
397
|
-
const schemaReplacers = schemas.map((schema) => [
|
|
398
|
-
schema.schema_name,
|
|
399
|
-
toSnakeCase(`${prefix}_${schema.name}`)
|
|
400
|
-
]);
|
|
401
|
-
const replace = [...schemaReplacers, replacements].map(([from, to]) => [new RegExp(from, 'g'), to]);
|
|
402
|
-
const replacer = (str, n = 0) => {
|
|
403
|
-
if (!str)
|
|
404
|
-
return '';
|
|
405
|
-
if (replace[n] && replace[n].length === 2) {
|
|
406
|
-
return replacer(str.replace(replace[n][0], replace[n][1]), n + 1);
|
|
407
|
-
}
|
|
408
|
-
return str;
|
|
409
|
-
};
|
|
410
|
-
return { replacer, replace };
|
|
411
|
-
};
|
package/export/export-meta.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
|
-
interface ExportMetaParams {
|
|
3
|
-
opts: PgpmOptions;
|
|
4
|
-
dbname: string;
|
|
5
|
-
database_id: string;
|
|
6
|
-
}
|
|
7
|
-
export type ExportMetaResult = Record<string, string>;
|
|
8
|
-
export declare const exportMeta: ({ opts, dbname, database_id }: ExportMetaParams) => Promise<ExportMetaResult>;
|
|
9
|
-
export {};
|