@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,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,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
|
+
};
|