@stage5/lumine 0.2.54 → 0.2.56
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/README.md +26 -12
- package/lib/admin-workflows.js +759 -59
- package/lib/admin.js +77 -3
- package/lib/auth.js +15 -9
- package/lib/commands.js +3 -3
- package/lib/sponsor.js +52 -32
- package/package.json +1 -1
- package/sdk/LUMINE_ADMIN.md +12 -7
package/lib/admin-workflows.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
createReadStream,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
truncateSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
3
17
|
import os from "node:os";
|
|
4
18
|
import path from "node:path";
|
|
19
|
+
import readline from "node:readline";
|
|
5
20
|
|
|
6
21
|
import { writeAdminJsonFile, readAdminJsonFile } from "./admin-news.js";
|
|
7
22
|
import { requestJson } from "./http.js";
|
|
@@ -9,9 +24,14 @@ import { requestJson } from "./http.js";
|
|
|
9
24
|
const MAX_BATCH_TARGET_FILE_BYTES = 2 * 1024 * 1024;
|
|
10
25
|
const MAX_BATCH_TARGETS = 20_000;
|
|
11
26
|
const MAX_AUTOMATIC_PAGES = 100_000;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
27
|
+
const MAX_LEGACY_ADMIN_CHECKPOINT_BYTES = 64 * 1024 * 1024;
|
|
28
|
+
const MAX_PAGINATION_CHECKPOINT_BYTES = 256 * 1024;
|
|
29
|
+
const MAX_BATCH_CHECKPOINT_BYTES = 64 * 1024 * 1024;
|
|
30
|
+
const ADMIN_PAGINATION_SCHEMA_VERSION = 3;
|
|
31
|
+
const LEGACY_ADMIN_PAGINATION_SCHEMA_VERSION = 2;
|
|
14
32
|
const ADMIN_BATCH_SCHEMA_VERSION = 2;
|
|
33
|
+
const PAGINATED_RESULT_STORAGE = Symbol("lumineAdminPaginationStorage");
|
|
34
|
+
const SPOOL_READ_BUFFER_BYTES = 64 * 1024;
|
|
15
35
|
|
|
16
36
|
function validationError(message) {
|
|
17
37
|
const error = new Error(message);
|
|
@@ -59,30 +79,522 @@ function defaultCheckpointPath({ runId, operationName }) {
|
|
|
59
79
|
return path.join(os.tmpdir(), `lumine-admin-run-${runId}-${safeName}.json`);
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
function
|
|
63
|
-
|
|
82
|
+
function operationFingerprintValue({
|
|
83
|
+
schemaVersion,
|
|
84
|
+
options,
|
|
85
|
+
operation,
|
|
86
|
+
runId,
|
|
87
|
+
}) {
|
|
88
|
+
return fingerprint({
|
|
89
|
+
workflowSchemaVersion: schemaVersion,
|
|
90
|
+
runId,
|
|
91
|
+
apiUrl: String(options.apiUrl || "").replace(/\/$/, ""),
|
|
92
|
+
name: operation.name,
|
|
93
|
+
path: pathWithCursor(operation.path, ""),
|
|
94
|
+
pagination: {
|
|
95
|
+
collectionKey: operation.pagination.collectionKey,
|
|
96
|
+
coverageQueue: operation.pagination.coverageQueue || null,
|
|
97
|
+
coverageMode: operation.pagination.coverageMode || null,
|
|
98
|
+
after: operation.pagination.after ?? null,
|
|
99
|
+
filters: operation.pagination.filters || {},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function candidateSpoolPath(checkpointPath) {
|
|
105
|
+
return `${checkpointPath}.candidates-${randomUUID()}.ndjson`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function candidateSpoolBelongsToCheckpoint({ checkpointPath, spoolPath }) {
|
|
109
|
+
const resolvedCheckpoint = path.resolve(checkpointPath);
|
|
110
|
+
const resolvedSpool = path.resolve(String(spoolPath || ""));
|
|
111
|
+
const expectedPrefix = `${path.basename(resolvedCheckpoint)}.candidates-`;
|
|
112
|
+
const spoolName = path.basename(resolvedSpool);
|
|
113
|
+
const spoolIdentifier = spoolName.slice(
|
|
114
|
+
expectedPrefix.length,
|
|
115
|
+
-".ndjson".length,
|
|
116
|
+
);
|
|
117
|
+
return (
|
|
118
|
+
path.dirname(resolvedSpool) === path.dirname(resolvedCheckpoint) &&
|
|
119
|
+
spoolName.startsWith(expectedPrefix) &&
|
|
120
|
+
spoolName.endsWith(".ndjson") &&
|
|
121
|
+
/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(
|
|
122
|
+
spoolIdentifier,
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createCandidateSpool(checkpointPath) {
|
|
128
|
+
const spoolPath = candidateSpoolPath(checkpointPath);
|
|
129
|
+
mkdirSync(path.dirname(spoolPath), { recursive: true });
|
|
130
|
+
const descriptor = openSync(spoolPath, "wx", 0o600);
|
|
131
|
+
closeSync(descriptor);
|
|
132
|
+
chmodSync(spoolPath, 0o600);
|
|
64
133
|
return {
|
|
134
|
+
spoolPath,
|
|
135
|
+
spoolHash: createHash("sha256"),
|
|
136
|
+
spoolBytes: 0,
|
|
137
|
+
spoolSha256: createHash("sha256").digest("hex"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function loadSupersededCandidateSpool(checkpointPath) {
|
|
142
|
+
let saved;
|
|
143
|
+
try {
|
|
144
|
+
saved = readAdminJsonFile(checkpointPath, "the previous pagination checkpoint", {
|
|
145
|
+
maxBytes: MAX_LEGACY_ADMIN_CHECKPOINT_BYTES,
|
|
146
|
+
});
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const spoolPath = path.resolve(String(saved?.spoolPath || ""));
|
|
151
|
+
if (
|
|
152
|
+
saved?.kind !== "admin-pagination" ||
|
|
153
|
+
Number(saved?.schemaVersion) !== ADMIN_PAGINATION_SCHEMA_VERSION ||
|
|
154
|
+
!candidateSpoolBelongsToCheckpoint({ checkpointPath, spoolPath })
|
|
155
|
+
) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const fileState = lstatSync(spoolPath);
|
|
160
|
+
return fileState.isFile() && !fileState.isSymbolicLink() ? spoolPath : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function appendCandidateItems({ spoolPath, spoolHash, items }) {
|
|
167
|
+
const lines = items.map((item) => {
|
|
168
|
+
const serialized = JSON.stringify(item);
|
|
169
|
+
if (serialized === undefined) {
|
|
170
|
+
throw validationError(
|
|
171
|
+
"The API returned a pagination candidate that cannot be serialized.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return serialized;
|
|
175
|
+
});
|
|
176
|
+
if (lines.length === 0) {
|
|
177
|
+
return { addedBytes: 0, spoolSha256: spoolHash.copy().digest("hex") };
|
|
178
|
+
}
|
|
179
|
+
const payload = Buffer.from(`${lines.join("\n")}\n`, "utf8");
|
|
180
|
+
const descriptor = openSync(spoolPath, "a", 0o600);
|
|
181
|
+
try {
|
|
182
|
+
writeFileSync(descriptor, payload);
|
|
183
|
+
fsyncSync(descriptor);
|
|
184
|
+
} finally {
|
|
185
|
+
closeSync(descriptor);
|
|
186
|
+
}
|
|
187
|
+
spoolHash.update(payload);
|
|
188
|
+
return {
|
|
189
|
+
addedBytes: payload.byteLength,
|
|
190
|
+
spoolSha256: spoolHash.copy().digest("hex"),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeCheckpointCount(value, label) {
|
|
195
|
+
const normalized = Number(value);
|
|
196
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
197
|
+
throw validationError(`The pagination checkpoint has an invalid ${label}.`);
|
|
198
|
+
}
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeFilterCount(value, label) {
|
|
203
|
+
const normalized = Number(value);
|
|
204
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
205
|
+
throw validationError(`Pagination has an invalid ${label}.`);
|
|
206
|
+
}
|
|
207
|
+
return normalized;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeClientFilterSummary(value) {
|
|
211
|
+
if (value === null || value === undefined) return null;
|
|
212
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
213
|
+
throw validationError("Pagination has invalid client-filter metadata.");
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
!Array.isArray(value.contentTypes) ||
|
|
217
|
+
value.contentTypes.some(
|
|
218
|
+
(contentType) =>
|
|
219
|
+
typeof contentType !== "string" || !contentType.trim(),
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
throw validationError(
|
|
223
|
+
"Pagination has invalid client-filter content types.",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
contentTypes: value.contentTypes.map((contentType) => contentType.trim()),
|
|
228
|
+
excludedItems: normalizeFilterCount(
|
|
229
|
+
value.excludedItems,
|
|
230
|
+
"client-filter excludedItems count",
|
|
231
|
+
),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function accumulateClientFilterSummary(previous, pageValue) {
|
|
236
|
+
const page = normalizeClientFilterSummary(pageValue);
|
|
237
|
+
if (!page) return previous;
|
|
238
|
+
if (!previous) return page;
|
|
239
|
+
if (
|
|
240
|
+
JSON.stringify(previous.contentTypes) !== JSON.stringify(page.contentTypes)
|
|
241
|
+
) {
|
|
242
|
+
throw validationError(
|
|
243
|
+
"The client-side content filter changed while paging one canonical snapshot.",
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...previous,
|
|
248
|
+
excludedItems: normalizeFilterCount(
|
|
249
|
+
previous.excludedItems + page.excludedItems,
|
|
250
|
+
"client-filter aggregate excludedItems count",
|
|
251
|
+
),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizeOperatorViewFilterSummary(value) {
|
|
256
|
+
if (value === null || value === undefined) return null;
|
|
257
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
258
|
+
throw validationError("Pagination has invalid operator-view metadata.");
|
|
259
|
+
}
|
|
260
|
+
const mode = String(value.mode || "").trim();
|
|
261
|
+
if (!mode) {
|
|
262
|
+
throw validationError("Pagination has an invalid operator-view mode.");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
mode,
|
|
266
|
+
excludedItems: normalizeFilterCount(
|
|
267
|
+
value.excludedItems,
|
|
268
|
+
"operator-view excludedItems count",
|
|
269
|
+
),
|
|
270
|
+
unknownStateItems: normalizeFilterCount(
|
|
271
|
+
value.unknownStateItems,
|
|
272
|
+
"operator-view unknownStateItems count",
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function accumulateOperatorViewFilterSummary(previous, pageValue) {
|
|
278
|
+
const page = normalizeOperatorViewFilterSummary(pageValue);
|
|
279
|
+
if (!page) return previous;
|
|
280
|
+
if (!previous) return page;
|
|
281
|
+
if (previous.mode !== page.mode) {
|
|
282
|
+
throw validationError(
|
|
283
|
+
"The operator-view filter changed while paging one canonical snapshot.",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...previous,
|
|
288
|
+
excludedItems: normalizeFilterCount(
|
|
289
|
+
previous.excludedItems + page.excludedItems,
|
|
290
|
+
"operator-view aggregate excludedItems count",
|
|
291
|
+
),
|
|
292
|
+
unknownStateItems: normalizeFilterCount(
|
|
293
|
+
previous.unknownStateItems + page.unknownStateItems,
|
|
294
|
+
"operator-view aggregate unknownStateItems count",
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function verifyCandidateSpool({
|
|
300
|
+
checkpointPath,
|
|
301
|
+
spoolPath,
|
|
302
|
+
spoolBytes,
|
|
303
|
+
spoolSha256,
|
|
304
|
+
candidateCount,
|
|
305
|
+
discardUnconfirmedTail,
|
|
306
|
+
}) {
|
|
307
|
+
if (!candidateSpoolBelongsToCheckpoint({ checkpointPath, spoolPath })) {
|
|
308
|
+
throw validationError(
|
|
309
|
+
"The pagination checkpoint references an invalid candidate spool.",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const confirmedBytes = normalizeCheckpointCount(spoolBytes, "spoolBytes");
|
|
313
|
+
const confirmedCount = normalizeCheckpointCount(
|
|
314
|
+
candidateCount,
|
|
315
|
+
"candidateCount",
|
|
316
|
+
);
|
|
317
|
+
const expectedDigest = String(spoolSha256 || "").toLowerCase();
|
|
318
|
+
if (!/^[a-f0-9]{64}$/.test(expectedDigest)) {
|
|
319
|
+
throw validationError(
|
|
320
|
+
"The pagination checkpoint has an invalid candidate-spool digest.",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
let fileState;
|
|
324
|
+
try {
|
|
325
|
+
fileState = lstatSync(spoolPath);
|
|
326
|
+
} catch {
|
|
327
|
+
throw validationError(`Could not read pagination candidates at ${spoolPath}.`);
|
|
328
|
+
}
|
|
329
|
+
if (!fileState.isFile() || fileState.isSymbolicLink()) {
|
|
330
|
+
throw validationError(
|
|
331
|
+
"The pagination candidate spool must be a regular private file.",
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (fileState.size < confirmedBytes) {
|
|
335
|
+
throw validationError(
|
|
336
|
+
"The pagination candidate spool is shorter than its confirmed checkpoint.",
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (fileState.size > confirmedBytes) {
|
|
340
|
+
if (!discardUnconfirmedTail) {
|
|
341
|
+
throw validationError(
|
|
342
|
+
"The pagination candidate spool changed after its checkpoint was confirmed.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
truncateSync(spoolPath, confirmedBytes);
|
|
346
|
+
}
|
|
347
|
+
chmodSync(spoolPath, 0o600);
|
|
348
|
+
const spoolHash = createHash("sha256");
|
|
349
|
+
const descriptor = openSync(spoolPath, "r");
|
|
350
|
+
const buffer = Buffer.allocUnsafe(SPOOL_READ_BUFFER_BYTES);
|
|
351
|
+
let remaining = confirmedBytes;
|
|
352
|
+
let lineCount = 0;
|
|
353
|
+
try {
|
|
354
|
+
while (remaining > 0) {
|
|
355
|
+
const bytesRead = readSync(
|
|
356
|
+
descriptor,
|
|
357
|
+
buffer,
|
|
358
|
+
0,
|
|
359
|
+
Math.min(buffer.length, remaining),
|
|
360
|
+
null,
|
|
361
|
+
);
|
|
362
|
+
if (bytesRead <= 0) {
|
|
363
|
+
throw validationError(
|
|
364
|
+
"The pagination candidate spool ended before its confirmed boundary.",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
368
|
+
spoolHash.update(chunk);
|
|
369
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
370
|
+
if (chunk[index] === 10) lineCount += 1;
|
|
371
|
+
}
|
|
372
|
+
remaining -= bytesRead;
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
closeSync(descriptor);
|
|
376
|
+
}
|
|
377
|
+
if (lineCount !== confirmedCount) {
|
|
378
|
+
throw validationError(
|
|
379
|
+
"The pagination candidate spool count does not match its checkpoint.",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (spoolHash.copy().digest("hex") !== expectedDigest) {
|
|
383
|
+
throw validationError(
|
|
384
|
+
"The pagination candidate spool does not match its confirmed checkpoint digest.",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return spoolHash;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function writePaginationCheckpoint(checkpointPath, state) {
|
|
391
|
+
const { resumed: _resumed, ...checkpoint } = state;
|
|
392
|
+
writeAdminJsonFile(
|
|
393
|
+
checkpointPath,
|
|
394
|
+
{ ...checkpoint, updatedAt: new Date().toISOString() },
|
|
395
|
+
{ privateFile: true, maxBytes: MAX_PAGINATION_CHECKPOINT_BYTES },
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function paginationStorageFromResult(result) {
|
|
400
|
+
return result?.[PAGINATED_RESULT_STORAGE] || null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function getPaginatedResultStorage(result) {
|
|
404
|
+
const storage = paginationStorageFromResult(result);
|
|
405
|
+
return storage ? { ...storage } : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function attachPaginationStorage(result, storage) {
|
|
409
|
+
Object.defineProperty(result, PAGINATED_RESULT_STORAGE, {
|
|
410
|
+
configurable: false,
|
|
411
|
+
enumerable: false,
|
|
412
|
+
writable: false,
|
|
413
|
+
value: Object.freeze({ ...storage }),
|
|
414
|
+
});
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function serializedObjectEntries(value, excludedKeys = new Set()) {
|
|
419
|
+
const entries = [];
|
|
420
|
+
for (const [key, entryValue] of Object.entries(value || {})) {
|
|
421
|
+
if (excludedKeys.has(key)) continue;
|
|
422
|
+
const serialized = JSON.stringify(entryValue);
|
|
423
|
+
if (serialized !== undefined) {
|
|
424
|
+
entries.push(`${JSON.stringify(key)}:${serialized}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return entries;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function forEachPaginatedResultItem(result, visit) {
|
|
431
|
+
const storage = paginationStorageFromResult(result);
|
|
432
|
+
if (!storage) {
|
|
433
|
+
throw validationError("The result does not contain spooled pagination data.");
|
|
434
|
+
}
|
|
435
|
+
verifyCandidateSpool({ ...storage, discardUnconfirmedTail: false });
|
|
436
|
+
const input = createReadStream(storage.spoolPath, { encoding: "utf8" });
|
|
437
|
+
const observedHash = createHash("sha256");
|
|
438
|
+
let observedBytes = 0;
|
|
439
|
+
input.on("data", (chunk) => {
|
|
440
|
+
observedHash.update(chunk, "utf8");
|
|
441
|
+
observedBytes += Buffer.byteLength(chunk, "utf8");
|
|
442
|
+
});
|
|
443
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
444
|
+
let index = 0;
|
|
445
|
+
try {
|
|
446
|
+
for await (const line of lines) {
|
|
447
|
+
if (!line) continue;
|
|
448
|
+
let item;
|
|
449
|
+
try {
|
|
450
|
+
item = JSON.parse(line);
|
|
451
|
+
} catch {
|
|
452
|
+
throw validationError(
|
|
453
|
+
"The pagination candidate spool contains invalid JSON.",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
await visit(item, index);
|
|
457
|
+
index += 1;
|
|
458
|
+
}
|
|
459
|
+
} finally {
|
|
460
|
+
lines.close();
|
|
461
|
+
input.destroy();
|
|
462
|
+
}
|
|
463
|
+
if (index !== storage.candidateCount) {
|
|
464
|
+
throw validationError(
|
|
465
|
+
"The pagination candidate spool changed while it was being read.",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (
|
|
469
|
+
observedBytes !== storage.spoolBytes ||
|
|
470
|
+
observedHash.digest("hex") !== storage.spoolSha256
|
|
471
|
+
) {
|
|
472
|
+
throw validationError(
|
|
473
|
+
"The pagination candidate spool changed while it was being read.",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function writePaginatedResultJson({ result, write }) {
|
|
479
|
+
const storage = paginationStorageFromResult(result);
|
|
480
|
+
if (!storage) {
|
|
481
|
+
await write(`${JSON.stringify(result)}\n`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const outerEntries = serializedObjectEntries(result, new Set(["data"]));
|
|
485
|
+
const dataEntries = serializedObjectEntries(
|
|
486
|
+
result.data,
|
|
487
|
+
new Set([storage.collectionKey]),
|
|
488
|
+
);
|
|
489
|
+
let prefix = "{";
|
|
490
|
+
if (outerEntries.length > 0) prefix += outerEntries.join(",");
|
|
491
|
+
if (outerEntries.length > 0) prefix += ",";
|
|
492
|
+
prefix += `${JSON.stringify("data")}:{`;
|
|
493
|
+
if (dataEntries.length > 0) prefix += `${dataEntries.join(",")},`;
|
|
494
|
+
prefix += `${JSON.stringify(storage.collectionKey)}:[`;
|
|
495
|
+
await write(prefix);
|
|
496
|
+
let wroteItem = false;
|
|
497
|
+
await forEachPaginatedResultItem(result, async (item) => {
|
|
498
|
+
await write(`${wroteItem ? "," : ""}${JSON.stringify(item)}`);
|
|
499
|
+
wroteItem = true;
|
|
500
|
+
});
|
|
501
|
+
await write("]}}\n");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function writePaginatedResultFile(filePath, result) {
|
|
505
|
+
const resolved = path.resolve(String(filePath || "").trim());
|
|
506
|
+
if (!String(filePath || "").trim()) {
|
|
507
|
+
throw validationError("An output file path is required.");
|
|
508
|
+
}
|
|
509
|
+
mkdirSync(path.dirname(resolved), { recursive: true });
|
|
510
|
+
const temporary = `${resolved}.tmp-${process.pid}-${randomUUID()}`;
|
|
511
|
+
let descriptor;
|
|
512
|
+
try {
|
|
513
|
+
descriptor = openSync(temporary, "wx", 0o600);
|
|
514
|
+
await writePaginatedResultJson({
|
|
515
|
+
result,
|
|
516
|
+
write: async (chunk) => writeFileSync(descriptor, chunk, "utf8"),
|
|
517
|
+
});
|
|
518
|
+
fsyncSync(descriptor);
|
|
519
|
+
closeSync(descriptor);
|
|
520
|
+
descriptor = undefined;
|
|
521
|
+
renameSync(temporary, resolved);
|
|
522
|
+
chmodSync(resolved, 0o600);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (descriptor !== undefined) {
|
|
525
|
+
try {
|
|
526
|
+
closeSync(descriptor);
|
|
527
|
+
} catch {
|
|
528
|
+
// The failed write may already have closed the descriptor.
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
unlinkSync(temporary);
|
|
533
|
+
} catch {
|
|
534
|
+
// Exclusive create or successful rename may leave nothing to clean.
|
|
535
|
+
}
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
return resolved;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function aggregatePageResult({ operation, lastPage, checkpointPath, state }) {
|
|
542
|
+
const last = lastPage || { ok: true, status: "success", data: {} };
|
|
543
|
+
const lastData = { ...(last.data || {}) };
|
|
544
|
+
delete lastData[operation.pagination.collectionKey];
|
|
545
|
+
if (state.filterSummariesComplete) {
|
|
546
|
+
if (state.clientFilter) lastData.clientFilter = state.clientFilter;
|
|
547
|
+
else delete lastData.clientFilter;
|
|
548
|
+
if (state.operatorViewFilter) {
|
|
549
|
+
lastData.operatorViewFilter = state.operatorViewFilter;
|
|
550
|
+
} else {
|
|
551
|
+
delete lastData.operatorViewFilter;
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
// Legacy v2 checkpoints stored filtered candidates but not exact per-page
|
|
555
|
+
// exclusion counters. Do not present a last-page subtotal as a full-scan
|
|
556
|
+
// total after migrating one of those checkpoints.
|
|
557
|
+
delete lastData.clientFilter;
|
|
558
|
+
delete lastData.operatorViewFilter;
|
|
559
|
+
}
|
|
560
|
+
const candidateStorage = {
|
|
561
|
+
format: "ndjson",
|
|
562
|
+
path: state.spoolPath,
|
|
563
|
+
byteLength: state.spoolBytes,
|
|
564
|
+
sha256: state.spoolSha256,
|
|
565
|
+
};
|
|
566
|
+
const result = {
|
|
65
567
|
...last,
|
|
66
568
|
data: {
|
|
67
|
-
...
|
|
68
|
-
[operation.pagination.collectionKey]: state.items,
|
|
569
|
+
...lastData,
|
|
69
570
|
pagination: {
|
|
70
571
|
...(last.data?.pagination || {}),
|
|
71
572
|
nextCursor: state.nextCursor,
|
|
72
573
|
hasMore: !state.exhausted,
|
|
73
574
|
exhausted: state.exhausted,
|
|
74
575
|
after: state.after,
|
|
576
|
+
snapshotMaxId: state.snapshotMaxId,
|
|
75
577
|
snapshotTimeStamp: state.snapshotTimeStamp,
|
|
76
578
|
},
|
|
77
579
|
scan: {
|
|
78
580
|
pages: state.pages,
|
|
79
581
|
scannedCount: state.scannedCount,
|
|
80
|
-
candidateCount: state.
|
|
582
|
+
candidateCount: state.candidateCount,
|
|
81
583
|
checkpointPath,
|
|
82
584
|
resumed: state.resumed,
|
|
585
|
+
filterSummariesComplete: state.filterSummariesComplete,
|
|
586
|
+
candidateStorage,
|
|
83
587
|
},
|
|
84
588
|
},
|
|
85
589
|
};
|
|
590
|
+
return attachPaginationStorage(result, {
|
|
591
|
+
checkpointPath,
|
|
592
|
+
collectionKey: operation.pagination.collectionKey,
|
|
593
|
+
spoolPath: state.spoolPath,
|
|
594
|
+
spoolBytes: state.spoolBytes,
|
|
595
|
+
spoolSha256: state.spoolSha256,
|
|
596
|
+
candidateCount: state.candidateCount,
|
|
597
|
+
});
|
|
86
598
|
}
|
|
87
599
|
|
|
88
600
|
export async function runAutomaticPagination({
|
|
@@ -101,19 +613,29 @@ export async function runAutomaticPagination({
|
|
|
101
613
|
options.adminCheckpoint ||
|
|
102
614
|
defaultCheckpointPath({ runId, operationName: operation.name }),
|
|
103
615
|
);
|
|
104
|
-
const
|
|
105
|
-
|
|
616
|
+
const requestedOutputPath = String(options.adminOutput || "").trim();
|
|
617
|
+
if (options.adminOutput && !requestedOutputPath) {
|
|
618
|
+
throw validationError("An output file path is required.");
|
|
619
|
+
}
|
|
620
|
+
const outputPath = requestedOutputPath
|
|
621
|
+
? path.resolve(requestedOutputPath)
|
|
622
|
+
: null;
|
|
623
|
+
if (outputPath === checkpointPath) {
|
|
624
|
+
throw validationError(
|
|
625
|
+
"The pagination checkpoint and result output must use different files.",
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const operationFingerprint = operationFingerprintValue({
|
|
629
|
+
schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
630
|
+
options,
|
|
631
|
+
operation,
|
|
632
|
+
runId,
|
|
633
|
+
});
|
|
634
|
+
const legacyOperationFingerprint = operationFingerprintValue({
|
|
635
|
+
schemaVersion: LEGACY_ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
636
|
+
options,
|
|
637
|
+
operation,
|
|
106
638
|
runId,
|
|
107
|
-
apiUrl: String(options.apiUrl || "").replace(/\/$/, ""),
|
|
108
|
-
name: operation.name,
|
|
109
|
-
path: pathWithCursor(operation.path, ""),
|
|
110
|
-
pagination: {
|
|
111
|
-
collectionKey: operation.pagination.collectionKey,
|
|
112
|
-
coverageQueue: operation.pagination.coverageQueue || null,
|
|
113
|
-
coverageMode: operation.pagination.coverageMode || null,
|
|
114
|
-
after: operation.pagination.after ?? null,
|
|
115
|
-
filters: operation.pagination.filters || {},
|
|
116
|
-
},
|
|
117
639
|
});
|
|
118
640
|
let state = {
|
|
119
641
|
schemaVersion: ADMIN_PAGINATION_SCHEMA_VERSION,
|
|
@@ -129,29 +651,188 @@ export async function runAutomaticPagination({
|
|
|
129
651
|
snapshotTimeStamp: null,
|
|
130
652
|
after: operation.pagination.after ?? null,
|
|
131
653
|
boundariesConfirmed: false,
|
|
132
|
-
|
|
654
|
+
candidateCount: 0,
|
|
655
|
+
spoolPath: "",
|
|
656
|
+
spoolBytes: 0,
|
|
657
|
+
spoolSha256: "",
|
|
658
|
+
filterSummariesComplete: true,
|
|
659
|
+
clientFilter: null,
|
|
660
|
+
operatorViewFilter: null,
|
|
133
661
|
resumed: false,
|
|
134
662
|
updatedAt: new Date().toISOString(),
|
|
135
663
|
};
|
|
664
|
+
let spoolHash;
|
|
136
665
|
if (options.adminResume) {
|
|
137
666
|
const saved = readAdminJsonFile(
|
|
138
667
|
checkpointPath,
|
|
139
668
|
"the pagination checkpoint",
|
|
140
|
-
{ maxBytes:
|
|
669
|
+
{ maxBytes: MAX_LEGACY_ADMIN_CHECKPOINT_BYTES },
|
|
670
|
+
);
|
|
671
|
+
const savedSchemaVersion = Number(saved?.schemaVersion);
|
|
672
|
+
const savedPages = normalizeCheckpointCount(saved?.pages, "pages");
|
|
673
|
+
const savedScannedCount = normalizeCheckpointCount(
|
|
674
|
+
saved?.scannedCount,
|
|
675
|
+
"scannedCount",
|
|
141
676
|
);
|
|
677
|
+
const baseMatches =
|
|
678
|
+
saved?.kind === "admin-pagination" &&
|
|
679
|
+
Number(saved?.runId) === runId &&
|
|
680
|
+
((savedPages === 0 && saved?.boundariesConfirmed === false) ||
|
|
681
|
+
(savedPages > 0 && saved?.boundariesConfirmed === true));
|
|
682
|
+
const currentMatches =
|
|
683
|
+
savedSchemaVersion === ADMIN_PAGINATION_SCHEMA_VERSION &&
|
|
684
|
+
saved?.operationFingerprint === operationFingerprint;
|
|
685
|
+
const legacyMatches =
|
|
686
|
+
savedSchemaVersion === LEGACY_ADMIN_PAGINATION_SCHEMA_VERSION &&
|
|
687
|
+
saved?.operationFingerprint === legacyOperationFingerprint &&
|
|
688
|
+
Array.isArray(saved?.items) &&
|
|
689
|
+
savedPages > 0;
|
|
690
|
+
if (!baseMatches || (!currentMatches && !legacyMatches)) {
|
|
691
|
+
throw validationError(
|
|
692
|
+
"The checkpoint does not belong to this run and exact listing request.",
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const savedNextCursor = String(saved?.nextCursor || "");
|
|
696
|
+
const savedExhausted = saved?.exhausted;
|
|
142
697
|
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Number(saved?.runId) !== runId ||
|
|
147
|
-
!Array.isArray(saved?.items) ||
|
|
148
|
-
saved?.boundariesConfirmed !== true
|
|
698
|
+
typeof savedExhausted !== "boolean" ||
|
|
699
|
+
(!savedExhausted && savedPages > 0 && !savedNextCursor) ||
|
|
700
|
+
(savedExhausted && savedNextCursor)
|
|
149
701
|
) {
|
|
150
702
|
throw validationError(
|
|
151
|
-
"The checkpoint
|
|
703
|
+
"The pagination checkpoint has inconsistent exhaustion metadata.",
|
|
152
704
|
);
|
|
153
705
|
}
|
|
154
|
-
|
|
706
|
+
if (legacyMatches) {
|
|
707
|
+
const createdSpool = createCandidateSpool(checkpointPath);
|
|
708
|
+
spoolHash = createdSpool.spoolHash;
|
|
709
|
+
let spoolBytes = 0;
|
|
710
|
+
let spoolSha256 = createdSpool.spoolSha256;
|
|
711
|
+
try {
|
|
712
|
+
for (let index = 0; index < saved.items.length; index += 250) {
|
|
713
|
+
const appended = appendCandidateItems({
|
|
714
|
+
spoolPath: createdSpool.spoolPath,
|
|
715
|
+
spoolHash,
|
|
716
|
+
items: saved.items.slice(index, index + 250),
|
|
717
|
+
});
|
|
718
|
+
spoolBytes += appended.addedBytes;
|
|
719
|
+
spoolSha256 = appended.spoolSha256;
|
|
720
|
+
}
|
|
721
|
+
state = {
|
|
722
|
+
...state,
|
|
723
|
+
nextCursor: savedNextCursor,
|
|
724
|
+
exhausted: savedExhausted,
|
|
725
|
+
pages: savedPages,
|
|
726
|
+
scannedCount: savedScannedCount,
|
|
727
|
+
snapshotMaxId: normalizeBoundary(
|
|
728
|
+
saved?.snapshotMaxId,
|
|
729
|
+
"snapshotMaxId",
|
|
730
|
+
),
|
|
731
|
+
snapshotTimeStamp: normalizeBoundary(
|
|
732
|
+
saved?.snapshotTimeStamp,
|
|
733
|
+
"snapshotTimeStamp",
|
|
734
|
+
),
|
|
735
|
+
after: normalizeBoundary(saved?.after, "after"),
|
|
736
|
+
boundariesConfirmed: true,
|
|
737
|
+
candidateCount: saved.items.length,
|
|
738
|
+
spoolPath: createdSpool.spoolPath,
|
|
739
|
+
spoolBytes,
|
|
740
|
+
spoolSha256,
|
|
741
|
+
filterSummariesComplete: false,
|
|
742
|
+
resumed: true,
|
|
743
|
+
};
|
|
744
|
+
writePaginationCheckpoint(checkpointPath, state);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
try {
|
|
747
|
+
unlinkSync(createdSpool.spoolPath);
|
|
748
|
+
} catch {
|
|
749
|
+
// A failed migration may have created no durable candidate spool.
|
|
750
|
+
}
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
const candidateCount = normalizeCheckpointCount(
|
|
755
|
+
saved?.candidateCount,
|
|
756
|
+
"candidateCount",
|
|
757
|
+
);
|
|
758
|
+
state = {
|
|
759
|
+
...state,
|
|
760
|
+
nextCursor: savedNextCursor,
|
|
761
|
+
exhausted: savedExhausted,
|
|
762
|
+
pages: savedPages,
|
|
763
|
+
scannedCount: savedScannedCount,
|
|
764
|
+
snapshotMaxId:
|
|
765
|
+
savedPages > 0
|
|
766
|
+
? normalizeBoundary(saved?.snapshotMaxId, "snapshotMaxId")
|
|
767
|
+
: null,
|
|
768
|
+
snapshotTimeStamp:
|
|
769
|
+
savedPages > 0
|
|
770
|
+
? normalizeBoundary(
|
|
771
|
+
saved?.snapshotTimeStamp,
|
|
772
|
+
"snapshotTimeStamp",
|
|
773
|
+
)
|
|
774
|
+
: null,
|
|
775
|
+
after:
|
|
776
|
+
savedPages > 0
|
|
777
|
+
? normalizeBoundary(saved?.after, "after")
|
|
778
|
+
: operation.pagination.after ?? null,
|
|
779
|
+
boundariesConfirmed: saved?.boundariesConfirmed,
|
|
780
|
+
candidateCount,
|
|
781
|
+
spoolPath: path.resolve(String(saved?.spoolPath || "")),
|
|
782
|
+
spoolBytes: normalizeCheckpointCount(saved?.spoolBytes, "spoolBytes"),
|
|
783
|
+
spoolSha256: String(saved?.spoolSha256 || "").toLowerCase(),
|
|
784
|
+
filterSummariesComplete:
|
|
785
|
+
savedPages === 0 || saved?.filterSummariesComplete === true,
|
|
786
|
+
clientFilter:
|
|
787
|
+
savedPages > 0 && saved?.filterSummariesComplete === true
|
|
788
|
+
? normalizeClientFilterSummary(saved?.clientFilter)
|
|
789
|
+
: null,
|
|
790
|
+
operatorViewFilter:
|
|
791
|
+
savedPages > 0 && saved?.filterSummariesComplete === true
|
|
792
|
+
? normalizeOperatorViewFilterSummary(saved?.operatorViewFilter)
|
|
793
|
+
: null,
|
|
794
|
+
resumed: true,
|
|
795
|
+
};
|
|
796
|
+
spoolHash = verifyCandidateSpool({
|
|
797
|
+
checkpointPath,
|
|
798
|
+
spoolPath: state.spoolPath,
|
|
799
|
+
spoolBytes: state.spoolBytes,
|
|
800
|
+
spoolSha256: state.spoolSha256,
|
|
801
|
+
candidateCount: state.candidateCount,
|
|
802
|
+
discardUnconfirmedTail: true,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
const supersededSpoolPath = loadSupersededCandidateSpool(checkpointPath);
|
|
807
|
+
const createdSpool = createCandidateSpool(checkpointPath);
|
|
808
|
+
spoolHash = createdSpool.spoolHash;
|
|
809
|
+
state.spoolPath = createdSpool.spoolPath;
|
|
810
|
+
state.spoolBytes = createdSpool.spoolBytes;
|
|
811
|
+
state.spoolSha256 = createdSpool.spoolSha256;
|
|
812
|
+
try {
|
|
813
|
+
writePaginationCheckpoint(checkpointPath, state);
|
|
814
|
+
} catch (error) {
|
|
815
|
+
try {
|
|
816
|
+
unlinkSync(state.spoolPath);
|
|
817
|
+
} catch {
|
|
818
|
+
// The failed initialization may leave no spool to clean.
|
|
819
|
+
}
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
if (supersededSpoolPath && supersededSpoolPath !== state.spoolPath) {
|
|
823
|
+
try {
|
|
824
|
+
unlinkSync(supersededSpoolPath);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
reportProgress(
|
|
827
|
+
`Lumine admin ${operation.name}: could not remove superseded candidate spool ${supersededSpoolPath} (${error?.message || error}).`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (outputPath === path.resolve(state.spoolPath)) {
|
|
833
|
+
throw validationError(
|
|
834
|
+
"The pagination candidate spool and result output must use different files.",
|
|
835
|
+
);
|
|
155
836
|
}
|
|
156
837
|
const progressEnabled = options.json === true;
|
|
157
838
|
if (progressEnabled) {
|
|
@@ -159,17 +840,17 @@ export async function runAutomaticPagination({
|
|
|
159
840
|
`Lumine admin ${operation.name}: ${state.resumed ? "resuming" : "starting"} canonical scan; checkpoint ${checkpointPath}.`,
|
|
160
841
|
);
|
|
161
842
|
}
|
|
162
|
-
|
|
843
|
+
let lastPage = null;
|
|
163
844
|
while (!state.exhausted) {
|
|
164
845
|
if (state.pages >= MAX_AUTOMATIC_PAGES) {
|
|
165
846
|
throw validationError(
|
|
166
|
-
`Automatic pagination stopped at the ${MAX_AUTOMATIC_PAGES}-page safety ceiling.
|
|
847
|
+
`Automatic pagination stopped at the ${MAX_AUTOMATIC_PAGES}-page safety ceiling. Start a narrower request with a new checkpoint.`,
|
|
167
848
|
);
|
|
168
849
|
}
|
|
169
850
|
const requestPath = pathWithCursor(operation.path, state.nextCursor);
|
|
170
851
|
const raw = await fetchPage(requestPath);
|
|
171
852
|
const page = transformPage(raw);
|
|
172
|
-
|
|
853
|
+
lastPage = page;
|
|
173
854
|
const data = page?.data || {};
|
|
174
855
|
const pagination = data.pagination;
|
|
175
856
|
if (!pagination || typeof pagination !== "object" || Array.isArray(pagination)) {
|
|
@@ -224,31 +905,52 @@ export async function runAutomaticPagination({
|
|
|
224
905
|
state.after = requestedAfter ?? pageAfter;
|
|
225
906
|
state.boundariesConfirmed = true;
|
|
226
907
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
items.length,
|
|
232
|
-
);
|
|
233
|
-
state.nextCursor = nextCursor;
|
|
234
|
-
state.exhausted = pagination.exhausted;
|
|
235
|
-
state.updatedAt = new Date().toISOString();
|
|
236
|
-
writeAdminJsonFile(checkpointPath, state, {
|
|
237
|
-
privateFile: true,
|
|
238
|
-
maxBytes: MAX_ADMIN_CHECKPOINT_BYTES,
|
|
908
|
+
const appended = appendCandidateItems({
|
|
909
|
+
spoolPath: state.spoolPath,
|
|
910
|
+
spoolHash,
|
|
911
|
+
items,
|
|
239
912
|
});
|
|
913
|
+
const clientFilter = state.filterSummariesComplete
|
|
914
|
+
? accumulateClientFilterSummary(state.clientFilter, data.clientFilter)
|
|
915
|
+
: null;
|
|
916
|
+
const operatorViewFilter = state.filterSummariesComplete
|
|
917
|
+
? accumulateOperatorViewFilterSummary(
|
|
918
|
+
state.operatorViewFilter,
|
|
919
|
+
data.operatorViewFilter,
|
|
920
|
+
)
|
|
921
|
+
: null;
|
|
922
|
+
const nextState = {
|
|
923
|
+
...state,
|
|
924
|
+
pages: state.pages + 1,
|
|
925
|
+
scannedCount:
|
|
926
|
+
state.scannedCount +
|
|
927
|
+
normalizeScannedCount(
|
|
928
|
+
pagination.scannedCount,
|
|
929
|
+
items.length,
|
|
930
|
+
),
|
|
931
|
+
candidateCount: state.candidateCount + items.length,
|
|
932
|
+
spoolBytes: state.spoolBytes + appended.addedBytes,
|
|
933
|
+
spoolSha256: appended.spoolSha256,
|
|
934
|
+
clientFilter,
|
|
935
|
+
operatorViewFilter,
|
|
936
|
+
nextCursor,
|
|
937
|
+
exhausted: pagination.exhausted,
|
|
938
|
+
updatedAt: new Date().toISOString(),
|
|
939
|
+
};
|
|
940
|
+
writePaginationCheckpoint(checkpointPath, nextState);
|
|
941
|
+
state = nextState;
|
|
240
942
|
if (
|
|
241
943
|
progressEnabled &&
|
|
242
|
-
(pages
|
|
944
|
+
(state.pages === 1 || state.pages % 10 === 0 || state.exhausted)
|
|
243
945
|
) {
|
|
244
946
|
reportProgress(
|
|
245
|
-
`Lumine admin ${operation.name}: ${state.pages} page(s), ${state.scannedCount} row(s) scanned, ${state.
|
|
947
|
+
`Lumine admin ${operation.name}: ${state.pages} page(s), ${state.scannedCount} row(s) scanned, ${state.candidateCount} candidate(s)${state.exhausted ? "; canonical snapshot exhausted." : "; continuing."}`,
|
|
246
948
|
);
|
|
247
949
|
}
|
|
248
950
|
}
|
|
249
951
|
const result = aggregatePageResult({
|
|
250
952
|
operation,
|
|
251
|
-
|
|
953
|
+
lastPage,
|
|
252
954
|
checkpointPath,
|
|
253
955
|
state,
|
|
254
956
|
});
|
|
@@ -259,18 +961,16 @@ export async function runAutomaticPagination({
|
|
|
259
961
|
after: state.after,
|
|
260
962
|
pages: state.pages,
|
|
261
963
|
scannedCount: state.scannedCount,
|
|
262
|
-
candidateCount: state.
|
|
964
|
+
candidateCount: state.candidateCount,
|
|
263
965
|
snapshotMaxId: state.snapshotMaxId,
|
|
264
966
|
snapshotTimeStamp: state.snapshotTimeStamp,
|
|
265
967
|
exhausted: state.exhausted,
|
|
266
968
|
filters: operation.pagination.filters || {},
|
|
267
969
|
});
|
|
268
970
|
}
|
|
269
|
-
if (
|
|
270
|
-
result.data.scan.outputPath =
|
|
271
|
-
|
|
272
|
-
maxBytes: MAX_ADMIN_CHECKPOINT_BYTES,
|
|
273
|
-
});
|
|
971
|
+
if (outputPath) {
|
|
972
|
+
result.data.scan.outputPath = outputPath;
|
|
973
|
+
await writePaginatedResultFile(outputPath, result);
|
|
274
974
|
}
|
|
275
975
|
return result;
|
|
276
976
|
}
|
|
@@ -365,7 +1065,7 @@ export async function runBatchSkips({
|
|
|
365
1065
|
};
|
|
366
1066
|
if (options.adminResume) {
|
|
367
1067
|
const saved = readAdminJsonFile(checkpointPath, "the batch checkpoint", {
|
|
368
|
-
maxBytes:
|
|
1068
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
369
1069
|
});
|
|
370
1070
|
if (
|
|
371
1071
|
saved?.schemaVersion !== ADMIN_BATCH_SCHEMA_VERSION ||
|
|
@@ -381,7 +1081,7 @@ export async function runBatchSkips({
|
|
|
381
1081
|
} else {
|
|
382
1082
|
writeAdminJsonFile(checkpointPath, state, {
|
|
383
1083
|
privateFile: true,
|
|
384
|
-
maxBytes:
|
|
1084
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
385
1085
|
});
|
|
386
1086
|
}
|
|
387
1087
|
for (const target of targets) {
|
|
@@ -405,7 +1105,7 @@ export async function runBatchSkips({
|
|
|
405
1105
|
state.updatedAt = new Date().toISOString();
|
|
406
1106
|
writeAdminJsonFile(checkpointPath, state, {
|
|
407
1107
|
privateFile: true,
|
|
408
|
-
maxBytes:
|
|
1108
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
409
1109
|
});
|
|
410
1110
|
}
|
|
411
1111
|
const result = {
|
|
@@ -427,7 +1127,7 @@ export async function runBatchSkips({
|
|
|
427
1127
|
if (options.adminOutput) {
|
|
428
1128
|
result.data.batch.outputPath = writeAdminJsonFile(options.adminOutput, result, {
|
|
429
1129
|
privateFile: true,
|
|
430
|
-
maxBytes:
|
|
1130
|
+
maxBytes: MAX_BATCH_CHECKPOINT_BYTES,
|
|
431
1131
|
});
|
|
432
1132
|
}
|
|
433
1133
|
return result;
|