@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.
Files changed (140) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +99 -0
  3. package/core/boilerplate-scanner.d.ts +41 -0
  4. package/core/boilerplate-scanner.js +106 -0
  5. package/core/boilerplate-types.d.ts +52 -0
  6. package/core/boilerplate-types.js +6 -0
  7. package/core/class/pgpm.d.ts +150 -0
  8. package/core/class/pgpm.js +1470 -0
  9. package/core/template-scaffold.d.ts +29 -0
  10. package/core/template-scaffold.js +168 -0
  11. package/esm/core/boilerplate-scanner.js +96 -0
  12. package/esm/core/boilerplate-types.js +5 -0
  13. package/esm/core/class/pgpm.js +1430 -0
  14. package/esm/core/template-scaffold.js +161 -0
  15. package/esm/export/export-meta.js +240 -0
  16. package/esm/export/export-migrations.js +180 -0
  17. package/esm/extensions/extensions.js +31 -0
  18. package/esm/files/extension/index.js +3 -0
  19. package/esm/files/extension/reader.js +79 -0
  20. package/esm/files/extension/writer.js +63 -0
  21. package/esm/files/index.js +6 -0
  22. package/esm/files/plan/generator.js +49 -0
  23. package/esm/files/plan/index.js +5 -0
  24. package/esm/files/plan/parser.js +296 -0
  25. package/esm/files/plan/validators.js +181 -0
  26. package/esm/files/plan/writer.js +114 -0
  27. package/esm/files/sql/index.js +1 -0
  28. package/esm/files/sql/writer.js +107 -0
  29. package/esm/files/sql-scripts/index.js +2 -0
  30. package/esm/files/sql-scripts/reader.js +19 -0
  31. package/esm/files/types/index.js +1 -0
  32. package/esm/files/types/package.js +1 -0
  33. package/esm/index.js +21 -0
  34. package/esm/init/client.js +144 -0
  35. package/esm/init/sql/bootstrap-roles.sql +55 -0
  36. package/esm/init/sql/bootstrap-test-roles.sql +72 -0
  37. package/esm/migrate/clean.js +23 -0
  38. package/esm/migrate/client.js +551 -0
  39. package/esm/migrate/index.js +5 -0
  40. package/esm/migrate/sql/procedures.sql +258 -0
  41. package/esm/migrate/sql/schema.sql +37 -0
  42. package/esm/migrate/types.js +1 -0
  43. package/esm/migrate/utils/event-logger.js +28 -0
  44. package/esm/migrate/utils/hash.js +27 -0
  45. package/esm/migrate/utils/transaction.js +125 -0
  46. package/esm/modules/modules.js +49 -0
  47. package/esm/packaging/package.js +96 -0
  48. package/esm/packaging/transform.js +70 -0
  49. package/esm/projects/deploy.js +123 -0
  50. package/esm/projects/revert.js +75 -0
  51. package/esm/projects/verify.js +61 -0
  52. package/esm/resolution/deps.js +526 -0
  53. package/esm/resolution/resolve.js +101 -0
  54. package/esm/utils/debug.js +147 -0
  55. package/esm/utils/target-utils.js +37 -0
  56. package/esm/workspace/paths.js +43 -0
  57. package/esm/workspace/utils.js +31 -0
  58. package/export/export-meta.d.ts +8 -0
  59. package/export/export-meta.js +244 -0
  60. package/export/export-migrations.d.ts +17 -0
  61. package/export/export-migrations.js +187 -0
  62. package/extensions/extensions.d.ts +5 -0
  63. package/extensions/extensions.js +35 -0
  64. package/files/extension/index.d.ts +2 -0
  65. package/files/extension/index.js +19 -0
  66. package/files/extension/reader.d.ts +24 -0
  67. package/files/extension/reader.js +86 -0
  68. package/files/extension/writer.d.ts +39 -0
  69. package/files/extension/writer.js +70 -0
  70. package/files/index.d.ts +5 -0
  71. package/files/index.js +22 -0
  72. package/files/plan/generator.d.ts +22 -0
  73. package/files/plan/generator.js +57 -0
  74. package/files/plan/index.d.ts +4 -0
  75. package/files/plan/index.js +21 -0
  76. package/files/plan/parser.d.ts +27 -0
  77. package/files/plan/parser.js +303 -0
  78. package/files/plan/validators.d.ts +52 -0
  79. package/files/plan/validators.js +187 -0
  80. package/files/plan/writer.d.ts +27 -0
  81. package/files/plan/writer.js +124 -0
  82. package/files/sql/index.d.ts +1 -0
  83. package/files/sql/index.js +17 -0
  84. package/files/sql/writer.d.ts +12 -0
  85. package/files/sql/writer.js +114 -0
  86. package/files/sql-scripts/index.d.ts +1 -0
  87. package/files/sql-scripts/index.js +18 -0
  88. package/files/sql-scripts/reader.d.ts +8 -0
  89. package/files/sql-scripts/reader.js +23 -0
  90. package/files/types/index.d.ts +46 -0
  91. package/files/types/index.js +17 -0
  92. package/files/types/package.d.ts +20 -0
  93. package/files/types/package.js +2 -0
  94. package/index.d.ts +21 -0
  95. package/index.js +45 -0
  96. package/init/client.d.ts +26 -0
  97. package/init/client.js +148 -0
  98. package/init/sql/bootstrap-roles.sql +55 -0
  99. package/init/sql/bootstrap-test-roles.sql +72 -0
  100. package/migrate/clean.d.ts +1 -0
  101. package/migrate/clean.js +27 -0
  102. package/migrate/client.d.ts +80 -0
  103. package/migrate/client.js +555 -0
  104. package/migrate/index.d.ts +5 -0
  105. package/migrate/index.js +21 -0
  106. package/migrate/sql/procedures.sql +258 -0
  107. package/migrate/sql/schema.sql +37 -0
  108. package/migrate/types.d.ts +67 -0
  109. package/migrate/types.js +2 -0
  110. package/migrate/utils/event-logger.d.ts +13 -0
  111. package/migrate/utils/event-logger.js +32 -0
  112. package/migrate/utils/hash.d.ts +12 -0
  113. package/migrate/utils/hash.js +32 -0
  114. package/migrate/utils/transaction.d.ts +27 -0
  115. package/migrate/utils/transaction.js +129 -0
  116. package/modules/modules.d.ts +31 -0
  117. package/modules/modules.js +56 -0
  118. package/package.json +70 -0
  119. package/packaging/package.d.ts +19 -0
  120. package/packaging/package.js +102 -0
  121. package/packaging/transform.d.ts +22 -0
  122. package/packaging/transform.js +75 -0
  123. package/projects/deploy.d.ts +8 -0
  124. package/projects/deploy.js +160 -0
  125. package/projects/revert.d.ts +15 -0
  126. package/projects/revert.js +112 -0
  127. package/projects/verify.d.ts +8 -0
  128. package/projects/verify.js +98 -0
  129. package/resolution/deps.d.ts +57 -0
  130. package/resolution/deps.js +531 -0
  131. package/resolution/resolve.d.ts +37 -0
  132. package/resolution/resolve.js +107 -0
  133. package/utils/debug.d.ts +21 -0
  134. package/utils/debug.js +153 -0
  135. package/utils/target-utils.d.ts +5 -0
  136. package/utils/target-utils.js +40 -0
  137. package/workspace/paths.d.ts +14 -0
  138. package/workspace/paths.js +50 -0
  139. package/workspace/utils.d.ts +8 -0
  140. package/workspace/utils.js +36 -0
