@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,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parsePlanFile = parsePlanFile;
|
|
4
|
+
exports.resolveReference = resolveReference;
|
|
5
|
+
exports.parsePlanFileSimple = parsePlanFileSimple;
|
|
6
|
+
exports.getChanges = getChanges;
|
|
7
|
+
exports.getLatestChange = getLatestChange;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const validators_1 = require("./validators");
|
|
10
|
+
const types_1 = require("@pgpmjs/types");
|
|
11
|
+
/**
|
|
12
|
+
* Parse a Sqitch plan file with full validation
|
|
13
|
+
* Supports both changes and tags
|
|
14
|
+
*/
|
|
15
|
+
function parsePlanFile(planPath) {
|
|
16
|
+
const content = (0, fs_1.readFileSync)(planPath, 'utf-8');
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
let project = '';
|
|
19
|
+
let uri = '';
|
|
20
|
+
const changes = [];
|
|
21
|
+
const tags = [];
|
|
22
|
+
const errors = [];
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const lineNum = i + 1;
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
// Skip empty lines
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
continue;
|
|
30
|
+
// Skip comments
|
|
31
|
+
if (trimmed.startsWith('#'))
|
|
32
|
+
continue;
|
|
33
|
+
// Parse package metadata
|
|
34
|
+
if (trimmed.startsWith('%project=')) {
|
|
35
|
+
project = trimmed.substring('%project='.length);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (trimmed.startsWith('%uri=')) {
|
|
39
|
+
uri = trimmed.substring('%uri='.length);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Skip other metadata lines
|
|
43
|
+
if (trimmed.startsWith('%')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Parse tag lines
|
|
47
|
+
if (trimmed.startsWith('@')) {
|
|
48
|
+
const lastChangeName = changes.length > 0 ? changes[changes.length - 1].name : null;
|
|
49
|
+
const tag = parseTagLine(trimmed, lastChangeName);
|
|
50
|
+
if (tag) {
|
|
51
|
+
if ((0, validators_1.isValidTagName)(tag.name)) {
|
|
52
|
+
tags.push(tag);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
errors.push({
|
|
56
|
+
line: lineNum,
|
|
57
|
+
message: `Invalid tag name: ${tag.name}`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
errors.push({
|
|
63
|
+
line: lineNum,
|
|
64
|
+
message: 'Invalid tag line format'
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Parse change lines
|
|
70
|
+
const change = parseChangeLine(trimmed);
|
|
71
|
+
if (change) {
|
|
72
|
+
// Validate change name
|
|
73
|
+
if (!(0, validators_1.isValidChangeName)(change.name)) {
|
|
74
|
+
errors.push({
|
|
75
|
+
line: lineNum,
|
|
76
|
+
message: `Invalid change name: ${change.name}`
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Validate dependencies
|
|
81
|
+
for (const dep of change.dependencies) {
|
|
82
|
+
if (!(0, validators_1.isValidDependency)(dep)) {
|
|
83
|
+
errors.push({
|
|
84
|
+
line: lineNum,
|
|
85
|
+
message: `Invalid dependency reference: ${dep}`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
changes.push(change);
|
|
90
|
+
}
|
|
91
|
+
else if (trimmed) {
|
|
92
|
+
// Non-empty line that couldn't be parsed
|
|
93
|
+
errors.push({
|
|
94
|
+
line: lineNum,
|
|
95
|
+
message: 'Invalid line format'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (errors.length > 0) {
|
|
100
|
+
return { errors };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
data: { package: project, uri, changes, tags },
|
|
104
|
+
errors: []
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse a single change line from a plan file
|
|
109
|
+
* Format: change_name [deps] timestamp planner <email> # comment
|
|
110
|
+
*/
|
|
111
|
+
function parseChangeLine(line) {
|
|
112
|
+
// More flexible regex that handles various formats, including planner names with spaces
|
|
113
|
+
// Format: change_name [deps] timestamp planner <email> # comment
|
|
114
|
+
// The timestamp is required if planner/email/comment are present
|
|
115
|
+
const regex = /^(\S+)(?:\s+\[([^\]]*)\])?(?:\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)(?:\s+([^<]+?))?(?:\s+<([^>]+)>)?(?:\s+#\s+(.*))?)?$/;
|
|
116
|
+
const match = line.match(regex);
|
|
117
|
+
if (!match) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const [, name, depsStr, timestamp, planner, email, comment] = match;
|
|
121
|
+
// Parse dependencies
|
|
122
|
+
const dependencies = depsStr
|
|
123
|
+
? depsStr.split(/\s+/).filter(dep => dep.length > 0)
|
|
124
|
+
: [];
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
dependencies,
|
|
128
|
+
timestamp,
|
|
129
|
+
planner,
|
|
130
|
+
email,
|
|
131
|
+
comment
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse a tag line from a plan file
|
|
136
|
+
* Format: @tag_name change_name timestamp planner <email> # comment
|
|
137
|
+
*/
|
|
138
|
+
function parseTagLine(line, lastChangeName) {
|
|
139
|
+
// Tag lines start with @
|
|
140
|
+
if (!line.startsWith('@')) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
// Remove the @ and parse
|
|
144
|
+
const tagContent = line.substring(1);
|
|
145
|
+
// Two possible formats:
|
|
146
|
+
// 1. tagname timestamp planner <email> # comment (tag for last change)
|
|
147
|
+
// 2. tagname changename timestamp planner <email> # comment (tag for specific change)
|
|
148
|
+
// First try to match with change name (format 2)
|
|
149
|
+
const regexWithChange = /^(\S+)\s+(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
|
|
150
|
+
let match = tagContent.match(regexWithChange);
|
|
151
|
+
if (match) {
|
|
152
|
+
const [, name, secondToken, timestamp, planner, email, comment] = match;
|
|
153
|
+
// Check if the second token is a timestamp (format 1) or change name (format 2)
|
|
154
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(secondToken)) {
|
|
155
|
+
// Format 1: no change name specified
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
change: lastChangeName || '', // Tag is associated with the last change
|
|
159
|
+
timestamp: secondToken,
|
|
160
|
+
planner: timestamp, // What we thought was timestamp is actually planner
|
|
161
|
+
email: planner, // Shift everything
|
|
162
|
+
comment: email || comment
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Format 2: explicit change name
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
change: secondToken,
|
|
170
|
+
timestamp,
|
|
171
|
+
planner,
|
|
172
|
+
email,
|
|
173
|
+
comment
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Try simple format without change name
|
|
178
|
+
const regexSimple = /^(\S+)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)\s+(.+?)\s+<([^>]+)>(?:\s+#\s+(.*))?$/;
|
|
179
|
+
match = tagContent.match(regexSimple);
|
|
180
|
+
if (!match) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const [, name, timestamp, planner, email, comment] = match;
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
change: lastChangeName || '', // Tag is associated with the last change
|
|
187
|
+
timestamp,
|
|
188
|
+
planner,
|
|
189
|
+
email,
|
|
190
|
+
comment
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolve a reference within a plan file context
|
|
195
|
+
* Handles symbolic references (HEAD, ROOT) and relative references
|
|
196
|
+
*/
|
|
197
|
+
function resolveReference(ref, plan, currentPackage) {
|
|
198
|
+
const parsed = (0, validators_1.parseReference)(ref);
|
|
199
|
+
if (!parsed) {
|
|
200
|
+
return { error: `Invalid reference: ${ref}` };
|
|
201
|
+
}
|
|
202
|
+
// Handle package qualifier
|
|
203
|
+
const planPackage = currentPackage || plan.package;
|
|
204
|
+
if (parsed.package && parsed.package !== planPackage) {
|
|
205
|
+
// Cross-package reference - return as-is
|
|
206
|
+
return { change: ref };
|
|
207
|
+
}
|
|
208
|
+
// Handle SHA1 references
|
|
209
|
+
if (parsed.sha1) {
|
|
210
|
+
// Would need to look up in database
|
|
211
|
+
return { change: ref };
|
|
212
|
+
}
|
|
213
|
+
// Handle symbolic references
|
|
214
|
+
if (parsed.symbolic === 'HEAD' && plan.changes.length > 0) {
|
|
215
|
+
return { change: plan.changes[plan.changes.length - 1].name };
|
|
216
|
+
}
|
|
217
|
+
if (parsed.symbolic === 'ROOT' && plan.changes.length > 0) {
|
|
218
|
+
return { change: plan.changes[0].name };
|
|
219
|
+
}
|
|
220
|
+
// Handle relative references
|
|
221
|
+
if (parsed.relative) {
|
|
222
|
+
const baseResolved = resolveReference(parsed.relative.base, plan, currentPackage);
|
|
223
|
+
if (baseResolved.error) {
|
|
224
|
+
return baseResolved;
|
|
225
|
+
}
|
|
226
|
+
// Get the change name from the resolved reference
|
|
227
|
+
const baseChange = baseResolved.change;
|
|
228
|
+
if (!baseChange) {
|
|
229
|
+
return { error: `Cannot resolve base reference: ${parsed.relative.base}` };
|
|
230
|
+
}
|
|
231
|
+
const changeIndex = plan.changes.findIndex(c => c.name === baseChange);
|
|
232
|
+
if (changeIndex === -1) {
|
|
233
|
+
return { error: `Change not found: ${baseChange}` };
|
|
234
|
+
}
|
|
235
|
+
let targetIndex;
|
|
236
|
+
if (parsed.relative.direction === '^') {
|
|
237
|
+
// Go backwards
|
|
238
|
+
targetIndex = changeIndex - parsed.relative.count;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Go forwards
|
|
242
|
+
targetIndex = changeIndex + parsed.relative.count;
|
|
243
|
+
}
|
|
244
|
+
if (targetIndex < 0 || targetIndex >= plan.changes.length) {
|
|
245
|
+
return { error: `Relative reference out of bounds: ${ref}` };
|
|
246
|
+
}
|
|
247
|
+
return { change: plan.changes[targetIndex].name };
|
|
248
|
+
}
|
|
249
|
+
// Handle tag references
|
|
250
|
+
if (parsed.tag) {
|
|
251
|
+
const tag = plan.tags.find(t => t.name === parsed.tag);
|
|
252
|
+
if (!tag) {
|
|
253
|
+
return { error: `Tag not found: ${parsed.tag}` };
|
|
254
|
+
}
|
|
255
|
+
return { tag: parsed.tag, change: tag.change };
|
|
256
|
+
}
|
|
257
|
+
// Handle change@tag format
|
|
258
|
+
if (parsed.change && parsed.tag) {
|
|
259
|
+
return { change: parsed.change, tag: parsed.tag };
|
|
260
|
+
}
|
|
261
|
+
// Plain change reference
|
|
262
|
+
if (parsed.change) {
|
|
263
|
+
// Validate that the change exists in the plan
|
|
264
|
+
const changeExists = plan.changes.some(c => c.name === parsed.change);
|
|
265
|
+
if (!changeExists) {
|
|
266
|
+
return { error: `Change not found: ${parsed.change}` };
|
|
267
|
+
}
|
|
268
|
+
return { change: parsed.change };
|
|
269
|
+
}
|
|
270
|
+
return { error: `Cannot resolve reference: ${ref}` };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Simple plan file parser without validation (for backwards compatibility)
|
|
274
|
+
*/
|
|
275
|
+
function parsePlanFileSimple(planPath) {
|
|
276
|
+
const result = parsePlanFile(planPath);
|
|
277
|
+
if (result.data) {
|
|
278
|
+
// Return without tags for simple format
|
|
279
|
+
const { tags, ...planWithoutTags } = result.data;
|
|
280
|
+
return planWithoutTags;
|
|
281
|
+
}
|
|
282
|
+
// If there are errors, throw with details
|
|
283
|
+
if (result.errors && result.errors.length > 0) {
|
|
284
|
+
const errorMessages = result.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n');
|
|
285
|
+
throw types_1.errors.PLAN_PARSE_ERROR({ planPath, errors: errorMessages });
|
|
286
|
+
}
|
|
287
|
+
// Return empty plan if no data and no errors
|
|
288
|
+
return { package: '', uri: '', changes: [] };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get all change names from a plan file
|
|
292
|
+
*/
|
|
293
|
+
function getChanges(planPath) {
|
|
294
|
+
const plan = parsePlanFileSimple(planPath);
|
|
295
|
+
return plan.changes.map(change => change.name);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get the latest (last) change from a plan file
|
|
299
|
+
*/
|
|
300
|
+
function getLatestChange(planPath) {
|
|
301
|
+
const changes = getChanges(planPath);
|
|
302
|
+
return changes[changes.length - 1] || '';
|
|
303
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validators for Sqitch change and tag names according to the spec:
|
|
3
|
+
* https://sqitch.org/docs/manual/sqitchchanges/
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validate a change name according to Sqitch rules:
|
|
7
|
+
* - ≥1 character
|
|
8
|
+
* - Cannot contain whitespace
|
|
9
|
+
* - First and last characters must not be punctuation (except _)
|
|
10
|
+
* - Must not end in ~, ^, /, = or % followed by digits
|
|
11
|
+
* - Disallowed characters anywhere: :, @, #, \
|
|
12
|
+
*/
|
|
13
|
+
export declare function isValidChangeName(name: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Validate a tag name according to Sqitch rules:
|
|
16
|
+
* - Same as change name rules
|
|
17
|
+
* - Additionally, must not contain /
|
|
18
|
+
*/
|
|
19
|
+
export declare function isValidTagName(name: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Parse a reference that might include project qualifier and/or tag
|
|
22
|
+
* Examples:
|
|
23
|
+
* - "users_table"
|
|
24
|
+
* - "@v1.0"
|
|
25
|
+
* - "users_table@v1.0"
|
|
26
|
+
* - "otherproj:users_table"
|
|
27
|
+
* - "otherproj:@v1.2"
|
|
28
|
+
* - "otherproj:users_table@v1.2"
|
|
29
|
+
* - "40763784148fa190d75bad036730ef44d1c2eac6"
|
|
30
|
+
* - "project:40763784148fa190d75bad036730ef44d1c2eac6"
|
|
31
|
+
*/
|
|
32
|
+
export interface ParsedReference {
|
|
33
|
+
package?: string;
|
|
34
|
+
change?: string;
|
|
35
|
+
tag?: string;
|
|
36
|
+
sha1?: string;
|
|
37
|
+
symbolic?: 'HEAD' | 'ROOT';
|
|
38
|
+
relative?: {
|
|
39
|
+
base: string;
|
|
40
|
+
direction: '^' | '~';
|
|
41
|
+
count: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a reference string into its components
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseReference(ref: string): ParsedReference | null;
|
|
48
|
+
/**
|
|
49
|
+
* Validate a dependency reference
|
|
50
|
+
* Dependencies can be any valid reference format
|
|
51
|
+
*/
|
|
52
|
+
export declare function isValidDependency(dep: string): boolean;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validators for Sqitch change and tag names according to the spec:
|
|
4
|
+
* https://sqitch.org/docs/manual/sqitchchanges/
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isValidChangeName = isValidChangeName;
|
|
8
|
+
exports.isValidTagName = isValidTagName;
|
|
9
|
+
exports.parseReference = parseReference;
|
|
10
|
+
exports.isValidDependency = isValidDependency;
|
|
11
|
+
/**
|
|
12
|
+
* Check if a character is punctuation (excluding underscore)
|
|
13
|
+
*/
|
|
14
|
+
function isPunctuation(char) {
|
|
15
|
+
// Common punctuation marks, excluding underscore
|
|
16
|
+
const punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{|}~]/;
|
|
17
|
+
return punctuation.test(char);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate a change name according to Sqitch rules:
|
|
21
|
+
* - ≥1 character
|
|
22
|
+
* - Cannot contain whitespace
|
|
23
|
+
* - First and last characters must not be punctuation (except _)
|
|
24
|
+
* - Must not end in ~, ^, /, = or % followed by digits
|
|
25
|
+
* - Disallowed characters anywhere: :, @, #, \
|
|
26
|
+
*/
|
|
27
|
+
function isValidChangeName(name) {
|
|
28
|
+
if (!name || name.length === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
// Check for whitespace
|
|
32
|
+
if (/\s/.test(name)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// Check for disallowed characters anywhere
|
|
36
|
+
if (/[:@#\\]/.test(name)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// Check first character - must not be punctuation except underscore
|
|
40
|
+
const firstChar = name[0];
|
|
41
|
+
if (firstChar !== '_' && isPunctuation(firstChar)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Check last character - must not be punctuation except underscore
|
|
45
|
+
const lastChar = name[name.length - 1];
|
|
46
|
+
if (lastChar !== '_' && isPunctuation(lastChar)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// Check for endings like ~N, ^N, /N, =N, %N where N is digits
|
|
50
|
+
if (/[~^/=%]\d+$/.test(name)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate a tag name according to Sqitch rules:
|
|
57
|
+
* - Same as change name rules
|
|
58
|
+
* - Additionally, must not contain /
|
|
59
|
+
*/
|
|
60
|
+
function isValidTagName(name) {
|
|
61
|
+
if (!isValidChangeName(name)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Tags must not contain /
|
|
65
|
+
if (name.includes('/')) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if a string is a valid SHA1 hash (40 hex characters)
|
|
72
|
+
*/
|
|
73
|
+
function isSHA1(str) {
|
|
74
|
+
return /^[0-9a-f]{40}$/i.test(str);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse a reference string into its components
|
|
78
|
+
*/
|
|
79
|
+
function parseReference(ref) {
|
|
80
|
+
if (!ref) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const result = {};
|
|
84
|
+
// Check for package qualifier
|
|
85
|
+
let workingRef = ref;
|
|
86
|
+
const colonIndex = ref.indexOf(':');
|
|
87
|
+
if (colonIndex > 0) {
|
|
88
|
+
const projectPart = ref.substring(0, colonIndex);
|
|
89
|
+
const remainingPart = ref.substring(colonIndex + 1);
|
|
90
|
+
// Check if this is actually a package qualifier or just a colon in the name
|
|
91
|
+
// Package names should be valid identifiers (alphanumeric, dash, underscore)
|
|
92
|
+
// Also check that the remaining part doesn't have more colons
|
|
93
|
+
if (/^[a-zA-Z0-9_-]+$/.test(projectPart) &&
|
|
94
|
+
remainingPart.length > 0 &&
|
|
95
|
+
remainingPart.indexOf(':') === -1) {
|
|
96
|
+
result.package = projectPart;
|
|
97
|
+
workingRef = remainingPart;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Not a valid package qualifier, treat the whole thing as a reference
|
|
101
|
+
workingRef = ref;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Check for symbolic references
|
|
105
|
+
if (workingRef === 'HEAD' || workingRef === '@HEAD') {
|
|
106
|
+
result.symbolic = 'HEAD';
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
if (workingRef === 'ROOT' || workingRef === '@ROOT') {
|
|
110
|
+
result.symbolic = 'ROOT';
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
// Check for relative references (e.g., HEAD^, @beta~2, HEAD^^)
|
|
114
|
+
// Handle multiple ^ characters
|
|
115
|
+
const multiCaretMatch = workingRef.match(/^(.+?)(\^+)$/);
|
|
116
|
+
if (multiCaretMatch) {
|
|
117
|
+
const [, base, carets] = multiCaretMatch;
|
|
118
|
+
result.relative = {
|
|
119
|
+
base: base, // Keep the original base including @ prefix
|
|
120
|
+
direction: '^',
|
|
121
|
+
count: carets.length
|
|
122
|
+
};
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
// Handle ^N or ~N format
|
|
126
|
+
const relativeMatch = workingRef.match(/^(.+?)([~^])(\d+)$/);
|
|
127
|
+
if (relativeMatch) {
|
|
128
|
+
const [, base, direction, countStr] = relativeMatch;
|
|
129
|
+
const count = parseInt(countStr, 10);
|
|
130
|
+
result.relative = {
|
|
131
|
+
base: base, // Keep the original base including @ prefix
|
|
132
|
+
direction: direction,
|
|
133
|
+
count
|
|
134
|
+
};
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
// Handle single ^ or ~ without number
|
|
138
|
+
const singleRelativeMatch = workingRef.match(/^(.+?)([~^])$/);
|
|
139
|
+
if (singleRelativeMatch) {
|
|
140
|
+
const [, base, direction] = singleRelativeMatch;
|
|
141
|
+
result.relative = {
|
|
142
|
+
base: base, // Keep the original base including @ prefix
|
|
143
|
+
direction: direction,
|
|
144
|
+
count: 1
|
|
145
|
+
};
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
// Check if it's a SHA1
|
|
149
|
+
if (isSHA1(workingRef)) {
|
|
150
|
+
result.sha1 = workingRef;
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
// Check for tag reference (starts with @)
|
|
154
|
+
if (workingRef.startsWith('@')) {
|
|
155
|
+
const tagName = workingRef.substring(1);
|
|
156
|
+
if (isValidTagName(tagName)) {
|
|
157
|
+
result.tag = tagName;
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
return null; // Invalid tag name
|
|
161
|
+
}
|
|
162
|
+
// Check for change@tag format
|
|
163
|
+
const atIndex = workingRef.indexOf('@');
|
|
164
|
+
if (atIndex > 0) {
|
|
165
|
+
const changeName = workingRef.substring(0, atIndex);
|
|
166
|
+
const tagName = workingRef.substring(atIndex + 1);
|
|
167
|
+
if (isValidChangeName(changeName) && isValidTagName(tagName)) {
|
|
168
|
+
result.change = changeName;
|
|
169
|
+
result.tag = tagName;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
return null; // Invalid change or tag name
|
|
173
|
+
}
|
|
174
|
+
// Must be a plain change name
|
|
175
|
+
if (isValidChangeName(workingRef)) {
|
|
176
|
+
result.change = workingRef;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
return null; // Invalid reference
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validate a dependency reference
|
|
183
|
+
* Dependencies can be any valid reference format
|
|
184
|
+
*/
|
|
185
|
+
function isValidDependency(dep) {
|
|
186
|
+
return parseReference(dep) !== null;
|
|
187
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Change, SqitchRow, Tag, ExtendedPlanFile } from '../types';
|
|
2
|
+
export interface PlanWriteOptions {
|
|
3
|
+
outdir: string;
|
|
4
|
+
name: string;
|
|
5
|
+
replacer: (str: string) => string;
|
|
6
|
+
author?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Write a Sqitch plan file based on the provided rows
|
|
10
|
+
*/
|
|
11
|
+
export declare function writeSqitchPlan(rows: SqitchRow[], opts: PlanWriteOptions): void;
|
|
12
|
+
/**
|
|
13
|
+
* Write a plan file with the provided content
|
|
14
|
+
*/
|
|
15
|
+
export declare function writePlanFile(planPath: string, plan: ExtendedPlanFile): void;
|
|
16
|
+
/**
|
|
17
|
+
* Generate content for a plan file
|
|
18
|
+
*/
|
|
19
|
+
export declare function generatePlanFileContent(plan: ExtendedPlanFile): string;
|
|
20
|
+
/**
|
|
21
|
+
* Generate a line for a change in a plan file
|
|
22
|
+
*/
|
|
23
|
+
export declare function generateChangeLineContent(change: Change): string;
|
|
24
|
+
/**
|
|
25
|
+
* Generate a line for a tag in a plan file
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateTagLineContent(tag: Tag): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeSqitchPlan = writeSqitchPlan;
|
|
7
|
+
exports.writePlanFile = writePlanFile;
|
|
8
|
+
exports.generatePlanFileContent = generatePlanFileContent;
|
|
9
|
+
exports.generateChangeLineContent = generateChangeLineContent;
|
|
10
|
+
exports.generateTagLineContent = generateTagLineContent;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
/**
|
|
14
|
+
* Write a Sqitch plan file based on the provided rows
|
|
15
|
+
*/
|
|
16
|
+
function writeSqitchPlan(rows, opts) {
|
|
17
|
+
const dir = path_1.default.resolve(path_1.default.join(opts.outdir, opts.name));
|
|
18
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
19
|
+
const date = () => '2017-08-11T08:11:51Z'; // stubbed timestamp
|
|
20
|
+
const author = opts.author || 'launchql';
|
|
21
|
+
const email = `${author}@5b0c196eeb62`;
|
|
22
|
+
const duplicates = {};
|
|
23
|
+
const plan = opts.replacer(`%syntax-version=1.0.0
|
|
24
|
+
%project=launchql-extension-name
|
|
25
|
+
%uri=launchql-extension-name
|
|
26
|
+
|
|
27
|
+
${rows
|
|
28
|
+
.map((row) => {
|
|
29
|
+
if (duplicates[row.deploy]) {
|
|
30
|
+
console.log('DUPLICATE ' + row.deploy);
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
duplicates[row.deploy] = true;
|
|
34
|
+
if (row.deps?.length) {
|
|
35
|
+
return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${author} <${email}> # add ${row.name}`;
|
|
36
|
+
}
|
|
37
|
+
return `${row.deploy} ${date()} ${author} <${email}> # add ${row.name}`;
|
|
38
|
+
})
|
|
39
|
+
.join('\n')}
|
|
40
|
+
`);
|
|
41
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'pgpm.plan'), plan);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Write a plan file with the provided content
|
|
45
|
+
*/
|
|
46
|
+
function writePlanFile(planPath, plan) {
|
|
47
|
+
const content = generatePlanFileContent(plan);
|
|
48
|
+
fs_1.default.writeFileSync(planPath, content);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Generate content for a plan file
|
|
52
|
+
*/
|
|
53
|
+
function generatePlanFileContent(plan) {
|
|
54
|
+
const { package: packageName, uri, changes, tags } = plan;
|
|
55
|
+
let content = `%syntax-version=1.0.0\n`;
|
|
56
|
+
content += `%project=${packageName}\n`;
|
|
57
|
+
if (uri) {
|
|
58
|
+
content += `%uri=${uri}\n`;
|
|
59
|
+
}
|
|
60
|
+
content += `\n`;
|
|
61
|
+
// Add changes and their associated tags
|
|
62
|
+
for (const change of changes) {
|
|
63
|
+
content += generateChangeLineContent(change);
|
|
64
|
+
content += `\n`;
|
|
65
|
+
const associatedTags = tags.filter(tag => tag.change === change.name);
|
|
66
|
+
for (const tag of associatedTags) {
|
|
67
|
+
content += generateTagLineContent(tag);
|
|
68
|
+
content += `\n`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return content;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate a line for a change in a plan file
|
|
75
|
+
*/
|
|
76
|
+
function generateChangeLineContent(change) {
|
|
77
|
+
const { name, dependencies, timestamp, planner, email, comment } = change;
|
|
78
|
+
let line = name;
|
|
79
|
+
// Add dependencies if present
|
|
80
|
+
if (dependencies && dependencies.length > 0) {
|
|
81
|
+
line += ` [${dependencies.join(' ')}]`;
|
|
82
|
+
}
|
|
83
|
+
// Add timestamp if present
|
|
84
|
+
if (timestamp) {
|
|
85
|
+
line += ` ${timestamp}`;
|
|
86
|
+
// Add planner if present
|
|
87
|
+
if (planner) {
|
|
88
|
+
line += ` ${planner}`;
|
|
89
|
+
// Add email if present
|
|
90
|
+
if (email) {
|
|
91
|
+
line += ` <${email}>`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Add comment if present
|
|
96
|
+
if (comment) {
|
|
97
|
+
line += ` # ${comment}`;
|
|
98
|
+
}
|
|
99
|
+
return line;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generate a line for a tag in a plan file
|
|
103
|
+
*/
|
|
104
|
+
function generateTagLineContent(tag) {
|
|
105
|
+
const { name, timestamp, planner, email, comment } = tag;
|
|
106
|
+
let line = `@${name}`;
|
|
107
|
+
// Add timestamp if present
|
|
108
|
+
if (timestamp) {
|
|
109
|
+
line += ` ${timestamp}`;
|
|
110
|
+
// Add planner if present
|
|
111
|
+
if (planner) {
|
|
112
|
+
line += ` ${planner}`;
|
|
113
|
+
// Add email if present
|
|
114
|
+
if (email) {
|
|
115
|
+
line += ` <${email}>`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Add comment if present
|
|
120
|
+
if (comment) {
|
|
121
|
+
line += ` # ${comment}`;
|
|
122
|
+
}
|
|
123
|
+
return line;
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './writer';
|