@boject/cli 0.0.1-rc.1
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 +95 -0
- package/README.md +352 -0
- package/dist/index.js +6122 -0
- package/dist/perf/index.js +1023 -0
- package/dist/vendor/contentBundleTypes.ts +115 -0
- package/dist/vendor/contentStatus.ts +30 -0
- package/dist/vendor/fieldTypes.ts +57 -0
- package/dist/vendor/perf/lib/auth-k6.ts +14 -0
- package/dist/vendor/perf/lib/config-k6.ts +21 -0
- package/dist/vendor/perf/lib/metrics-k6.ts +9 -0
- package/dist/vendor/perf/lib/pg-sampler.ts +236 -0
- package/dist/vendor/perf/scenarios/graphql-flat.ts +97 -0
- package/dist/vendor/perf/scenarios/graphql-sitemap.ts +99 -0
- package/dist/vendor/perf/scenarios/rest-crud-cycle.ts +148 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/perf/prng.ts
|
|
4
|
+
function rng(seed) {
|
|
5
|
+
let state = seed | 0;
|
|
6
|
+
if (state === 0) state = 1;
|
|
7
|
+
return () => {
|
|
8
|
+
state ^= state << 13;
|
|
9
|
+
state ^= state >>> 17;
|
|
10
|
+
state ^= state << 5;
|
|
11
|
+
return (state >>> 0) / 4294967296;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function pickN(arr, n, rand) {
|
|
15
|
+
if (n > 0 && arr.length === 0)
|
|
16
|
+
throw new Error("pickN: cannot pick from empty array");
|
|
17
|
+
const out = [];
|
|
18
|
+
for (let i = 0; i < n; i++) {
|
|
19
|
+
out.push(arr[Math.floor(rand() * arr.length)]);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function pickOne(arr, rand) {
|
|
24
|
+
if (arr.length === 0) throw new Error("pickOne: array must not be empty");
|
|
25
|
+
return arr[Math.floor(rand() * arr.length)];
|
|
26
|
+
}
|
|
27
|
+
function sampleWithoutReplacement(arr, n, rand) {
|
|
28
|
+
if (n >= arr.length) return [...arr];
|
|
29
|
+
const pool = [...arr];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (let i = 0; i < n; i++) {
|
|
32
|
+
const idx = Math.floor(rand() * pool.length);
|
|
33
|
+
out.push(pool[idx]);
|
|
34
|
+
pool.splice(idx, 1);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function intInRange(min, max, rand) {
|
|
39
|
+
return Math.floor(rand() * (max - min + 1)) + min;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/perf/topoSort.ts
|
|
43
|
+
var CycleRequiresNullError = class extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* The set of nodes that remained when the algorithm got stuck.
|
|
46
|
+
* This is a *superset* of the actual cycle — it includes any node
|
|
47
|
+
* that was waiting on the cycle, transitively. The `requiredEdges`
|
|
48
|
+
* field is the precise set of edges that prevented further progress.
|
|
49
|
+
*/
|
|
50
|
+
residual;
|
|
51
|
+
requiredEdges;
|
|
52
|
+
constructor(residual, requiredEdges) {
|
|
53
|
+
super(
|
|
54
|
+
`Stuck on residual nodes [${residual.join(", ")}]: cycle through them has only required edges; cannot defer. Make at least one edge optional. Required edges: ${requiredEdges.map((e) => `${e.from}.${e.field} \u2192 ${e.to}`).join(", ")}`
|
|
55
|
+
);
|
|
56
|
+
this.name = "CycleRequiresNullError";
|
|
57
|
+
this.residual = residual;
|
|
58
|
+
this.requiredEdges = requiredEdges;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function topoSort(identifiers, edges) {
|
|
62
|
+
const remainingEdges = [...edges];
|
|
63
|
+
const order = [];
|
|
64
|
+
const deferredEdges = [];
|
|
65
|
+
const remainingNodes = new Set(identifiers);
|
|
66
|
+
while (remainingNodes.size > 0) {
|
|
67
|
+
const ready = [...remainingNodes].find(
|
|
68
|
+
(n) => !remainingEdges.some((e) => e.from === n && remainingNodes.has(e.to))
|
|
69
|
+
);
|
|
70
|
+
if (ready) {
|
|
71
|
+
order.push(ready);
|
|
72
|
+
remainingNodes.delete(ready);
|
|
73
|
+
for (let i = remainingEdges.length - 1; i >= 0; i--) {
|
|
74
|
+
if (remainingEdges[i].from === ready) remainingEdges.splice(i, 1);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const cycleEdges = remainingEdges.filter(
|
|
79
|
+
(e) => remainingNodes.has(e.from) && remainingNodes.has(e.to)
|
|
80
|
+
);
|
|
81
|
+
const optional = cycleEdges.find((e) => !e.required);
|
|
82
|
+
if (!optional) {
|
|
83
|
+
throw new CycleRequiresNullError(
|
|
84
|
+
[...remainingNodes],
|
|
85
|
+
cycleEdges.filter((e) => e.required)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
deferredEdges.push(optional);
|
|
89
|
+
const idx = remainingEdges.indexOf(optional);
|
|
90
|
+
remainingEdges.splice(idx, 1);
|
|
91
|
+
}
|
|
92
|
+
return { order, deferredEdges };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/perf/valueGen/lorem.ts
|
|
96
|
+
var LOREM = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium totam rem aperiam".split(" ");
|
|
97
|
+
|
|
98
|
+
// src/perf/valueGen/scalars.ts
|
|
99
|
+
function titleCase(s) {
|
|
100
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
101
|
+
}
|
|
102
|
+
function generateEntryTitle(opts) {
|
|
103
|
+
const wordCount = intInRange(3, 6, opts.rand);
|
|
104
|
+
const words = pickN(LOREM, wordCount, opts.rand).map(titleCase).join(" ");
|
|
105
|
+
return `${words} #${opts.index}`;
|
|
106
|
+
}
|
|
107
|
+
function generateSlug(opts) {
|
|
108
|
+
const base = opts.entryTitle.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
109
|
+
return `${base}-${opts.index}`;
|
|
110
|
+
}
|
|
111
|
+
function generateText(opts) {
|
|
112
|
+
const value = pickN(LOREM, intInRange(4, 10, opts.rand), opts.rand).join(" ");
|
|
113
|
+
if (opts.unique && opts.seenValues) {
|
|
114
|
+
if (opts.seenValues.has(value)) {
|
|
115
|
+
const deduplicated = `${value}-${opts.index}`;
|
|
116
|
+
opts.seenValues.add(deduplicated);
|
|
117
|
+
return deduplicated;
|
|
118
|
+
}
|
|
119
|
+
opts.seenValues.add(value);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
function generateTextarea(opts) {
|
|
124
|
+
const paragraphCount = intInRange(1, 3, opts.rand);
|
|
125
|
+
const paragraphs = [];
|
|
126
|
+
for (let i = 0; i < paragraphCount; i++) {
|
|
127
|
+
paragraphs.push(
|
|
128
|
+
pickN(LOREM, intInRange(40, 80, opts.rand), opts.rand).join(" ")
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return paragraphs.join("\n\n");
|
|
132
|
+
}
|
|
133
|
+
function generateNumber(opts) {
|
|
134
|
+
if (opts.unique) {
|
|
135
|
+
return opts.index * 1e5 + intInRange(0, 99999, opts.rand);
|
|
136
|
+
}
|
|
137
|
+
return intInRange(0, 999999, opts.rand);
|
|
138
|
+
}
|
|
139
|
+
function generateBoolean(opts) {
|
|
140
|
+
return opts.rand() < 0.5;
|
|
141
|
+
}
|
|
142
|
+
function generateDatetime(opts) {
|
|
143
|
+
const span = opts.window.to.getTime() - opts.window.from.getTime();
|
|
144
|
+
const ts = opts.window.from.getTime() + Math.floor(opts.rand() * span);
|
|
145
|
+
return new Date(ts).toISOString();
|
|
146
|
+
}
|
|
147
|
+
function generateSelect(opts) {
|
|
148
|
+
if (opts.choices.length === 0) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"SELECT field requires options.choices \u2014 refusing to synthesise an empty value"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return pickOne(opts.choices, opts.rand);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/perf/valueGen/richtext.ts
|
|
157
|
+
var LINK_INSERTION_PROBABILITY = 0.4;
|
|
158
|
+
var MAX_EMBEDS_PER_DOC = 2;
|
|
159
|
+
function paragraph(rand, linkPool) {
|
|
160
|
+
const wordCount = intInRange(70, 110, rand);
|
|
161
|
+
const text = pickN(LOREM, wordCount, rand).join(" ");
|
|
162
|
+
const usableLinkPool = linkPool.filter((p) => p.entryIds.length > 0);
|
|
163
|
+
const words = text.split(" ");
|
|
164
|
+
const canInsertLink = usableLinkPool.length > 0 && words.length >= 13 && rand() < LINK_INSERTION_PROBABILITY;
|
|
165
|
+
if (!canInsertLink) {
|
|
166
|
+
return {
|
|
167
|
+
type: "paragraph",
|
|
168
|
+
content: [{ type: "text", text }]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const linkStart = intInRange(2, words.length - 10, rand);
|
|
172
|
+
const linkLen = intInRange(3, 7, rand);
|
|
173
|
+
const prefix = words.slice(0, linkStart).join(" ");
|
|
174
|
+
const linked = words.slice(linkStart, linkStart + linkLen).join(" ");
|
|
175
|
+
const suffix = words.slice(linkStart + linkLen).join(" ");
|
|
176
|
+
const target = pickOne(usableLinkPool, rand);
|
|
177
|
+
const mark = {
|
|
178
|
+
type: "cmsLink",
|
|
179
|
+
attrs: {
|
|
180
|
+
contentTypeId: target.contentTypeId,
|
|
181
|
+
contentTypeIdentifier: target.contentTypeIdentifier,
|
|
182
|
+
entryId: pickOne(target.entryIds, rand)
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
type: "paragraph",
|
|
187
|
+
content: [
|
|
188
|
+
{ type: "text", text: prefix + " " },
|
|
189
|
+
{ type: "text", text: linked, marks: [mark] },
|
|
190
|
+
{ type: "text", text: " " + suffix }
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function heading(level, rand) {
|
|
195
|
+
return {
|
|
196
|
+
type: "heading",
|
|
197
|
+
attrs: { level },
|
|
198
|
+
content: [{ type: "text", text: pickN(LOREM, 5, rand).join(" ") }]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function bulletList(rand) {
|
|
202
|
+
const items = [];
|
|
203
|
+
const count = intInRange(3, 5, rand);
|
|
204
|
+
for (let i = 0; i < count; i++) {
|
|
205
|
+
items.push({
|
|
206
|
+
type: "listItem",
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "paragraph",
|
|
210
|
+
content: [{ type: "text", text: pickN(LOREM, 8, rand).join(" ") }]
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return { type: "bulletList", content: items };
|
|
216
|
+
}
|
|
217
|
+
function embedNode(rand, pool) {
|
|
218
|
+
const usable = pool.filter((p) => p.entryIds.length > 0);
|
|
219
|
+
if (usable.length === 0) return null;
|
|
220
|
+
const target = pickOne(usable, rand);
|
|
221
|
+
return {
|
|
222
|
+
type: "cmsEmbed",
|
|
223
|
+
attrs: {
|
|
224
|
+
contentTypeId: target.contentTypeId,
|
|
225
|
+
contentTypeIdentifier: target.contentTypeIdentifier,
|
|
226
|
+
entryId: pickOne(target.entryIds, rand)
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function generateRichtext(opts) {
|
|
231
|
+
const { rand, refPool } = opts;
|
|
232
|
+
const linkPool = refPool?.link ?? [];
|
|
233
|
+
const embedPool = refPool?.embed ?? [];
|
|
234
|
+
const content = [
|
|
235
|
+
heading(1, rand),
|
|
236
|
+
paragraph(rand, linkPool),
|
|
237
|
+
paragraph(rand, linkPool)
|
|
238
|
+
];
|
|
239
|
+
const embedCount = intInRange(0, MAX_EMBEDS_PER_DOC, rand);
|
|
240
|
+
for (let i = 0; i < embedCount; i++) {
|
|
241
|
+
const node = embedNode(rand, embedPool);
|
|
242
|
+
if (node) content.push(node);
|
|
243
|
+
}
|
|
244
|
+
content.push(
|
|
245
|
+
heading(2, rand),
|
|
246
|
+
paragraph(rand, linkPool),
|
|
247
|
+
bulletList(rand),
|
|
248
|
+
paragraph(rand, linkPool),
|
|
249
|
+
heading(2, rand),
|
|
250
|
+
paragraph(rand, linkPool)
|
|
251
|
+
);
|
|
252
|
+
return { type: "doc", content };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/perf/valueGen/relations.ts
|
|
256
|
+
function generateRelation(opts) {
|
|
257
|
+
const usable = opts.pool.filter((p) => p.entryIds.length > 0);
|
|
258
|
+
if (usable.length === 0) return null;
|
|
259
|
+
const target = pickOne(usable, opts.rand);
|
|
260
|
+
return {
|
|
261
|
+
contentTypeId: target.contentTypeId,
|
|
262
|
+
contentTypeIdentifier: target.contentTypeIdentifier,
|
|
263
|
+
entryId: pickOne(target.entryIds, opts.rand)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function generateMultirelation(opts) {
|
|
267
|
+
const usable = opts.pool.filter((p) => p.entryIds.length > 0);
|
|
268
|
+
if (usable.length === 0) return [];
|
|
269
|
+
const totalAvailable = usable.reduce((s, p) => s + p.entryIds.length, 0);
|
|
270
|
+
const cap = Math.min(opts.fanout.max, totalAvailable);
|
|
271
|
+
if (cap < opts.fanout.min) return [];
|
|
272
|
+
const n = intInRange(opts.fanout.min, cap, opts.rand);
|
|
273
|
+
if (n === 0) return [];
|
|
274
|
+
const flat = [];
|
|
275
|
+
for (const p of usable) {
|
|
276
|
+
for (const eid of p.entryIds) {
|
|
277
|
+
flat.push({
|
|
278
|
+
contentTypeId: p.contentTypeId,
|
|
279
|
+
contentTypeIdentifier: p.contentTypeIdentifier,
|
|
280
|
+
entryId: eid
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return sampleWithoutReplacement(flat, n, opts.rand);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/perf/valueGen/image.ts
|
|
288
|
+
function nextUuid(rand) {
|
|
289
|
+
const hex = (n, width) => Math.floor(rand() * Math.pow(16, n)).toString(16).padStart(width, "0");
|
|
290
|
+
const segments = [
|
|
291
|
+
hex(8, 8),
|
|
292
|
+
hex(4, 4),
|
|
293
|
+
// Force version-4 nibble in the third group.
|
|
294
|
+
"4" + hex(3, 3),
|
|
295
|
+
// Force variant nibble (8/9/a/b) in the fourth group.
|
|
296
|
+
(Math.floor(rand() * 4) + 8).toString(16) + hex(3, 3),
|
|
297
|
+
hex(8, 8) + hex(4, 4)
|
|
298
|
+
];
|
|
299
|
+
return segments.join("-");
|
|
300
|
+
}
|
|
301
|
+
function generateImage(opts) {
|
|
302
|
+
const { rand, index } = opts;
|
|
303
|
+
const storageKey = nextUuid(rand);
|
|
304
|
+
const width = intInRange(800, 3e3, rand);
|
|
305
|
+
const height = intInRange(800, 3e3, rand);
|
|
306
|
+
return {
|
|
307
|
+
storageKey,
|
|
308
|
+
mimeType: "image/jpeg",
|
|
309
|
+
width,
|
|
310
|
+
height,
|
|
311
|
+
fileSize: width * height * 3,
|
|
312
|
+
originalName: `image-${index}.jpg`,
|
|
313
|
+
focalPointX: 0.5,
|
|
314
|
+
focalPointY: 0.5
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/vendor/slugify.ts
|
|
319
|
+
function slugify(text) {
|
|
320
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/vendor/fieldTypes.ts
|
|
324
|
+
var FIELD_TYPES = {
|
|
325
|
+
ENTRY_TITLE: "ENTRY_TITLE",
|
|
326
|
+
SLUG: "SLUG",
|
|
327
|
+
TEXT: "TEXT",
|
|
328
|
+
TEXTAREA: "TEXTAREA",
|
|
329
|
+
NUMBER: "NUMBER",
|
|
330
|
+
BOOLEAN: "BOOLEAN",
|
|
331
|
+
DATETIME: "DATETIME",
|
|
332
|
+
SELECT: "SELECT",
|
|
333
|
+
RICHTEXT: "RICHTEXT",
|
|
334
|
+
RELATION: "RELATION",
|
|
335
|
+
MULTIRELATION: "MULTIRELATION",
|
|
336
|
+
IMAGE: "IMAGE"
|
|
337
|
+
};
|
|
338
|
+
var FIELD_TYPE_NAMES = Object.values(FIELD_TYPES);
|
|
339
|
+
var FIELD_TYPES_SET = new Set(
|
|
340
|
+
FIELD_TYPE_NAMES
|
|
341
|
+
);
|
|
342
|
+
var FIELD_TYPE_LABELS = {
|
|
343
|
+
ENTRY_TITLE: "Entry Title",
|
|
344
|
+
SLUG: "Slug",
|
|
345
|
+
TEXT: "Text",
|
|
346
|
+
TEXTAREA: "Textarea",
|
|
347
|
+
NUMBER: "Number",
|
|
348
|
+
BOOLEAN: "Boolean",
|
|
349
|
+
DATETIME: "Date/Time",
|
|
350
|
+
SELECT: "Select",
|
|
351
|
+
RICHTEXT: "Rich Text",
|
|
352
|
+
RELATION: "Relation",
|
|
353
|
+
MULTIRELATION: "Multi Relation",
|
|
354
|
+
IMAGE: "Image"
|
|
355
|
+
};
|
|
356
|
+
var FIELD_TYPE_OPTIONS = FIELD_TYPE_NAMES.map((value) => ({
|
|
357
|
+
label: FIELD_TYPE_LABELS[value],
|
|
358
|
+
value
|
|
359
|
+
}));
|
|
360
|
+
|
|
361
|
+
// src/vendor/contentStatus.ts
|
|
362
|
+
var CONTENT_STATUSES = {
|
|
363
|
+
DRAFT: "DRAFT",
|
|
364
|
+
PUBLISHED: "PUBLISHED",
|
|
365
|
+
CHANGED: "CHANGED",
|
|
366
|
+
ARCHIVED: "ARCHIVED"
|
|
367
|
+
};
|
|
368
|
+
var CONTENT_STATUS_NAMES = Object.values(CONTENT_STATUSES);
|
|
369
|
+
var CONTENT_STATUSES_SET = new Set(
|
|
370
|
+
CONTENT_STATUS_NAMES
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// src/perf/generate.ts
|
|
374
|
+
var DEFAULT_FANOUT = { min: 0, max: 5 };
|
|
375
|
+
var DEFAULT_TARGET_CAP = 200;
|
|
376
|
+
function nextUuid2(rand) {
|
|
377
|
+
const hex = (n, width) => Math.floor(rand() * Math.pow(16, n)).toString(16).padStart(width, "0");
|
|
378
|
+
const segments = [
|
|
379
|
+
hex(8, 8),
|
|
380
|
+
hex(4, 4),
|
|
381
|
+
// Force version-4 nibble in the third group.
|
|
382
|
+
"4" + hex(3, 3),
|
|
383
|
+
// Force variant nibble (8/9/a/b) in the fourth group.
|
|
384
|
+
(Math.floor(rand() * 4) + 8).toString(16) + hex(3, 3),
|
|
385
|
+
hex(8, 8) + hex(4, 4)
|
|
386
|
+
];
|
|
387
|
+
return segments.join("-");
|
|
388
|
+
}
|
|
389
|
+
function generatePerfData(bundle, options) {
|
|
390
|
+
const types = bundle.contentTypes ?? [];
|
|
391
|
+
const target = types.find(
|
|
392
|
+
(t) => t.identifier === options.contentTypeIdentifier
|
|
393
|
+
);
|
|
394
|
+
if (!target) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Unknown content type "${options.contentTypeIdentifier}". Available: ${types.map((t) => t.identifier).join(", ") || "(none)"}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const window = options.datetimeWindow ?? defaultWindow();
|
|
400
|
+
const fanout = options.multirelationFanout ?? DEFAULT_FANOUT;
|
|
401
|
+
const seed = options.seed ?? 1;
|
|
402
|
+
const warnings = [];
|
|
403
|
+
const publishedAtIso = new Date(window.from.getTime()).toISOString();
|
|
404
|
+
const identifierToType = new Map(
|
|
405
|
+
types.map((t) => [t.identifier, t])
|
|
406
|
+
);
|
|
407
|
+
const idToIdentifier = buildIdToIdentifierMap(types);
|
|
408
|
+
const reachable = collectReachable(target, identifierToType, idToIdentifier);
|
|
409
|
+
const edges = buildEdges(reachable, identifierToType, idToIdentifier);
|
|
410
|
+
const sorted = topoSort(
|
|
411
|
+
reachable.map((t) => t.identifier),
|
|
412
|
+
edges
|
|
413
|
+
);
|
|
414
|
+
const sizeFor = (id) => {
|
|
415
|
+
if (id === options.contentTypeIdentifier) return options.count;
|
|
416
|
+
if (options.targetCount) return options.targetCount(id);
|
|
417
|
+
return Math.min(options.count, DEFAULT_TARGET_CAP);
|
|
418
|
+
};
|
|
419
|
+
const idPools = /* @__PURE__ */ new Map();
|
|
420
|
+
const groups = [];
|
|
421
|
+
const rand = rng(seed);
|
|
422
|
+
const deferredByType = /* @__PURE__ */ new Map();
|
|
423
|
+
for (const e of sorted.deferredEdges) {
|
|
424
|
+
const list = deferredByType.get(e.from) ?? [];
|
|
425
|
+
list.push(e);
|
|
426
|
+
deferredByType.set(e.from, list);
|
|
427
|
+
}
|
|
428
|
+
for (const identifier of sorted.order) {
|
|
429
|
+
const ct = identifierToType.get(identifier);
|
|
430
|
+
const count = sizeFor(identifier);
|
|
431
|
+
const deferred = deferredByType.get(identifier) ?? [];
|
|
432
|
+
const deferredFieldIds = new Set(deferred.map((e) => e.field));
|
|
433
|
+
const fieldByIdentifier = new Map(ct.fields.map((f) => [f.identifier, f]));
|
|
434
|
+
const entries = [];
|
|
435
|
+
const patches = [];
|
|
436
|
+
const uniqueTrackers = /* @__PURE__ */ new Map();
|
|
437
|
+
const slugField = ct.fields.find((f) => f.type === FIELD_TYPES.SLUG);
|
|
438
|
+
for (let i = 0; i < count; i++) {
|
|
439
|
+
const entryId = nextUuid2(rand);
|
|
440
|
+
const data = {};
|
|
441
|
+
let entryTitle = "";
|
|
442
|
+
for (const field of ct.fields) {
|
|
443
|
+
if (deferredFieldIds.has(field.identifier)) continue;
|
|
444
|
+
if (field.type === FIELD_TYPES.SLUG) continue;
|
|
445
|
+
const value = generateFieldValue({
|
|
446
|
+
field,
|
|
447
|
+
rand,
|
|
448
|
+
index: i,
|
|
449
|
+
window,
|
|
450
|
+
fanout,
|
|
451
|
+
uniqueTrackers,
|
|
452
|
+
idPools,
|
|
453
|
+
idToIdentifier,
|
|
454
|
+
warnings,
|
|
455
|
+
contentTypeIdentifier: ct.identifier
|
|
456
|
+
});
|
|
457
|
+
if (value === void 0) continue;
|
|
458
|
+
data[field.identifier] = value;
|
|
459
|
+
if (field.type === FIELD_TYPES.ENTRY_TITLE)
|
|
460
|
+
entryTitle = value;
|
|
461
|
+
}
|
|
462
|
+
if (slugField && !deferredFieldIds.has(slugField.identifier)) {
|
|
463
|
+
const slugValue = generateSlug({
|
|
464
|
+
entryTitle: entryTitle || `entry-${i}`,
|
|
465
|
+
index: i
|
|
466
|
+
});
|
|
467
|
+
data[slugField.identifier] = slugValue;
|
|
468
|
+
}
|
|
469
|
+
const slugForEnvelope = slugField ? data[slugField.identifier] : null;
|
|
470
|
+
entries.push({
|
|
471
|
+
id: entryId,
|
|
472
|
+
contentTypeId: ct.id,
|
|
473
|
+
contentTypeIdentifier: ct.identifier,
|
|
474
|
+
entryTitle,
|
|
475
|
+
entryKey: slugify(entryTitle),
|
|
476
|
+
slug: slugForEnvelope ?? null,
|
|
477
|
+
versions: [
|
|
478
|
+
{
|
|
479
|
+
status: CONTENT_STATUSES.PUBLISHED,
|
|
480
|
+
data,
|
|
481
|
+
publishedAt: publishedAtIso
|
|
482
|
+
}
|
|
483
|
+
]
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
idPools.set(ct.identifier, {
|
|
487
|
+
contentTypeId: ct.identifier,
|
|
488
|
+
contentTypeIdentifier: ct.identifier,
|
|
489
|
+
entryIds: entries.map((e) => e.id)
|
|
490
|
+
});
|
|
491
|
+
for (let i = 0; i < entries.length; i++) {
|
|
492
|
+
const entry = entries[i];
|
|
493
|
+
const fieldUpdates = {};
|
|
494
|
+
for (const edge of deferred) {
|
|
495
|
+
const field = fieldByIdentifier.get(edge.field);
|
|
496
|
+
if (!field) continue;
|
|
497
|
+
const value = generateFieldValue({
|
|
498
|
+
field,
|
|
499
|
+
rand,
|
|
500
|
+
index: i,
|
|
501
|
+
window,
|
|
502
|
+
fanout,
|
|
503
|
+
uniqueTrackers,
|
|
504
|
+
idPools,
|
|
505
|
+
idToIdentifier,
|
|
506
|
+
warnings,
|
|
507
|
+
contentTypeIdentifier: ct.identifier
|
|
508
|
+
});
|
|
509
|
+
if (value === void 0) continue;
|
|
510
|
+
fieldUpdates[field.identifier] = value;
|
|
511
|
+
}
|
|
512
|
+
if (Object.keys(fieldUpdates).length > 0) {
|
|
513
|
+
patches.push({ entryId: entry.id, fieldUpdates });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
groups.push({
|
|
517
|
+
contentTypeIdentifier: ct.identifier,
|
|
518
|
+
entries,
|
|
519
|
+
patches: patches.length > 0 ? patches : void 0
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
return { groups, warnings };
|
|
523
|
+
}
|
|
524
|
+
function buildIdToIdentifierMap(types) {
|
|
525
|
+
const out = /* @__PURE__ */ new Map();
|
|
526
|
+
for (const t of types) {
|
|
527
|
+
if (t.id) out.set(t.id, t.identifier);
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
function resolveFieldTargetIdentifiers(targetIds, targetIdentifiers, idToIdentifier) {
|
|
532
|
+
if (targetIdentifiers && targetIdentifiers.length > 0) {
|
|
533
|
+
return targetIdentifiers;
|
|
534
|
+
}
|
|
535
|
+
if (!targetIds) return [];
|
|
536
|
+
const out = [];
|
|
537
|
+
for (const id of targetIds) {
|
|
538
|
+
if (typeof id !== "string") continue;
|
|
539
|
+
const ident = idToIdentifier.get(id);
|
|
540
|
+
if (ident) out.push(ident);
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function collectReachable(root, identifierToType, idToIdentifier) {
|
|
545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
546
|
+
const out = [];
|
|
547
|
+
const queue = [root];
|
|
548
|
+
while (queue.length > 0) {
|
|
549
|
+
const t = queue.shift();
|
|
550
|
+
if (seen.has(t.identifier)) continue;
|
|
551
|
+
seen.add(t.identifier);
|
|
552
|
+
out.push(t);
|
|
553
|
+
for (const f of t.fields) {
|
|
554
|
+
if (f.type !== FIELD_TYPES.RELATION && f.type !== FIELD_TYPES.MULTIRELATION)
|
|
555
|
+
continue;
|
|
556
|
+
const targets = resolveFieldTargetIdentifiers(
|
|
557
|
+
f.options?.targetContentTypeIds,
|
|
558
|
+
f.options?.targetContentTypeIdentifiers,
|
|
559
|
+
idToIdentifier
|
|
560
|
+
);
|
|
561
|
+
for (const ident of targets) {
|
|
562
|
+
const target = identifierToType.get(ident);
|
|
563
|
+
if (target && !seen.has(target.identifier)) queue.push(target);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
function buildEdges(types, identifierToType, idToIdentifier) {
|
|
570
|
+
const edges = [];
|
|
571
|
+
for (const t of types) {
|
|
572
|
+
for (const f of t.fields) {
|
|
573
|
+
if (f.type !== FIELD_TYPES.RELATION && f.type !== FIELD_TYPES.MULTIRELATION)
|
|
574
|
+
continue;
|
|
575
|
+
const targets = resolveFieldTargetIdentifiers(
|
|
576
|
+
f.options?.targetContentTypeIds,
|
|
577
|
+
f.options?.targetContentTypeIdentifiers,
|
|
578
|
+
idToIdentifier
|
|
579
|
+
);
|
|
580
|
+
for (const ident of targets) {
|
|
581
|
+
const target = identifierToType.get(ident);
|
|
582
|
+
if (!target) continue;
|
|
583
|
+
const required = f.type === FIELD_TYPES.RELATION && f.required;
|
|
584
|
+
edges.push({
|
|
585
|
+
from: t.identifier,
|
|
586
|
+
to: target.identifier,
|
|
587
|
+
field: f.identifier,
|
|
588
|
+
required
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return edges;
|
|
594
|
+
}
|
|
595
|
+
function generateFieldValue(ctx) {
|
|
596
|
+
const { field } = ctx;
|
|
597
|
+
switch (field.type) {
|
|
598
|
+
case FIELD_TYPES.ENTRY_TITLE:
|
|
599
|
+
return generateEntryTitle({ rand: ctx.rand, index: ctx.index });
|
|
600
|
+
case FIELD_TYPES.SLUG:
|
|
601
|
+
return void 0;
|
|
602
|
+
case FIELD_TYPES.TEXT: {
|
|
603
|
+
const key = `${ctx.contentTypeIdentifier}.${field.identifier}`;
|
|
604
|
+
let tracker = ctx.uniqueTrackers.get(key);
|
|
605
|
+
if (!tracker) {
|
|
606
|
+
tracker = /* @__PURE__ */ new Set();
|
|
607
|
+
ctx.uniqueTrackers.set(key, tracker);
|
|
608
|
+
}
|
|
609
|
+
return generateText({
|
|
610
|
+
rand: ctx.rand,
|
|
611
|
+
unique: field.unique === true,
|
|
612
|
+
index: ctx.index,
|
|
613
|
+
seenValues: tracker
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
case FIELD_TYPES.TEXTAREA:
|
|
617
|
+
return generateTextarea({ rand: ctx.rand });
|
|
618
|
+
case FIELD_TYPES.NUMBER:
|
|
619
|
+
return generateNumber({
|
|
620
|
+
rand: ctx.rand,
|
|
621
|
+
unique: field.unique === true,
|
|
622
|
+
index: ctx.index
|
|
623
|
+
});
|
|
624
|
+
case FIELD_TYPES.BOOLEAN:
|
|
625
|
+
return generateBoolean({ rand: ctx.rand });
|
|
626
|
+
case FIELD_TYPES.DATETIME:
|
|
627
|
+
return generateDatetime({ rand: ctx.rand, window: ctx.window });
|
|
628
|
+
case FIELD_TYPES.SELECT:
|
|
629
|
+
return generateSelect({
|
|
630
|
+
rand: ctx.rand,
|
|
631
|
+
choices: field.options?.choices ?? []
|
|
632
|
+
});
|
|
633
|
+
case FIELD_TYPES.RICHTEXT: {
|
|
634
|
+
const refPool = buildRichtextRefPool(
|
|
635
|
+
field,
|
|
636
|
+
ctx.idPools,
|
|
637
|
+
ctx.idToIdentifier
|
|
638
|
+
);
|
|
639
|
+
return generateRichtext({ rand: ctx.rand, refPool });
|
|
640
|
+
}
|
|
641
|
+
case FIELD_TYPES.RELATION: {
|
|
642
|
+
const pool = buildRelationPool(field, ctx.idPools, ctx.idToIdentifier);
|
|
643
|
+
return generateRelation({ rand: ctx.rand, pool });
|
|
644
|
+
}
|
|
645
|
+
case FIELD_TYPES.MULTIRELATION: {
|
|
646
|
+
const pool = buildRelationPool(field, ctx.idPools, ctx.idToIdentifier);
|
|
647
|
+
return generateMultirelation({
|
|
648
|
+
rand: ctx.rand,
|
|
649
|
+
pool,
|
|
650
|
+
fanout: ctx.fanout
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
case FIELD_TYPES.IMAGE:
|
|
654
|
+
return generateImage({ rand: ctx.rand, index: ctx.index });
|
|
655
|
+
default:
|
|
656
|
+
ctx.warnings.push(
|
|
657
|
+
`unknown field type "${field.type}" on ${ctx.contentTypeIdentifier}.${field.identifier}`
|
|
658
|
+
);
|
|
659
|
+
return void 0;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function buildRelationPool(field, idPools, idToIdentifier) {
|
|
663
|
+
const targetIdentifiers = resolveFieldTargetIdentifiers(
|
|
664
|
+
field.options?.targetContentTypeIds,
|
|
665
|
+
field.options?.targetContentTypeIdentifiers,
|
|
666
|
+
idToIdentifier
|
|
667
|
+
);
|
|
668
|
+
const pool = [];
|
|
669
|
+
for (const ident of targetIdentifiers) {
|
|
670
|
+
const entry = idPools.get(ident);
|
|
671
|
+
if (entry) pool.push(entry);
|
|
672
|
+
}
|
|
673
|
+
return pool;
|
|
674
|
+
}
|
|
675
|
+
function buildRichtextRefPool(field, idPools, idToIdentifier) {
|
|
676
|
+
const embedTargets = resolveFieldTargetIdentifiers(
|
|
677
|
+
field.options?.targetContentTypeIds,
|
|
678
|
+
field.options?.targetContentTypeIdentifiers,
|
|
679
|
+
idToIdentifier
|
|
680
|
+
);
|
|
681
|
+
const linkTargets = resolveFieldTargetIdentifiers(
|
|
682
|
+
field.options?.linkTargetContentTypeIds,
|
|
683
|
+
void 0,
|
|
684
|
+
idToIdentifier
|
|
685
|
+
);
|
|
686
|
+
if (embedTargets.length === 0 && linkTargets.length === 0) return null;
|
|
687
|
+
return {
|
|
688
|
+
embed: matchPools(embedTargets, idPools),
|
|
689
|
+
link: matchPools(linkTargets, idPools)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function matchPools(targetIds, idPools) {
|
|
693
|
+
const out = [];
|
|
694
|
+
for (const tid of targetIds) {
|
|
695
|
+
const entry = idPools.get(tid);
|
|
696
|
+
if (entry) {
|
|
697
|
+
out.push({
|
|
698
|
+
contentTypeId: entry.contentTypeId,
|
|
699
|
+
contentTypeIdentifier: entry.contentTypeIdentifier,
|
|
700
|
+
entryIds: entry.entryIds
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
var DEFAULT_WINDOW_END_MS = Date.UTC(2026, 0, 1);
|
|
707
|
+
var FIVE_YEARS_MS = 5 * 365 * 24 * 60 * 60 * 1e3;
|
|
708
|
+
function defaultWindow() {
|
|
709
|
+
const to = new Date(DEFAULT_WINDOW_END_MS);
|
|
710
|
+
const from = new Date(DEFAULT_WINDOW_END_MS - FIVE_YEARS_MS);
|
|
711
|
+
return { from, to };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/perf/rewriteSyntheticIds.ts
|
|
715
|
+
function rewriteSyntheticIds(data, map) {
|
|
716
|
+
return walk(data);
|
|
717
|
+
function walk(value) {
|
|
718
|
+
if (value === null || typeof value !== "object") return value;
|
|
719
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
720
|
+
const obj = value;
|
|
721
|
+
if (typeof obj.entryId === "string" && typeof obj.contentTypeId === "string" && Object.keys(obj).length <= 4) {
|
|
722
|
+
const real = map.get(obj.entryId);
|
|
723
|
+
const next2 = { ...obj };
|
|
724
|
+
if (real) next2.entryId = real;
|
|
725
|
+
return next2;
|
|
726
|
+
}
|
|
727
|
+
const next = {};
|
|
728
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
729
|
+
if (k === "attrs" && v && typeof v === "object" && typeof v.entryId === "string") {
|
|
730
|
+
const attrs = v;
|
|
731
|
+
const real = map.get(attrs.entryId);
|
|
732
|
+
next[k] = real ? { ...attrs, entryId: real } : { ...attrs };
|
|
733
|
+
} else {
|
|
734
|
+
next[k] = walk(v);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return next;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function findUnresolvedRefs(data, map) {
|
|
741
|
+
const unresolved = /* @__PURE__ */ new Set();
|
|
742
|
+
walk(data);
|
|
743
|
+
return unresolved;
|
|
744
|
+
function walk(value) {
|
|
745
|
+
if (value === null || typeof value !== "object") return;
|
|
746
|
+
if (Array.isArray(value)) {
|
|
747
|
+
for (const v of value) walk(v);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const obj = value;
|
|
751
|
+
if (typeof obj.entryId === "string" && typeof obj.contentTypeId === "string" && Object.keys(obj).length <= 4) {
|
|
752
|
+
if (!map.has(obj.entryId)) unresolved.add(obj.entryId);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
756
|
+
if (k === "attrs" && v && typeof v === "object" && typeof v.entryId === "string") {
|
|
757
|
+
const attrs = v;
|
|
758
|
+
if (!map.has(attrs.entryId)) unresolved.add(attrs.entryId);
|
|
759
|
+
} else {
|
|
760
|
+
walk(v);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function rewriteContentTypeIds(data, typeIdByIdentifier) {
|
|
766
|
+
return walk(data);
|
|
767
|
+
function walk(value) {
|
|
768
|
+
if (value === null || typeof value !== "object") return value;
|
|
769
|
+
if (Array.isArray(value)) return value.map(walk);
|
|
770
|
+
const obj = value;
|
|
771
|
+
if (typeof obj.entryId === "string" && typeof obj.contentTypeId === "string" && Object.keys(obj).length <= 4) {
|
|
772
|
+
const realCt = typeIdByIdentifier.get(obj.contentTypeId);
|
|
773
|
+
const next2 = { ...obj };
|
|
774
|
+
if (realCt) next2.contentTypeId = realCt;
|
|
775
|
+
return next2;
|
|
776
|
+
}
|
|
777
|
+
const next = {};
|
|
778
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
779
|
+
if (k === "attrs" && v && typeof v === "object" && typeof v.entryId === "string") {
|
|
780
|
+
const attrs = v;
|
|
781
|
+
const ct = typeof attrs.contentTypeId === "string" ? typeIdByIdentifier.get(attrs.contentTypeId) : void 0;
|
|
782
|
+
next[k] = ct ? { ...attrs, contentTypeId: ct } : { ...attrs };
|
|
783
|
+
} else {
|
|
784
|
+
next[k] = walk(v);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return next;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/perf/seedErrors.ts
|
|
792
|
+
var SEED_DUPLICATE_THRESHOLD = 0.5;
|
|
793
|
+
var SeedMostlyDuplicateError = class extends Error {
|
|
794
|
+
constructor(inserted, skipped, total) {
|
|
795
|
+
const pct = Math.round(skipped / total * 100);
|
|
796
|
+
super(
|
|
797
|
+
`Seed step skipped ${skipped} of ${total} entries (${pct}%) due to uniqueness conflicts. This likely means the target DB already contains entries with these titles/slugs. Pass --seed <n> for a different deterministic set, or reset the target DB first with \`boject perf reset --database-url <url>\`.`
|
|
798
|
+
);
|
|
799
|
+
this.inserted = inserted;
|
|
800
|
+
this.skipped = skipped;
|
|
801
|
+
this.total = total;
|
|
802
|
+
this.name = "SeedMostlyDuplicateError";
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// src/perf/writeViaSql.ts
|
|
807
|
+
var MissingContentTypeError = class extends Error {
|
|
808
|
+
constructor(identifier) {
|
|
809
|
+
super(
|
|
810
|
+
`Content type "${identifier}" not found in target database. Run "boject schema apply" first.`
|
|
811
|
+
);
|
|
812
|
+
this.identifier = identifier;
|
|
813
|
+
this.name = "MissingContentTypeError";
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
async function writeViaSql(client, generated, opts = {}) {
|
|
817
|
+
const batchSize = opts.batchSize ?? 500;
|
|
818
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
819
|
+
let inserted = 0;
|
|
820
|
+
let skipped = 0;
|
|
821
|
+
const skippedIds = /* @__PURE__ */ new Set();
|
|
822
|
+
const typeIdByIdentifier = /* @__PURE__ */ new Map();
|
|
823
|
+
for (const group of generated.groups) {
|
|
824
|
+
const r = await client.query(
|
|
825
|
+
'SELECT id FROM "ContentType" WHERE identifier = $1',
|
|
826
|
+
[group.contentTypeIdentifier]
|
|
827
|
+
);
|
|
828
|
+
const row = r.rows[0];
|
|
829
|
+
if (!row?.id)
|
|
830
|
+
throw new MissingContentTypeError(group.contentTypeIdentifier);
|
|
831
|
+
typeIdByIdentifier.set(group.contentTypeIdentifier, row.id);
|
|
832
|
+
}
|
|
833
|
+
for (const group of generated.groups) {
|
|
834
|
+
const contentTypeId = typeIdByIdentifier.get(group.contentTypeIdentifier);
|
|
835
|
+
for (let start = 0; start < group.entries.length; start += batchSize) {
|
|
836
|
+
const slice = group.entries.slice(start, start + batchSize);
|
|
837
|
+
const refValid = slice.filter((e) => {
|
|
838
|
+
const data = e.versions?.[0]?.data;
|
|
839
|
+
const unresolved = findUnresolvedRefs(data, idMap);
|
|
840
|
+
return ![...unresolved].some((id) => skippedIds.has(id));
|
|
841
|
+
});
|
|
842
|
+
for (const e of slice) {
|
|
843
|
+
if (!refValid.includes(e) && e.id) skippedIds.add(e.id);
|
|
844
|
+
}
|
|
845
|
+
skipped += slice.length - refValid.length;
|
|
846
|
+
const { insertedIds } = await insertEnvelopes(
|
|
847
|
+
client,
|
|
848
|
+
refValid,
|
|
849
|
+
contentTypeId
|
|
850
|
+
);
|
|
851
|
+
const survivors = refValid.filter((e) => e.id && insertedIds.has(e.id));
|
|
852
|
+
for (const e of refValid) {
|
|
853
|
+
if (!survivors.includes(e) && e.id) skippedIds.add(e.id);
|
|
854
|
+
}
|
|
855
|
+
skipped += refValid.length - survivors.length;
|
|
856
|
+
await insertVersions(client, survivors, idMap, typeIdByIdentifier);
|
|
857
|
+
for (const e of survivors) {
|
|
858
|
+
if (!e.id) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
"Bundle entry is missing an id; cannot map for ID rewriting"
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
idMap.set(e.id, e.id);
|
|
864
|
+
}
|
|
865
|
+
inserted += survivors.length;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const patchTs = generated.groups[0]?.entries[0]?.versions?.[0]?.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
869
|
+
for (const group of generated.groups) {
|
|
870
|
+
if (!group.patches) continue;
|
|
871
|
+
for (const patch of group.patches) {
|
|
872
|
+
if (skippedIds.has(patch.entryId)) {
|
|
873
|
+
process.stderr.write(
|
|
874
|
+
`[perf:seed] skipping patch \u2014 target entry ${patch.entryId} was skipped this run
|
|
875
|
+
`
|
|
876
|
+
);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const unresolved = findUnresolvedRefs(patch.fieldUpdates, idMap);
|
|
880
|
+
if ([...unresolved].some((id) => skippedIds.has(id))) {
|
|
881
|
+
process.stderr.write(
|
|
882
|
+
`[perf:seed] skipping patch on ${patch.entryId} \u2014 fieldUpdates reference skipped entries
|
|
883
|
+
`
|
|
884
|
+
);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const realEntryId = idMap.get(patch.entryId) ?? patch.entryId;
|
|
888
|
+
const rewrittenIds = rewriteSyntheticIds(patch.fieldUpdates, idMap);
|
|
889
|
+
const rewritten = rewriteContentTypeIds(
|
|
890
|
+
rewrittenIds,
|
|
891
|
+
typeIdByIdentifier
|
|
892
|
+
);
|
|
893
|
+
await applyPatch(client, realEntryId, rewritten, patchTs);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const total = inserted + skipped;
|
|
897
|
+
if (total > 0 && skipped / total > SEED_DUPLICATE_THRESHOLD) {
|
|
898
|
+
throw new SeedMostlyDuplicateError(inserted, skipped, total);
|
|
899
|
+
}
|
|
900
|
+
return { inserted, skipped };
|
|
901
|
+
}
|
|
902
|
+
async function insertEnvelopes(client, entries, contentTypeId) {
|
|
903
|
+
if (entries.length === 0) return { insertedIds: /* @__PURE__ */ new Set() };
|
|
904
|
+
const valuesPlaceholders = [];
|
|
905
|
+
const params = [];
|
|
906
|
+
let p = 1;
|
|
907
|
+
for (const e of entries) {
|
|
908
|
+
if (!e.versions?.[0]) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Bundle entry ${e.id ?? "<unknown>"} has no versions; refusing to insert envelope`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const ts = e.versions[0].publishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
914
|
+
valuesPlaceholders.push(
|
|
915
|
+
`($${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++}, $${p++})`
|
|
916
|
+
);
|
|
917
|
+
params.push(e.id, contentTypeId, e.entryTitle, e.entryKey, e.slug, ts, ts);
|
|
918
|
+
}
|
|
919
|
+
const result = await client.query(
|
|
920
|
+
`INSERT INTO "ContentEntry" ("id", "contentTypeId", "entryTitle", "entryKey", "slug", "createdAt", "updatedAt") VALUES ${valuesPlaceholders.join(", ")} ON CONFLICT DO NOTHING RETURNING id`,
|
|
921
|
+
params
|
|
922
|
+
);
|
|
923
|
+
const insertedIds = new Set(result.rows.map((r) => r.id));
|
|
924
|
+
return { insertedIds };
|
|
925
|
+
}
|
|
926
|
+
async function insertVersions(client, entries, idMap, typeIdByIdentifier) {
|
|
927
|
+
if (entries.length === 0) return;
|
|
928
|
+
const valuesPlaceholders = [];
|
|
929
|
+
const params = [];
|
|
930
|
+
let p = 1;
|
|
931
|
+
for (const e of entries) {
|
|
932
|
+
if (!e.versions?.[0]) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`Bundle entry ${e.id ?? "<unknown>"} has no versions; refusing to insert version`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const v = e.versions[0];
|
|
938
|
+
const ts = v.publishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
939
|
+
const dataWithRealEntryIds = rewriteSyntheticIds(v.data, idMap);
|
|
940
|
+
const data = rewriteContentTypeIds(
|
|
941
|
+
dataWithRealEntryIds,
|
|
942
|
+
typeIdByIdentifier
|
|
943
|
+
);
|
|
944
|
+
valuesPlaceholders.push(
|
|
945
|
+
`(gen_random_uuid(), $${p++}, 'PUBLISHED', $${p++}, $${p++}::jsonb, $${p++}, $${p++}, $${p++})`
|
|
946
|
+
);
|
|
947
|
+
params.push(
|
|
948
|
+
e.id,
|
|
949
|
+
e.entryTitle,
|
|
950
|
+
JSON.stringify(data),
|
|
951
|
+
v.publishedAt,
|
|
952
|
+
ts,
|
|
953
|
+
ts
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
await client.query(
|
|
957
|
+
`INSERT INTO "ContentEntryVersion" ("id", "entryId", "status", "entryTitle", "data", "publishedAt", "createdAt", "updatedAt") VALUES ${valuesPlaceholders.join(", ")}`,
|
|
958
|
+
params
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
async function applyPatch(client, entryId, fieldUpdates, ts) {
|
|
962
|
+
await client.query(
|
|
963
|
+
`UPDATE "ContentEntryVersion" SET "data" = "data" || $1::jsonb, "updatedAt" = $2 WHERE "entryId" = $3 AND "status" = 'PUBLISHED'`,
|
|
964
|
+
[JSON.stringify(fieldUpdates), ts, entryId]
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/perf/allowedDatabase.ts
|
|
969
|
+
var DisallowedDatabaseError = class extends Error {
|
|
970
|
+
constructor(databaseUrl, dbName) {
|
|
971
|
+
super(
|
|
972
|
+
`Refusing to operate on database "${dbName}" (${redact(databaseUrl)}): name must end in "_perf" or "_staging". Pass --allow-database ${dbName} to override. Reset operations TRUNCATE every entry in the named database, not just perf-seeded rows \u2014 only override for disposable / cloned databases.`
|
|
973
|
+
);
|
|
974
|
+
this.databaseUrl = databaseUrl;
|
|
975
|
+
this.dbName = dbName;
|
|
976
|
+
this.name = "DisallowedDatabaseError";
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
var UnparseableDatabaseUrlError = class extends Error {
|
|
980
|
+
constructor(databaseUrl) {
|
|
981
|
+
super(
|
|
982
|
+
`Could not parse database name from URL (${redact(databaseUrl)}): expected a connection string of the form postgres://user:pass@host/<dbname>.`
|
|
983
|
+
);
|
|
984
|
+
this.databaseUrl = databaseUrl;
|
|
985
|
+
this.name = "UnparseableDatabaseUrlError";
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
function extractDatabaseName(databaseUrl) {
|
|
989
|
+
let url;
|
|
990
|
+
try {
|
|
991
|
+
url = new URL(databaseUrl);
|
|
992
|
+
} catch {
|
|
993
|
+
throw new UnparseableDatabaseUrlError(databaseUrl);
|
|
994
|
+
}
|
|
995
|
+
const name = url.pathname.replace(/^\//, "");
|
|
996
|
+
if (!name) throw new UnparseableDatabaseUrlError(databaseUrl);
|
|
997
|
+
return name;
|
|
998
|
+
}
|
|
999
|
+
function assertAllowedDatabase(databaseUrl, allowDatabase) {
|
|
1000
|
+
const dbName = extractDatabaseName(databaseUrl);
|
|
1001
|
+
if (/_perf$/.test(dbName) || /_staging$/.test(dbName)) return;
|
|
1002
|
+
if (allowDatabase.includes(dbName)) return;
|
|
1003
|
+
throw new DisallowedDatabaseError(databaseUrl, dbName);
|
|
1004
|
+
}
|
|
1005
|
+
function redact(url) {
|
|
1006
|
+
return url.replace(/\/\/[^@]*@/, "//<redacted>@");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/perf/resetPerfDb.ts
|
|
1010
|
+
async function resetPerfDb(opts) {
|
|
1011
|
+
assertAllowedDatabase(opts.databaseUrl, opts.allowDatabase ?? []);
|
|
1012
|
+
await opts.runQuery(
|
|
1013
|
+
`TRUNCATE TABLE "ContentEntryVersion" RESTART IDENTITY CASCADE`
|
|
1014
|
+
);
|
|
1015
|
+
await opts.runQuery(`TRUNCATE TABLE "ContentEntry" RESTART IDENTITY CASCADE`);
|
|
1016
|
+
}
|
|
1017
|
+
export {
|
|
1018
|
+
CycleRequiresNullError,
|
|
1019
|
+
MissingContentTypeError,
|
|
1020
|
+
generatePerfData,
|
|
1021
|
+
resetPerfDb,
|
|
1022
|
+
writeViaSql
|
|
1023
|
+
};
|