@@ -0,0 +1,161 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { CacheManager, GitCloner, Templatizer } from 'create-gen-app';
5
+ import { readBoilerplateConfig, readBoilerplatesConfig, } from './boilerplate-scanner';
6
+ export const DEFAULT_TEMPLATE_REPO = 'https://github.com/constructive-io/pgpm-boilerplates.git';
7
+ export const DEFAULT_TEMPLATE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
8
+ export const DEFAULT_TEMPLATE_TOOL_NAME = 'pgpm';
9
+ const templatizer = new Templatizer();
10
+ const looksLikePath = (value) => {
11
+ return (value.startsWith('.') || value.startsWith('/') || value.startsWith('~'));
12
+ };
13
+ const normalizeQuestions = (questions) => questions?.map((q) => ({
14
+ ...q,
15
+ type: q.type || 'text',
16
+ }));
17
+ const attachQuestionsToTemplatizer = (templ, questions) => {
18
+ if (!questions?.length || typeof templ?.extract !== 'function')
19
+ return;
20
+ const originalExtract = templ.extract.bind(templ);
21
+ templ.extract = async (templateDir) => {
22
+ const extracted = await originalExtract(templateDir);
23
+ extracted.projectQuestions = {
24
+ questions: normalizeQuestions(questions),
25
+ };
26
+ return extracted;
27
+ };
28
+ };
29
+ /**
30
+ * Resolve the template path using the new metadata-driven resolution.
31
+ *
32
+ * Resolution order:
33
+ * 1. If explicit `templatePath` is provided, use it directly
34
+ * 2. If `.boilerplates.json` exists, use its `dir` field to find the base directory
35
+ * 3. Look for `{baseDir}/{type}` (e.g., "default/module")
36
+ * 4. Fallback to legacy structure: `{type}` directly in root
37
+ */
38
+ const resolveFromPath = (templateDir, templatePath, type, dirOverride) => {
39
+ // If explicit templatePath is provided, use it directly
40
+ if (templatePath) {
41
+ const candidateDir = path.isAbsolute(templatePath)
42
+ ? templatePath
43
+ : path.join(templateDir, templatePath);
44
+ if (fs.existsSync(candidateDir) &&
45
+ fs.statSync(candidateDir).isDirectory()) {
46
+ return {
47
+ fromPath: path.relative(templateDir, candidateDir) || '.',
48
+ resolvedTemplatePath: candidateDir,
49
+ };
50
+ }
51
+ return {
52
+ fromPath: templatePath,
53
+ resolvedTemplatePath: path.join(templateDir, templatePath),
54
+ };
55
+ }
56
+ // Try new metadata-driven resolution
57
+ const rootConfig = readBoilerplatesConfig(templateDir);
58
+ const baseDir = dirOverride ?? rootConfig?.dir;
59
+ if (baseDir) {
60
+ // New structure: {templateDir}/{baseDir}/{type}
61
+ const newStructurePath = path.join(templateDir, baseDir, type);
62
+ if (fs.existsSync(newStructurePath) &&
63
+ fs.statSync(newStructurePath).isDirectory()) {
64
+ return {
65
+ fromPath: path.join(baseDir, type),
66
+ resolvedTemplatePath: newStructurePath,
67
+ };
68
+ }
69
+ }
70
+ // Fallback to legacy structure: {templateDir}/{type}
71
+ const legacyPath = path.join(templateDir, type);
72
+ if (fs.existsSync(legacyPath) && fs.statSync(legacyPath).isDirectory()) {
73
+ return {
74
+ fromPath: type,
75
+ resolvedTemplatePath: legacyPath,
76
+ };
77
+ }
78
+ // Default fallback
79
+ return {
80
+ fromPath: type,
81
+ resolvedTemplatePath: path.join(templateDir, type),
82
+ };
83
+ };
84
+ export async function scaffoldTemplate(options) {
85
+ const { type, outputDir, templateRepo = DEFAULT_TEMPLATE_REPO, branch, templatePath, answers, noTty = false, cacheTtlMs = DEFAULT_TEMPLATE_TTL_MS, toolName = DEFAULT_TEMPLATE_TOOL_NAME, cwd, cacheBaseDir, dir, } = options;
86
+ const resolvedRepo = looksLikePath(templateRepo)
87
+ ? path.resolve(cwd ?? process.cwd(), templateRepo)
88
+ : templateRepo;
89
+ // Handle local template directories without caching
90
+ if (looksLikePath(templateRepo) &&
91
+ fs.existsSync(resolvedRepo) &&
92
+ fs.statSync(resolvedRepo).isDirectory()) {
93
+ const { fromPath, resolvedTemplatePath } = resolveFromPath(resolvedRepo, templatePath, type, dir);
94
+ // Read boilerplate config for questions
95
+ const boilerplateConfig = readBoilerplateConfig(resolvedTemplatePath);
96
+ // Inject questions into the templatizer pipeline so prompt types and defaults are applied
97
+ attachQuestionsToTemplatizer(templatizer, boilerplateConfig?.questions);
98
+ await templatizer.process(resolvedRepo, outputDir, {
99
+ argv: answers,
100
+ noTty,
101
+ fromPath,
102
+ });
103
+ return {
104
+ cacheUsed: false,
105
+ cacheExpired: false,
106
+ templateDir: resolvedRepo,
107
+ questions: boilerplateConfig?.questions,
108
+ };
109
+ }
110
+ // Remote repo with caching
111
+ const cacheManager = new CacheManager({
112
+ toolName,
113
+ ttl: cacheTtlMs,
114
+ baseDir: cacheBaseDir ??
115
+ process.env.PGPM_CACHE_BASE_DIR ??
116
+ (process.env.JEST_WORKER_ID
117
+ ? path.join(os.tmpdir(), `pgpm-cache-${process.env.JEST_WORKER_ID}`)
118
+ : undefined),
119
+ });
120
+ const gitCloner = new GitCloner();
121
+ const normalizedUrl = gitCloner.normalizeUrl(resolvedRepo);
122
+ const cacheKey = cacheManager.createKey(normalizedUrl, branch);
123
+ const expiredMetadata = cacheManager.checkExpiration(cacheKey);
124
+ if (expiredMetadata) {
125
+ cacheManager.clear(cacheKey);
126
+ }
127
+ let templateDir;
128
+ let cacheUsed = false;
129
+ const cachedPath = cacheManager.get(cacheKey);
130
+ if (cachedPath && !expiredMetadata) {
131
+ templateDir = cachedPath;
132
+ cacheUsed = true;
133
+ }
134
+ else {
135
+ const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
136
+ gitCloner.clone(normalizedUrl, tempDest, {
137
+ branch,
138
+ depth: 1,
139
+ singleBranch: true,
140
+ });
141
+ cacheManager.set(cacheKey, tempDest);
142
+ templateDir = tempDest;
143
+ }
144
+ const { fromPath, resolvedTemplatePath } = resolveFromPath(templateDir, templatePath, type, dir);
145
+ // Read boilerplate config for questions
146
+ const boilerplateConfig = readBoilerplateConfig(resolvedTemplatePath);
147
+ // Inject questions into the templatizer pipeline so prompt types and defaults are applied
148
+ attachQuestionsToTemplatizer(templatizer, boilerplateConfig?.questions);
149
+ await templatizer.process(templateDir, outputDir, {
150
+ argv: answers,
151
+ noTty,
152
+ fromPath,
153
+ });
154
+ return {
155
+ cacheUsed,
156
+ cacheExpired: Boolean(expiredMetadata),
157
+ cachePath: templateDir,
158
+ templateDir,
159
+ questions: boilerplateConfig?.questions,
160
+ };
161
+ }
@@ -0,0 +1,240 @@
1
+ import { Parser } from 'csv-to-pg';
2
+ import { getPgPool } from 'pg-cache';
3
+ const config = {
4
+ database: {
5
+ schema: 'collections_public',
6
+ table: 'database',
7
+ fields: {
8
+ id: 'uuid',
9
+ owner_id: 'uuid',
10
+ name: 'text',
11
+ hash: 'uuid'
12
+ }
13
+ },
14
+ database_extension: {
15
+ schema: 'collections_public',
16
+ table: 'database_extensions',
17
+ fields: {
18
+ name: 'text',
19
+ database_id: 'uuid'
20
+ }
21
+ },
22
+ schema: {
23
+ schema: 'collections_public',
24
+ table: 'schema',
25
+ fields: {
26
+ id: 'uuid',
27
+ database_id: 'uuid',
28
+ name: 'text',
29
+ schema_name: 'text',
30
+ description: 'text'
31
+ }
32
+ },
33
+ table: {
34
+ schema: 'collections_public',
35
+ table: 'table',
36
+ fields: {
37
+ id: 'uuid',
38
+ database_id: 'uuid',
39
+ schema_id: 'uuid',
40
+ name: 'text',
41
+ description: 'text'
42
+ }
43
+ },
44
+ field: {
45
+ schema: 'collections_public',
46
+ table: 'field',
47
+ fields: {
48
+ id: 'uuid',
49
+ database_id: 'uuid',
50
+ table_id: 'uuid',
51
+ name: 'text',
52
+ type: 'text',
53
+ description: 'text'
54
+ }
55
+ },
56
+ domains: {
57
+ schema: 'meta_public',
58
+ table: 'domains',
59
+ fields: {
60
+ id: 'uuid',
61
+ database_id: 'uuid',
62
+ site_id: 'uuid',
63
+ api_id: 'uuid',
64
+ domain: 'text',
65
+ subdomain: 'text'
66
+ }
67
+ },
68
+ sites: {
69
+ schema: 'meta_public',
70
+ table: 'sites',
71
+ fields: {
72
+ id: 'uuid',
73
+ database_id: 'uuid',
74
+ title: 'text',
75
+ description: 'text',
76
+ og_image: 'image',
77
+ favicon: 'upload',
78
+ apple_touch_icon: 'image',
79
+ logo: 'image',
80
+ dbname: 'text'
81
+ }
82
+ },
83
+ apis: {
84
+ schema: 'meta_public',
85
+ table: 'apis',
86
+ fields: {
87
+ id: 'uuid',
88
+ database_id: 'uuid',
89
+ name: 'text',
90
+ dbname: 'text',
91
+ is_public: 'boolean',
92
+ role_name: 'text',
93
+ anon_role: 'text'
94
+ }
95
+ },
96
+ apps: {
97
+ schema: 'meta_public',
98
+ table: 'apps',
99
+ fields: {
100
+ id: 'uuid',
101
+ database_id: 'uuid',
102
+ site_id: 'uuid',
103
+ name: 'text',
104
+ app_image: 'image',
105
+ app_store_link: 'url',
106
+ app_store_id: 'text',
107
+ app_id_prefix: 'text',
108
+ play_store_link: 'url'
109
+ }
110
+ },
111
+ site_modules: {
112
+ schema: 'meta_public',
113
+ table: 'site_modules',
114
+ fields: {
115
+ id: 'uuid',
116
+ database_id: 'uuid',
117
+ site_id: 'uuid',
118
+ name: 'text',
119
+ data: 'jsonb'
120
+ }
121
+ },
122
+ site_themes: {
123
+ schema: 'meta_public',
124
+ table: 'site_themes',
125
+ fields: {
126
+ id: 'uuid',
127
+ database_id: 'uuid',
128
+ site_id: 'uuid',
129
+ theme: 'jsonb'
130
+ }
131
+ },
132
+ api_modules: {
133
+ schema: 'meta_public',
134
+ table: 'api_modules',
135
+ fields: {
136
+ id: 'uuid',
137
+ database_id: 'uuid',
138
+ api_id: 'uuid',
139
+ name: 'text',
140
+ data: 'jsonb'
141
+ }
142
+ },
143
+ api_extensions: {
144
+ schema: 'meta_public',
145
+ table: 'api_extensions',
146
+ fields: {
147
+ id: 'uuid',
148
+ database_id: 'uuid',
149
+ api_id: 'uuid',
150
+ schema_name: 'text'
151
+ }
152
+ },
153
+ api_schemata: {
154
+ schema: 'meta_public',
155
+ table: 'api_schemata',
156
+ fields: {
157
+ id: 'uuid',
158
+ database_id: 'uuid',
159
+ schema_id: 'uuid',
160
+ api_id: 'uuid'
161
+ }
162
+ },
163
+ rls_module: {
164
+ schema: 'meta_public',
165
+ table: 'rls_module',
166
+ fields: {
167
+ id: 'uuid',
168
+ database_id: 'uuid',
169
+ api_id: 'uuid',
170
+ schema_id: 'uuid',
171
+ private_schema_id: 'uuid',
172
+ tokens_table_id: 'uuid',
173
+ users_table_id: 'uuid',
174
+ authenticate: 'text',
175
+ authenticate_strict: 'text',
176
+ current_role: 'text',
177
+ current_role_id: 'text'
178
+ }
179
+ },
180
+ user_auth_module: {
181
+ schema: 'meta_public',
182
+ table: 'user_auth_module',
183
+ fields: {
184
+ id: 'uuid',
185
+ database_id: 'uuid',
186
+ schema_id: 'uuid',
187
+ emails_table_id: 'uuid',
188
+ users_table_id: 'uuid',
189
+ secrets_table_id: 'uuid',
190
+ encrypted_table_id: 'uuid',
191
+ tokens_table_id: 'uuid',
192
+ sign_in_function: 'text',
193
+ sign_up_function: 'text',
194
+ sign_out_function: 'text',
195
+ sign_in_one_time_token_function: 'text',
196
+ one_time_token_function: 'text',
197
+ extend_token_expires: 'text',
198
+ send_account_deletion_email_function: 'text',
199
+ delete_account_function: 'text',
200
+ set_password_function: 'text',
201
+ reset_password_function: 'text',
202
+ forgot_password_function: 'text',
203
+ send_verification_email_function: 'text',
204
+ verify_email_function: 'text'
205
+ }
206
+ }
207
+ };
208
+ export const exportMeta = async ({ opts, dbname, database_id }) => {
209
+ const pool = getPgPool({
210
+ ...opts.pg,
211
+ database: dbname
212
+ });
213
+ const sql = {};
214
+ const parsers = Object.entries(config).reduce((m, [name, config]) => {
215
+ m[name] = new Parser(config);
216
+ return m;
217
+ }, {});
218
+ const queryAndParse = async (key, query) => {
219
+ const result = await pool.query(query, [database_id]);
220
+ if (result.rows.length) {
221
+ sql[key] = await parsers[key].parse(result.rows);
222
+ }
223
+ };
224
+ await queryAndParse('database', `SELECT * FROM collections_public.database WHERE id = $1`);
225
+ await queryAndParse('schema', `SELECT * FROM collections_public.schema WHERE database_id = $1`);
226
+ await queryAndParse('table', `SELECT * FROM collections_public.table WHERE database_id = $1`);
227
+ await queryAndParse('domains', `SELECT * FROM meta_public.domains WHERE database_id = $1`);
228
+ await queryAndParse('apis', `SELECT * FROM meta_public.apis WHERE database_id = $1`);
229
+ await queryAndParse('sites', `SELECT * FROM meta_public.sites WHERE database_id = $1`);
230
+ await queryAndParse('api_modules', `SELECT * FROM meta_public.api_modules WHERE database_id = $1`);
231
+ await queryAndParse('site_modules', `SELECT * FROM meta_public.site_modules WHERE database_id = $1`);
232
+ await queryAndParse('site_themes', `SELECT * FROM meta_public.site_themes WHERE database_id = $1`);
233
+ await queryAndParse('apps', `SELECT * FROM meta_public.apps WHERE database_id = $1`);
234
+ await queryAndParse('database_extension', `SELECT * FROM collections_public.database_extension WHERE database_id = $1`);
235
+ await queryAndParse('api_extensions', `SELECT * FROM meta_public.api_extensions WHERE database_id = $1`);
236
+ await queryAndParse('api_schemata', `SELECT * FROM meta_public.api_schemata WHERE database_id = $1`);
237
+ await queryAndParse('rls_module', `SELECT * FROM meta_public.rls_module WHERE database_id = $1`);
238
+ await queryAndParse('user_auth_module', `SELECT * FROM meta_public.user_auth_module WHERE database_id = $1`);
239
+ return Object.entries(sql).reduce((m, [_, v]) => m + '\n\n' + v, '');
240
+ };
@@ -0,0 +1,180 @@
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 { writeSqitchFiles, writeSqitchPlan } from '../files';
7
+ import { exportMeta } from './export-meta';
8
+ const exportMigrationsToDisk = async ({ project, options, database, databaseId, author, outdir, schema_names, extensionName, metaExtensionName }) => {
9
+ outdir = outdir + '/';
10
+ const pgPool = getPgPool({
11
+ ...options.pg,
12
+ database
13
+ });
14
+ const db = await pgPool.query(`select * from collections_public.database where id=$1`, [databaseId]);
15
+ const schemas = await pgPool.query(`select * from collections_public.schema where database_id=$1`, [databaseId]);
16
+ if (!db?.rows?.length) {
17
+ console.log('NO DATABASES.');
18
+ return;
19
+ }
20
+ if (!schemas?.rows?.length) {
21
+ console.log('NO SCHEMAS.');
22
+ return;
23
+ }
24
+ const name = extensionName || db.rows[0].name;
25
+ const { replace, replacer } = makeReplacer({
26
+ schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
27
+ name
28
+ });
29
+ const results = await pgPool.query(`select * from db_migrate.sql_actions order by id`);
30
+ const opts = {
31
+ name,
32
+ replacer,
33
+ outdir,
34
+ author
35
+ };
36
+ if (results?.rows?.length > 0) {
37
+ await preparePackage({
38
+ project,
39
+ author,
40
+ outdir,
41
+ name,
42
+ extensions: [
43
+ 'plpgsql',
44
+ 'uuid-ossp',
45
+ 'citext',
46
+ 'pgcrypto',
47
+ 'btree_gist',
48
+ 'postgis',
49
+ 'hstore',
50
+ 'db-meta-schema',
51
+ 'launchql-inflection',
52
+ 'launchql-uuid',
53
+ 'launchql-utils',
54
+ 'launchql-database-jobs',
55
+ 'launchql-jwt-claims',
56
+ 'launchql-stamps',
57
+ 'launchql-base32',
58
+ 'launchql-totp',
59
+ 'launchql-types',
60
+ 'launchql-default-roles'
61
+ ]
62
+ });
63
+ writeSqitchPlan(results.rows, opts);
64
+ writeSqitchFiles(results.rows, opts);
65
+ let meta = await exportMeta({
66
+ opts: options,
67
+ dbname: database,
68
+ database_id: databaseId
69
+ });
70
+ meta = replacer(meta);
71
+ await preparePackage({
72
+ project,
73
+ author,
74
+ outdir,
75
+ extensions: ['plpgsql', 'db-meta-schema', 'db-meta-modules'],
76
+ name: metaExtensionName
77
+ });
78
+ const metaReplacer = makeReplacer({
79
+ schemas: schemas.rows.filter((schema) => schema_names.includes(schema.schema_name)),
80
+ name: metaExtensionName
81
+ });
82
+ const metaPackage = [
83
+ {
84
+ deps: [],
85
+ deploy: 'migrate/meta',
86
+ content: `SET session_replication_role TO replica;
87
+ -- using replica in case we are deploying triggers to collections_public
88
+
89
+ -- unaccent, postgis affected and require grants
90
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public to public;
91
+
92
+ DO $LQLMIGRATION$
93
+ DECLARE
94
+ BEGIN
95
+
96
+ EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_user');
97
+ EXECUTE format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), 'app_admin');
98
+
99
+ END;
100
+ $LQLMIGRATION$;
101
+
102
+ ${meta}
103
+
104
+ UPDATE meta_public.apis
105
+ SET dbname = current_database() WHERE TRUE;
106
+
107
+ UPDATE meta_public.sites
108
+ SET dbname = current_database() WHERE TRUE;
109
+
110
+ SET session_replication_role TO DEFAULT;
111
+ `
112
+ }
113
+ ];
114
+ opts.replacer = metaReplacer.replacer;
115
+ opts.name = metaExtensionName;
116
+ writeSqitchPlan(metaPackage, opts);
117
+ writeSqitchFiles(metaPackage, opts);
118
+ }
119
+ pgPool.end();
120
+ };
121
+ export const exportMigrations = async ({ project, options, dbInfo, author, outdir, schema_names, extensionName, metaExtensionName }) => {
122
+ for (let v = 0; v < dbInfo.database_ids.length; v++) {
123
+ const databaseId = dbInfo.database_ids[v];
124
+ await exportMigrationsToDisk({
125
+ project,
126
+ options,
127
+ extensionName,
128
+ metaExtensionName,
129
+ database: dbInfo.dbname,
130
+ databaseId,
131
+ schema_names,
132
+ author,
133
+ outdir
134
+ });
135
+ }
136
+ };
137
+ /**
138
+ * Creates a Sqitch package directory or resets the deploy/revert/verify directories if it exists.
139
+ */
140
+ const preparePackage = async ({ project, author, outdir, name, extensions }) => {
141
+ const curDir = process.cwd();
142
+ const sqitchDir = path.resolve(path.join(outdir, name));
143
+ mkdirSync(sqitchDir, { recursive: true });
144
+ process.chdir(sqitchDir);
145
+ const plan = glob(path.join(sqitchDir, 'pgpm.plan'));
146
+ if (!plan.length) {
147
+ await project.initModule({
148
+ name,
149
+ description: name,
150
+ author,
151
+ extensions,
152
+ });
153
+ }
154
+ else {
155
+ rmSync(path.resolve(sqitchDir, 'deploy'), { recursive: true, force: true });
156
+ rmSync(path.resolve(sqitchDir, 'revert'), { recursive: true, force: true });
157
+ rmSync(path.resolve(sqitchDir, 'verify'), { recursive: true, force: true });
158
+ }
159
+ process.chdir(curDir);
160
+ };
161
+ /**
162
+ * Generates a function for replacing schema names and extension names in strings.
163
+ */
164
+ const makeReplacer = ({ schemas, name }) => {
165
+ const replacements = ['launchql-extension-name', name];
166
+ const schemaReplacers = schemas.map((schema) => [
167
+ schema.schema_name,
168
+ toSnakeCase(`${name}_${schema.name}`)
169
+ ]);
170
+ const replace = [...schemaReplacers, replacements].map(([from, to]) => [new RegExp(from, 'g'), to]);
171
+ const replacer = (str, n = 0) => {
172
+ if (!str)
173
+ return '';
174
+ if (replace[n] && replace[n].length === 2) {
175
+ return replacer(str.replace(replace[n][0], replace[n][1]), n + 1);
176
+ }
177
+ return str;
178
+ };
179
+ return { replacer, replace };
180
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Get the list of available extensions, including predefined core extensions.
3
+ */
4
+ export const getAvailableExtensions = (modules) => {
5
+ const coreExtensions = [
6
+ 'address_standardizer',
7
+ 'address_standardizer_data_us',
8
+ 'bloom',
9
+ 'btree_gin',
10
+ 'btree_gist',
11
+ 'citext',
12
+ 'hstore',
13
+ 'intarray',
14
+ 'pg_trgm',
15
+ 'pgcrypto',
16
+ 'plpgsql',
17
+ 'plperl',
18
+ 'plv8',
19
+ 'postgis_tiger_geocoder',
20
+ 'postgis_topology',
21
+ 'postgis',
22
+ 'postgres_fdw',
23
+ 'unaccent',
24
+ 'uuid-ossp',
25
+ ];
26
+ return Object.keys(modules).reduce((acc, module) => {
27
+ if (!acc.includes(module))
28
+ acc.push(module);
29
+ return acc;
30
+ }, [...coreExtensions]);
31
+ };
@@ -0,0 +1,3 @@
1
+ // Re-export all extension functionality
2
+ export * from './reader';
3
+ export * from './writer';
@@ -0,0 +1,79 @@
1
+ import { readFileSync } from 'fs';
2
+ import { basename, dirname, relative } from 'path';
3
+ import { parsePlanFileSimple } from '../plan';
4
+ /**
5
+ * Parse a .control file and extract its metadata.
6
+ * https://www.postgresql.org/docs/current/extend-extensions.html
7
+ */
8
+ export function parseControlFile(filePath, basePath) {
9
+ const contents = readFileSync(filePath, 'utf-8');
10
+ const key = basename(filePath).split('.control')[0];
11
+ const requires = contents
12
+ .split('\n')
13
+ .find((line) => /^requires/.test(line))
14
+ ?.split('=')[1]
15
+ .split(',')
16
+ .map((req) => req.replace(/[\'\s]*/g, '').trim()) || [];
17
+ const version = contents
18
+ .split('\n')
19
+ .find((line) => /^default_version/.test(line))
20
+ ?.split('=')[1]
21
+ .replace(/[\']*/g, '')
22
+ .trim() || '';
23
+ return {
24
+ path: dirname(relative(basePath, filePath)),
25
+ requires,
26
+ version,
27
+ };
28
+ }
29
+ /**
30
+ * Parse the pgpm.plan file to get the extension name.
31
+ */
32
+ export const getExtensionName = (packageDir) => {
33
+ const planPath = `${packageDir}/pgpm.plan`;
34
+ const plan = parsePlanFileSimple(planPath);
35
+ if (!plan.package) {
36
+ throw new Error('No package name found in pgpm.plan!');
37
+ }
38
+ return plan.package;
39
+ };
40
+ /**
41
+ * Get detailed information about an extension in the specified directory.
42
+ */
43
+ export const getExtensionInfo = (packageDir) => {
44
+ const pkgPath = `${packageDir}/package.json`;
45
+ const pkg = require(pkgPath);
46
+ const extname = getExtensionName(packageDir);
47
+ const version = pkg.version;
48
+ const Makefile = `${packageDir}/Makefile`;
49
+ const controlFile = `${packageDir}/${extname}.control`;
50
+ const sqlFile = `sql/${extname}--${version}.sql`;
51
+ return { extname, packageDir, version, Makefile, controlFile, sqlFile };
52
+ };
53
+ /**
54
+ * Get a list of extensions required by an extension from its control file.
55
+ * Returns an empty array if the file doesn't exist or has no requires line.
56
+ */
57
+ export const getInstalledExtensions = (controlFilePath) => {
58
+ try {
59
+ const contents = readFileSync(controlFilePath, 'utf-8');
60
+ const match = contents.match(/^\s*requires\s*=\s*'([^']*)'/m);
61
+ if (!match) {
62
+ return [];
63
+ }
64
+ const requiresValue = match[1];
65
+ if (!requiresValue || requiresValue.trim() === '') {
66
+ return [];
67
+ }
68
+ return requiresValue
69
+ .split(',')
70
+ .map((ext) => ext.trim())
71
+ .filter((ext) => ext.length > 0);
72
+ }
73
+ catch (e) {
74
+ if (e.code === 'ENOENT') {
75
+ return [];
76
+ }
77
+ throw new Error(`Error reading control file at ${controlFilePath}: ${e.message}`);
78
+ }
79
+ };