@pgpmjs/core 3.0.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 +99 -0
- package/core/boilerplate-scanner.d.ts +41 -0
- package/core/boilerplate-scanner.js +106 -0
- package/core/boilerplate-types.d.ts +52 -0
- package/core/boilerplate-types.js +6 -0
- package/core/class/pgpm.d.ts +150 -0
- package/core/class/pgpm.js +1470 -0
- package/core/template-scaffold.d.ts +29 -0
- package/core/template-scaffold.js +168 -0
- package/esm/core/boilerplate-scanner.js +96 -0
- package/esm/core/boilerplate-types.js +5 -0
- package/esm/core/class/pgpm.js +1430 -0
- package/esm/core/template-scaffold.js +161 -0
- package/esm/export/export-meta.js +240 -0
- package/esm/export/export-migrations.js +180 -0
- package/esm/extensions/extensions.js +31 -0
- package/esm/files/extension/index.js +3 -0
- package/esm/files/extension/reader.js +79 -0
- package/esm/files/extension/writer.js +63 -0
- package/esm/files/index.js +6 -0
- package/esm/files/plan/generator.js +49 -0
- package/esm/files/plan/index.js +5 -0
- package/esm/files/plan/parser.js +296 -0
- package/esm/files/plan/validators.js +181 -0
- package/esm/files/plan/writer.js +114 -0
- package/esm/files/sql/index.js +1 -0
- package/esm/files/sql/writer.js +107 -0
- package/esm/files/sql-scripts/index.js +2 -0
- package/esm/files/sql-scripts/reader.js +19 -0
- package/esm/files/types/index.js +1 -0
- package/esm/files/types/package.js +1 -0
- package/esm/index.js +21 -0
- package/esm/init/client.js +144 -0
- package/esm/init/sql/bootstrap-roles.sql +55 -0
- package/esm/init/sql/bootstrap-test-roles.sql +72 -0
- package/esm/migrate/clean.js +23 -0
- package/esm/migrate/client.js +551 -0
- package/esm/migrate/index.js +5 -0
- package/esm/migrate/sql/procedures.sql +258 -0
- package/esm/migrate/sql/schema.sql +37 -0
- package/esm/migrate/types.js +1 -0
- package/esm/migrate/utils/event-logger.js +28 -0
- package/esm/migrate/utils/hash.js +27 -0
- package/esm/migrate/utils/transaction.js +125 -0
- package/esm/modules/modules.js +49 -0
- package/esm/packaging/package.js +96 -0
- package/esm/packaging/transform.js +70 -0
- package/esm/projects/deploy.js +123 -0
- package/esm/projects/revert.js +75 -0
- package/esm/projects/verify.js +61 -0
- package/esm/resolution/deps.js +526 -0
- package/esm/resolution/resolve.js +101 -0
- package/esm/utils/debug.js +147 -0
- package/esm/utils/target-utils.js +37 -0
- package/esm/workspace/paths.js +43 -0
- package/esm/workspace/utils.js +31 -0
- package/export/export-meta.d.ts +8 -0
- package/export/export-meta.js +244 -0
- package/export/export-migrations.d.ts +17 -0
- package/export/export-migrations.js +187 -0
- package/extensions/extensions.d.ts +5 -0
- package/extensions/extensions.js +35 -0
- package/files/extension/index.d.ts +2 -0
- package/files/extension/index.js +19 -0
- package/files/extension/reader.d.ts +24 -0
- package/files/extension/reader.js +86 -0
- package/files/extension/writer.d.ts +39 -0
- package/files/extension/writer.js +70 -0
- package/files/index.d.ts +5 -0
- package/files/index.js +22 -0
- package/files/plan/generator.d.ts +22 -0
- package/files/plan/generator.js +57 -0
- package/files/plan/index.d.ts +4 -0
- package/files/plan/index.js +21 -0
- package/files/plan/parser.d.ts +27 -0
- package/files/plan/parser.js +303 -0
- package/files/plan/validators.d.ts +52 -0
- package/files/plan/validators.js +187 -0
- package/files/plan/writer.d.ts +27 -0
- package/files/plan/writer.js +124 -0
- package/files/sql/index.d.ts +1 -0
- package/files/sql/index.js +17 -0
- package/files/sql/writer.d.ts +12 -0
- package/files/sql/writer.js +114 -0
- package/files/sql-scripts/index.d.ts +1 -0
- package/files/sql-scripts/index.js +18 -0
- package/files/sql-scripts/reader.d.ts +8 -0
- package/files/sql-scripts/reader.js +23 -0
- package/files/types/index.d.ts +46 -0
- package/files/types/index.js +17 -0
- package/files/types/package.d.ts +20 -0
- package/files/types/package.js +2 -0
- package/index.d.ts +21 -0
- package/index.js +45 -0
- package/init/client.d.ts +26 -0
- package/init/client.js +148 -0
- package/init/sql/bootstrap-roles.sql +55 -0
- package/init/sql/bootstrap-test-roles.sql +72 -0
- package/migrate/clean.d.ts +1 -0
- package/migrate/clean.js +27 -0
- package/migrate/client.d.ts +80 -0
- package/migrate/client.js +555 -0
- package/migrate/index.d.ts +5 -0
- package/migrate/index.js +21 -0
- package/migrate/sql/procedures.sql +258 -0
- package/migrate/sql/schema.sql +37 -0
- package/migrate/types.d.ts +67 -0
- package/migrate/types.js +2 -0
- package/migrate/utils/event-logger.d.ts +13 -0
- package/migrate/utils/event-logger.js +32 -0
- package/migrate/utils/hash.d.ts +12 -0
- package/migrate/utils/hash.js +32 -0
- package/migrate/utils/transaction.d.ts +27 -0
- package/migrate/utils/transaction.js +129 -0
- package/modules/modules.d.ts +31 -0
- package/modules/modules.js +56 -0
- package/package.json +70 -0
- package/packaging/package.d.ts +19 -0
- package/packaging/package.js +102 -0
- package/packaging/transform.d.ts +22 -0
- package/packaging/transform.js +75 -0
- package/projects/deploy.d.ts +8 -0
- package/projects/deploy.js +160 -0
- package/projects/revert.d.ts +15 -0
- package/projects/revert.js +112 -0
- package/projects/verify.d.ts +8 -0
- package/projects/verify.js +98 -0
- package/resolution/deps.d.ts +57 -0
- package/resolution/deps.js +531 -0
- package/resolution/resolve.d.ts +37 -0
- package/resolution/resolve.js +107 -0
- package/utils/debug.d.ts +21 -0
- package/utils/debug.js +153 -0
- package/utils/target-utils.d.ts +5 -0
- package/utils/target-utils.js +40 -0
- package/workspace/paths.d.ts +14 -0
- package/workspace/paths.js +50 -0
- package/workspace/utils.d.ts +8 -0
- package/workspace/utils.js +36 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Write a Sqitch plan file based on the provided rows
|
|
5
|
+
*/
|
|
6
|
+
export function writeSqitchPlan(rows, opts) {
|
|
7
|
+
const dir = path.resolve(path.join(opts.outdir, opts.name));
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
const date = () => '2017-08-11T08:11:51Z'; // stubbed timestamp
|
|
10
|
+
const author = opts.author || 'launchql';
|
|
11
|
+
const email = `${author}@5b0c196eeb62`;
|
|
12
|
+
const duplicates = {};
|
|
13
|
+
const plan = opts.replacer(`%syntax-version=1.0.0
|
|
14
|
+
%project=launchql-extension-name
|
|
15
|
+
%uri=launchql-extension-name
|
|
16
|
+
|
|
17
|
+
${rows
|
|
18
|
+
.map((row) => {
|
|
19
|
+
if (duplicates[row.deploy]) {
|
|
20
|
+
console.log('DUPLICATE ' + row.deploy);
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
duplicates[row.deploy] = true;
|
|
24
|
+
if (row.deps?.length) {
|
|
25
|
+
return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${author} <${email}> # add ${row.name}`;
|
|
26
|
+
}
|
|
27
|
+
return `${row.deploy} ${date()} ${author} <${email}> # add ${row.name}`;
|
|
28
|
+
})
|
|
29
|
+
.join('\n')}
|
|
30
|
+
`);
|
|
31
|
+
fs.writeFileSync(path.join(dir, 'pgpm.plan'), plan);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write a plan file with the provided content
|
|
35
|
+
*/
|
|
36
|
+
export function writePlanFile(planPath, plan) {
|
|
37
|
+
const content = generatePlanFileContent(plan);
|
|
38
|
+
fs.writeFileSync(planPath, content);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate content for a plan file
|
|
42
|
+
*/
|
|
43
|
+
export function generatePlanFileContent(plan) {
|
|
44
|
+
const { package: packageName, uri, changes, tags } = plan;
|
|
45
|
+
let content = `%syntax-version=1.0.0\n`;
|
|
46
|
+
content += `%project=${packageName}\n`;
|
|
47
|
+
if (uri) {
|
|
48
|
+
content += `%uri=${uri}\n`;
|
|
49
|
+
}
|
|
50
|
+
content += `\n`;
|
|
51
|
+
// Add changes and their associated tags
|
|
52
|
+
for (const change of changes) {
|
|
53
|
+
content += generateChangeLineContent(change);
|
|
54
|
+
content += `\n`;
|
|
55
|
+
const associatedTags = tags.filter(tag => tag.change === change.name);
|
|
56
|
+
for (const tag of associatedTags) {
|
|
57
|
+
content += generateTagLineContent(tag);
|
|
58
|
+
content += `\n`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate a line for a change in a plan file
|
|
65
|
+
*/
|
|
66
|
+
export function generateChangeLineContent(change) {
|
|
67
|
+
const { name, dependencies, timestamp, planner, email, comment } = change;
|
|
68
|
+
let line = name;
|
|
69
|
+
// Add dependencies if present
|
|
70
|
+
if (dependencies && dependencies.length > 0) {
|
|
71
|
+
line += ` [${dependencies.join(' ')}]`;
|
|
72
|
+
}
|
|
73
|
+
// Add timestamp if present
|
|
74
|
+
if (timestamp) {
|
|
75
|
+
line += ` ${timestamp}`;
|
|
76
|
+
// Add planner if present
|
|
77
|
+
if (planner) {
|
|
78
|
+
line += ` ${planner}`;
|
|
79
|
+
// Add email if present
|
|
80
|
+
if (email) {
|
|
81
|
+
line += ` <${email}>`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Add comment if present
|
|
86
|
+
if (comment) {
|
|
87
|
+
line += ` # ${comment}`;
|
|
88
|
+
}
|
|
89
|
+
return line;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate a line for a tag in a plan file
|
|
93
|
+
*/
|
|
94
|
+
export function generateTagLineContent(tag) {
|
|
95
|
+
const { name, timestamp, planner, email, comment } = tag;
|
|
96
|
+
let line = `@${name}`;
|
|
97
|
+
// Add timestamp if present
|
|
98
|
+
if (timestamp) {
|
|
99
|
+
line += ` ${timestamp}`;
|
|
100
|
+
// Add planner if present
|
|
101
|
+
if (planner) {
|
|
102
|
+
line += ` ${planner}`;
|
|
103
|
+
// Add email if present
|
|
104
|
+
if (email) {
|
|
105
|
+
line += ` <${email}>`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Add comment if present
|
|
110
|
+
if (comment) {
|
|
111
|
+
line += ` # ${comment}`;
|
|
112
|
+
}
|
|
113
|
+
return line;
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './writer';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getEnvOptions } from '@pgpmjs/env';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Write SQL files for Sqitch migrations (deploy, revert, verify)
|
|
6
|
+
*/
|
|
7
|
+
export const writeSqitchFiles = (rows, opts) => {
|
|
8
|
+
rows.forEach((row) => writeVerify(row, opts));
|
|
9
|
+
rows.forEach((row) => writeRevert(row, opts));
|
|
10
|
+
rows.forEach((row) => writeDeploy(row, opts));
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Sort dependencies in a consistent order
|
|
14
|
+
*/
|
|
15
|
+
const ordered = (arr) => {
|
|
16
|
+
if (!arr)
|
|
17
|
+
return [];
|
|
18
|
+
return arr.sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Write a deploy SQL file for a Sqitch change
|
|
22
|
+
*/
|
|
23
|
+
const writeDeploy = (row, opts) => {
|
|
24
|
+
const globalOpts = getEnvOptions({
|
|
25
|
+
migrations: {
|
|
26
|
+
codegen: {
|
|
27
|
+
useTx: opts.useTx
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const useTx = globalOpts.migrations.codegen.useTx;
|
|
32
|
+
const deploy = opts.replacer(row.deploy);
|
|
33
|
+
const dir = path.dirname(deploy);
|
|
34
|
+
const prefix = path.join(opts.outdir, opts.name, 'deploy');
|
|
35
|
+
const actualDir = path.resolve(prefix, dir);
|
|
36
|
+
const actualFile = path.resolve(prefix, `${deploy}.sql`);
|
|
37
|
+
fs.mkdirSync(actualDir, { recursive: true });
|
|
38
|
+
const sqlContent = opts.replacer(row.content);
|
|
39
|
+
const content = `-- Deploy: ${deploy}
|
|
40
|
+
-- made with <3 @ constructive.io
|
|
41
|
+
|
|
42
|
+
${opts.replacer(ordered(row?.deps)
|
|
43
|
+
.map((dep) => `-- requires: ${dep}`)
|
|
44
|
+
.join('\n') || '')}
|
|
45
|
+
|
|
46
|
+
${useTx ? 'BEGIN;' : ''}
|
|
47
|
+
${sqlContent}
|
|
48
|
+
${useTx ? 'COMMIT;' : ''}
|
|
49
|
+
`;
|
|
50
|
+
fs.writeFileSync(actualFile, content);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Write a verify SQL file for a Sqitch change
|
|
54
|
+
*/
|
|
55
|
+
const writeVerify = (row, opts) => {
|
|
56
|
+
const globalOpts = getEnvOptions({
|
|
57
|
+
migrations: {
|
|
58
|
+
codegen: {
|
|
59
|
+
useTx: opts.useTx
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const useTx = globalOpts.migrations.codegen.useTx;
|
|
64
|
+
const deploy = opts.replacer(row.deploy);
|
|
65
|
+
const dir = path.dirname(deploy);
|
|
66
|
+
const prefix = path.join(opts.outdir, opts.name, 'verify');
|
|
67
|
+
const actualDir = path.resolve(prefix, dir);
|
|
68
|
+
const actualFile = path.resolve(prefix, `${deploy}.sql`);
|
|
69
|
+
fs.mkdirSync(actualDir, { recursive: true });
|
|
70
|
+
const sqlContent = opts.replacer(row.verify);
|
|
71
|
+
const content = opts.replacer(`-- Verify: ${deploy}
|
|
72
|
+
|
|
73
|
+
${useTx ? 'BEGIN;' : ''}
|
|
74
|
+
${sqlContent}
|
|
75
|
+
${useTx ? 'COMMIT;' : ''}
|
|
76
|
+
|
|
77
|
+
`);
|
|
78
|
+
fs.writeFileSync(actualFile, content);
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Write a revert SQL file for a Sqitch change
|
|
82
|
+
*/
|
|
83
|
+
const writeRevert = (row, opts) => {
|
|
84
|
+
const globalOpts = getEnvOptions({
|
|
85
|
+
migrations: {
|
|
86
|
+
codegen: {
|
|
87
|
+
useTx: opts.useTx
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const useTx = globalOpts.migrations.codegen.useTx;
|
|
92
|
+
const deploy = opts.replacer(row.deploy);
|
|
93
|
+
const dir = path.dirname(deploy);
|
|
94
|
+
const prefix = path.join(opts.outdir, opts.name, 'revert');
|
|
95
|
+
const actualDir = path.resolve(prefix, dir);
|
|
96
|
+
const actualFile = path.resolve(prefix, `${deploy}.sql`);
|
|
97
|
+
fs.mkdirSync(actualDir, { recursive: true });
|
|
98
|
+
const sqlContent = opts.replacer(row.revert);
|
|
99
|
+
const content = `-- Revert: ${deploy}
|
|
100
|
+
|
|
101
|
+
${useTx ? 'BEGIN;' : ''}
|
|
102
|
+
${sqlContent}
|
|
103
|
+
${useTx ? 'COMMIT;' : ''}
|
|
104
|
+
|
|
105
|
+
`;
|
|
106
|
+
fs.writeFileSync(actualFile, content);
|
|
107
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Read a SQL script file, return empty string if not found
|
|
5
|
+
*/
|
|
6
|
+
export function readScript(basePath, scriptType, changeName) {
|
|
7
|
+
const scriptPath = join(basePath, scriptType, `${changeName}.sql`);
|
|
8
|
+
if (!existsSync(scriptPath)) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
return readFileSync(scriptPath, 'utf-8');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check if a script file exists
|
|
15
|
+
*/
|
|
16
|
+
export function scriptExists(basePath, scriptType, changeName) {
|
|
17
|
+
const scriptPath = join(basePath, scriptType, `${changeName}.sql`);
|
|
18
|
+
return existsSync(scriptPath);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './package';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './core/class/pgpm';
|
|
2
|
+
export * from './export/export-meta';
|
|
3
|
+
export * from './export/export-migrations';
|
|
4
|
+
export * from './extensions/extensions';
|
|
5
|
+
export * from './modules/modules';
|
|
6
|
+
export * from './packaging/package';
|
|
7
|
+
export * from './packaging/transform';
|
|
8
|
+
export * from './resolution/deps';
|
|
9
|
+
export * from './resolution/resolve';
|
|
10
|
+
export * from './workspace/paths';
|
|
11
|
+
export * from './workspace/utils';
|
|
12
|
+
export * from './core/template-scaffold';
|
|
13
|
+
export * from './core/boilerplate-types';
|
|
14
|
+
export * from './core/boilerplate-scanner';
|
|
15
|
+
// Export package-files functionality (now integrated into core)
|
|
16
|
+
export * from './files';
|
|
17
|
+
export { cleanSql } from './migrate/clean';
|
|
18
|
+
export { PgpmMigrate } from './migrate/client';
|
|
19
|
+
export { PgpmInit } from './init/client';
|
|
20
|
+
export { hashFile, hashString } from './migrate/utils/hash';
|
|
21
|
+
export { executeQuery, withTransaction } from './migrate/utils/transaction';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Logger } from '@pgpmjs/logger';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getPgPool } from 'pg-cache';
|
|
5
|
+
const log = new Logger('init');
|
|
6
|
+
export class PgpmInit {
|
|
7
|
+
pool;
|
|
8
|
+
pgConfig;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.pgConfig = config;
|
|
11
|
+
this.pool = getPgPool(this.pgConfig);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Bootstrap standard roles (anonymous, authenticated, administrator)
|
|
15
|
+
*/
|
|
16
|
+
async bootstrapRoles() {
|
|
17
|
+
try {
|
|
18
|
+
log.info('Bootstrapping PGPM roles...');
|
|
19
|
+
const sqlPath = join(__dirname, 'sql', 'bootstrap-roles.sql');
|
|
20
|
+
const sql = readFileSync(sqlPath, 'utf-8');
|
|
21
|
+
await this.pool.query(sql);
|
|
22
|
+
log.success('Successfully bootstrapped PGPM roles');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
log.error('Failed to bootstrap roles:', error);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Bootstrap test roles (roles only, no users)
|
|
31
|
+
*/
|
|
32
|
+
async bootstrapTestRoles() {
|
|
33
|
+
try {
|
|
34
|
+
log.warn('WARNING: This command creates test roles and should NEVER be run on a production database!');
|
|
35
|
+
log.info('Bootstrapping PGPM test roles...');
|
|
36
|
+
const sqlPath = join(__dirname, 'sql', 'bootstrap-test-roles.sql');
|
|
37
|
+
const sql = readFileSync(sqlPath, 'utf-8');
|
|
38
|
+
await this.pool.query(sql);
|
|
39
|
+
log.success('Successfully bootstrapped PGPM test roles');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log.error('Failed to bootstrap test roles:', error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Bootstrap database roles with custom username and password
|
|
48
|
+
*/
|
|
49
|
+
async bootstrapDbRoles(username, password) {
|
|
50
|
+
try {
|
|
51
|
+
log.info(`Bootstrapping PGPM database roles for user: ${username}...`);
|
|
52
|
+
const sql = `
|
|
53
|
+
BEGIN;
|
|
54
|
+
DO $do$
|
|
55
|
+
DECLARE
|
|
56
|
+
v_username TEXT := '${username.replace(/'/g, "''")}';
|
|
57
|
+
v_password TEXT := '${password.replace(/'/g, "''")}';
|
|
58
|
+
BEGIN
|
|
59
|
+
BEGIN
|
|
60
|
+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', v_username, v_password);
|
|
61
|
+
EXCEPTION
|
|
62
|
+
WHEN duplicate_object THEN
|
|
63
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
64
|
+
NULL;
|
|
65
|
+
END;
|
|
66
|
+
END
|
|
67
|
+
$do$;
|
|
68
|
+
|
|
69
|
+
-- Robust GRANTs under concurrency: GRANT can race on pg_auth_members unique index.
|
|
70
|
+
-- Catch unique_violation (23505) and continue so CI/CD concurrent jobs don't fail.
|
|
71
|
+
DO $do$
|
|
72
|
+
DECLARE
|
|
73
|
+
v_username TEXT := '${username.replace(/'/g, "''")}';
|
|
74
|
+
BEGIN
|
|
75
|
+
BEGIN
|
|
76
|
+
EXECUTE format('GRANT %I TO %I', 'anonymous', v_username);
|
|
77
|
+
EXCEPTION
|
|
78
|
+
WHEN unique_violation THEN
|
|
79
|
+
-- Membership was granted concurrently; ignore.
|
|
80
|
+
NULL;
|
|
81
|
+
WHEN undefined_object THEN
|
|
82
|
+
-- One of the roles doesn't exist yet; order operations as needed.
|
|
83
|
+
RAISE NOTICE 'Missing role when granting % to %', 'anonymous', v_username;
|
|
84
|
+
END;
|
|
85
|
+
|
|
86
|
+
BEGIN
|
|
87
|
+
EXECUTE format('GRANT %I TO %I', 'authenticated', v_username);
|
|
88
|
+
EXCEPTION
|
|
89
|
+
WHEN unique_violation THEN
|
|
90
|
+
-- Membership was granted concurrently; ignore.
|
|
91
|
+
NULL;
|
|
92
|
+
WHEN undefined_object THEN
|
|
93
|
+
RAISE NOTICE 'Missing role when granting % to %', 'authenticated', v_username;
|
|
94
|
+
END;
|
|
95
|
+
END
|
|
96
|
+
$do$;
|
|
97
|
+
COMMIT;
|
|
98
|
+
`;
|
|
99
|
+
await this.pool.query(sql);
|
|
100
|
+
log.success(`Successfully bootstrapped PGPM database roles for user: ${username}`);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
log.error(`Failed to bootstrap database roles for user ${username}:`, error);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Remove database roles and revoke grants
|
|
109
|
+
*/
|
|
110
|
+
async removeDbRoles(username) {
|
|
111
|
+
try {
|
|
112
|
+
log.info(`Removing PGPM database roles for user: ${username}...`);
|
|
113
|
+
const sql = `
|
|
114
|
+
BEGIN;
|
|
115
|
+
DO $do$
|
|
116
|
+
BEGIN
|
|
117
|
+
IF EXISTS (
|
|
118
|
+
SELECT 1
|
|
119
|
+
FROM
|
|
120
|
+
pg_catalog.pg_roles
|
|
121
|
+
WHERE
|
|
122
|
+
rolname = '${username}') THEN
|
|
123
|
+
REVOKE anonymous FROM ${username};
|
|
124
|
+
REVOKE authenticated FROM ${username};
|
|
125
|
+
DROP ROLE ${username};
|
|
126
|
+
END IF;
|
|
127
|
+
END
|
|
128
|
+
$do$;
|
|
129
|
+
COMMIT;
|
|
130
|
+
`;
|
|
131
|
+
await this.pool.query(sql);
|
|
132
|
+
log.success(`Successfully removed PGPM database roles for user: ${username}`);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
log.error(`Failed to remove database roles for user ${username}:`, error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Close the database connection
|
|
141
|
+
*/
|
|
142
|
+
async close() {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
BEGIN;
|
|
2
|
+
DO $do$
|
|
3
|
+
BEGIN
|
|
4
|
+
-- anonymous
|
|
5
|
+
BEGIN
|
|
6
|
+
EXECUTE format('CREATE ROLE %I', 'anonymous');
|
|
7
|
+
EXCEPTION
|
|
8
|
+
WHEN duplicate_object THEN
|
|
9
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
10
|
+
NULL;
|
|
11
|
+
END;
|
|
12
|
+
|
|
13
|
+
-- authenticated
|
|
14
|
+
BEGIN
|
|
15
|
+
EXECUTE format('CREATE ROLE %I', 'authenticated');
|
|
16
|
+
EXCEPTION
|
|
17
|
+
WHEN duplicate_object THEN
|
|
18
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
19
|
+
NULL;
|
|
20
|
+
END;
|
|
21
|
+
|
|
22
|
+
-- administrator
|
|
23
|
+
BEGIN
|
|
24
|
+
EXECUTE format('CREATE ROLE %I', 'administrator');
|
|
25
|
+
EXCEPTION
|
|
26
|
+
WHEN duplicate_object THEN
|
|
27
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
28
|
+
NULL;
|
|
29
|
+
END;
|
|
30
|
+
END
|
|
31
|
+
$do$;
|
|
32
|
+
|
|
33
|
+
-- Set role attributes (safe to run even if role already exists)
|
|
34
|
+
ALTER USER anonymous WITH NOCREATEDB;
|
|
35
|
+
ALTER USER anonymous WITH NOSUPERUSER;
|
|
36
|
+
ALTER USER anonymous WITH NOCREATEROLE;
|
|
37
|
+
ALTER USER anonymous WITH NOLOGIN;
|
|
38
|
+
ALTER USER anonymous WITH NOREPLICATION;
|
|
39
|
+
ALTER USER anonymous WITH NOBYPASSRLS;
|
|
40
|
+
|
|
41
|
+
ALTER USER authenticated WITH NOCREATEDB;
|
|
42
|
+
ALTER USER authenticated WITH NOSUPERUSER;
|
|
43
|
+
ALTER USER authenticated WITH NOCREATEROLE;
|
|
44
|
+
ALTER USER authenticated WITH NOLOGIN;
|
|
45
|
+
ALTER USER authenticated WITH NOREPLICATION;
|
|
46
|
+
ALTER USER authenticated WITH NOBYPASSRLS;
|
|
47
|
+
|
|
48
|
+
ALTER USER administrator WITH NOCREATEDB;
|
|
49
|
+
ALTER USER administrator WITH NOSUPERUSER;
|
|
50
|
+
ALTER USER administrator WITH NOCREATEROLE;
|
|
51
|
+
ALTER USER administrator WITH NOLOGIN;
|
|
52
|
+
ALTER USER administrator WITH NOREPLICATION;
|
|
53
|
+
-- they CAN bypass RLS
|
|
54
|
+
ALTER USER administrator WITH BYPASSRLS;
|
|
55
|
+
COMMIT;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
BEGIN;
|
|
2
|
+
DO $do$
|
|
3
|
+
BEGIN
|
|
4
|
+
BEGIN
|
|
5
|
+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_user', 'app_password');
|
|
6
|
+
EXCEPTION
|
|
7
|
+
WHEN duplicate_object THEN
|
|
8
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
9
|
+
NULL;
|
|
10
|
+
END;
|
|
11
|
+
|
|
12
|
+
BEGIN
|
|
13
|
+
EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', 'app_admin', 'admin_password');
|
|
14
|
+
EXCEPTION
|
|
15
|
+
WHEN duplicate_object THEN
|
|
16
|
+
-- Role already exists; optionally sync attributes here with ALTER ROLE
|
|
17
|
+
NULL;
|
|
18
|
+
END;
|
|
19
|
+
END
|
|
20
|
+
$do$;
|
|
21
|
+
|
|
22
|
+
DO $do$
|
|
23
|
+
BEGIN
|
|
24
|
+
BEGIN
|
|
25
|
+
EXECUTE format('GRANT %I TO %I', 'anonymous', 'app_user');
|
|
26
|
+
EXCEPTION
|
|
27
|
+
WHEN unique_violation THEN
|
|
28
|
+
-- Membership was granted concurrently; ignore.
|
|
29
|
+
NULL;
|
|
30
|
+
WHEN undefined_object THEN
|
|
31
|
+
-- One of the roles doesn't exist yet; order operations as needed.
|
|
32
|
+
RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'app_user';
|
|
33
|
+
END;
|
|
34
|
+
|
|
35
|
+
BEGIN
|
|
36
|
+
EXECUTE format('GRANT %I TO %I', 'authenticated', 'app_user');
|
|
37
|
+
EXCEPTION
|
|
38
|
+
WHEN unique_violation THEN
|
|
39
|
+
NULL;
|
|
40
|
+
WHEN undefined_object THEN
|
|
41
|
+
RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'app_user';
|
|
42
|
+
END;
|
|
43
|
+
|
|
44
|
+
BEGIN
|
|
45
|
+
EXECUTE format('GRANT %I TO %I', 'anonymous', 'administrator');
|
|
46
|
+
EXCEPTION
|
|
47
|
+
WHEN unique_violation THEN
|
|
48
|
+
NULL;
|
|
49
|
+
WHEN undefined_object THEN
|
|
50
|
+
RAISE NOTICE 'Missing role when granting % to %', 'anonymous', 'administrator';
|
|
51
|
+
END;
|
|
52
|
+
|
|
53
|
+
BEGIN
|
|
54
|
+
EXECUTE format('GRANT %I TO %I', 'authenticated', 'administrator');
|
|
55
|
+
EXCEPTION
|
|
56
|
+
WHEN unique_violation THEN
|
|
57
|
+
NULL;
|
|
58
|
+
WHEN undefined_object THEN
|
|
59
|
+
RAISE NOTICE 'Missing role when granting % to %', 'authenticated', 'administrator';
|
|
60
|
+
END;
|
|
61
|
+
|
|
62
|
+
BEGIN
|
|
63
|
+
EXECUTE format('GRANT %I TO %I', 'administrator', 'app_admin');
|
|
64
|
+
EXCEPTION
|
|
65
|
+
WHEN unique_violation THEN
|
|
66
|
+
NULL;
|
|
67
|
+
WHEN undefined_object THEN
|
|
68
|
+
RAISE NOTICE 'Missing role when granting % to %', 'administrator', 'app_admin';
|
|
69
|
+
END;
|
|
70
|
+
END
|
|
71
|
+
$do$;
|
|
72
|
+
COMMIT;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { deparse, parse } from 'pgsql-parser';
|
|
2
|
+
const filterStatements = (stmts) => {
|
|
3
|
+
const filteredStmts = stmts.filter(node => {
|
|
4
|
+
const stmt = node.stmt;
|
|
5
|
+
return stmt && !stmt.hasOwnProperty('TransactionStmt') &&
|
|
6
|
+
!stmt.hasOwnProperty('CreateExtensionStmt');
|
|
7
|
+
});
|
|
8
|
+
const hasFiltered = filteredStmts.length !== stmts.length;
|
|
9
|
+
return { filteredStmts, hasFiltered };
|
|
10
|
+
};
|
|
11
|
+
export const cleanSql = async (sql, pretty, functionDelimiter) => {
|
|
12
|
+
const parsed = await parse(sql);
|
|
13
|
+
const { filteredStmts, hasFiltered } = filterStatements(parsed.stmts);
|
|
14
|
+
if (!hasFiltered) {
|
|
15
|
+
return sql;
|
|
16
|
+
}
|
|
17
|
+
parsed.stmts = filteredStmts;
|
|
18
|
+
const finalSql = await deparse(parsed, {
|
|
19
|
+
pretty,
|
|
20
|
+
functionDelimiter
|
|
21
|
+
});
|
|
22
|
+
return finalSql;
|
|
23
|
+
};
|