monday-cli 0.5.0 → 0.7.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/CHANGELOG.md +665 -0
- package/README.md +209 -35
- package/dist/api/column-types.d.ts +81 -19
- package/dist/api/column-types.d.ts.map +1 -1
- package/dist/api/column-types.js +44 -11
- package/dist/api/column-types.js.map +1 -1
- package/dist/api/column-values.d.ts +22 -10
- package/dist/api/column-values.d.ts.map +1 -1
- package/dist/api/column-values.js +50 -20
- package/dist/api/column-values.js.map +1 -1
- package/dist/api/file-column-set.d.ts +613 -0
- package/dist/api/file-column-set.d.ts.map +1 -0
- package/dist/api/file-column-set.js +568 -0
- package/dist/api/file-column-set.js.map +1 -0
- package/dist/api/raw-write.d.ts +38 -17
- package/dist/api/raw-write.d.ts.map +1 -1
- package/dist/api/raw-write.js +62 -25
- package/dist/api/raw-write.js.map +1 -1
- package/dist/api/resolver-error-fold.d.ts +25 -0
- package/dist/api/resolver-error-fold.d.ts.map +1 -1
- package/dist/api/resolver-error-fold.js +56 -0
- package/dist/api/resolver-error-fold.js.map +1 -1
- package/dist/commands/board/column-create.d.ts +13 -3
- package/dist/commands/board/column-create.d.ts.map +1 -1
- package/dist/commands/board/column-create.js +27 -8
- package/dist/commands/board/column-create.js.map +1 -1
- package/dist/commands/item/create.d.ts +24 -8
- package/dist/commands/item/create.d.ts.map +1 -1
- package/dist/commands/item/create.js +601 -44
- package/dist/commands/item/create.js.map +1 -1
- package/dist/commands/item/set.d.ts +33 -3
- package/dist/commands/item/set.d.ts.map +1 -1
- package/dist/commands/item/set.js +193 -15
- package/dist/commands/item/set.js.map +1 -1
- package/dist/commands/item/update.d.ts +203 -3
- package/dist/commands/item/update.d.ts.map +1 -1
- package/dist/commands/item/update.js +1015 -68
- package/dist/commands/item/update.js.map +1 -1
- package/dist/commands/item/upload.d.ts.map +1 -1
- package/dist/commands/item/upload.js +16 -69
- package/dist/commands/item/upload.js.map +1 -1
- package/dist/commands/update/upload.d.ts.map +1 -1
- package/dist/commands/update/upload.js +9 -59
- package/dist/commands/update/upload.js.map +1 -1
- package/dist/utils/file-source.d.ts +93 -0
- package/dist/utils/file-source.d.ts.map +1 -0
- package/dist/utils/file-source.js +140 -0
- package/dist/utils/file-source.js.map +1 -0
- package/package.json +1 -1
|
@@ -52,13 +52,18 @@ import { parseArgv } from '../parse-argv.js';
|
|
|
52
52
|
import { ApiError, MondayCliError, UsageError } from '../../utils/errors.js';
|
|
53
53
|
import { selectMutation, } from '../../api/column-values.js';
|
|
54
54
|
import { executeItemMutation } from '../../api/item-mutation-execute.js';
|
|
55
|
+
import { executeFileColumnSet, fileColumnSetOutputSchema, preCheckM38FileDispatch, } from '../../api/file-column-set.js';
|
|
56
|
+
import { precheckLocalFile } from '../../utils/file-source.js';
|
|
57
|
+
import { invalidateBoard } from '../../api/cache.js';
|
|
58
|
+
import { dispatchSequential, } from '../../api/partial-success-mutation.js';
|
|
59
|
+
import { dispatchParallel } from '../../api/parallel-dispatch.js';
|
|
55
60
|
import { parseSetRawExpression, } from '../../api/raw-write.js';
|
|
56
61
|
import { splitSetExpression } from '../../api/set-expression.js';
|
|
57
62
|
import { buildResolutionContexts } from '../../api/resolution-context.js';
|
|
58
63
|
import { resolveBoardId } from '../../api/item-board-lookup.js';
|
|
59
|
-
import { SourceAggregator } from '../../api/source-aggregator.js';
|
|
64
|
+
import { SourceAggregator, mergeCacheAge, mergeSource, } from '../../api/source-aggregator.js';
|
|
60
65
|
import { resolveAndTranslate } from '../../api/resolution-pass.js';
|
|
61
|
-
import { foldAndRemap } from '../../api/resolver-error-fold.js';
|
|
66
|
+
import { foldAndRemap, mergeResolverWarningsIntoError, } from '../../api/resolver-error-fold.js';
|
|
62
67
|
import { planChanges } from '../../api/dry-run.js';
|
|
63
68
|
import { buildQueryParams } from '../../api/filters.js';
|
|
64
69
|
import { loadBoardMetadata, refreshBoardMetadata, } from '../../api/board-metadata.js';
|
|
@@ -69,7 +74,23 @@ import { resolveMeFactory } from '../../api/item-helpers.js';
|
|
|
69
74
|
import { projectedItemSchema, } from '../../api/item-projection.js';
|
|
70
75
|
import { runPartialSuccessBulkUpdate, buildPartialSuccessBulkSummary, partialSuccessBulkUpdateDataSchema, PARTIAL_SUCCESS_BULK_DISPATCH_SOURCE, } from '../../api/partial-success-bulk.js';
|
|
71
76
|
import { MIN_CONCURRENCY, MAX_CONCURRENCY, } from '../../api/parallel-dispatch.js';
|
|
72
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Output envelope union — projected-item for the JSON translator
|
|
79
|
+
* path (text / status / dropdown / date / people / etc.) +
|
|
80
|
+
* file-dispatch envelope for the friendly `--set <file-col>=<path>`
|
|
81
|
+
* path. The file-dispatch shape ships across v0.6-M38 (single-item
|
|
82
|
+
* — `operation: 'add_file_to_column'`) and v0.7-M42 (bulk —
|
|
83
|
+
* `operation: 'item_update_bulk_file_set'`; the per-item fan-out's
|
|
84
|
+
* `data.results[i].asset` slots wrap M31's `Asset` projection). The
|
|
85
|
+
* union below admits only the single-item shape because the bulk
|
|
86
|
+
* variant is emitted via its own `bulkFileSetDataSchema` at the
|
|
87
|
+
* `runItemUpdateBulkFileDispatch` helper; agents discriminate on
|
|
88
|
+
* `operation` (present + literal value identifies the variant).
|
|
89
|
+
*/
|
|
90
|
+
export const itemUpdateOutputSchema = z.union([
|
|
91
|
+
projectedItemSchema,
|
|
92
|
+
fileColumnSetOutputSchema,
|
|
93
|
+
]);
|
|
73
94
|
/**
|
|
74
95
|
* Input shape — supports both single-item and bulk shapes.
|
|
75
96
|
*
|
|
@@ -256,7 +277,7 @@ export const itemUpdateCommand = {
|
|
|
256
277
|
...(itemId === undefined ? {} : { itemId }),
|
|
257
278
|
...opts,
|
|
258
279
|
});
|
|
259
|
-
const { client, globalFlags, apiVersion, toEmit } = resolveClient(ctx, program.opts());
|
|
280
|
+
const { client, globalFlags, apiVersion, multipart, toEmit } = resolveClient(ctx, program.opts());
|
|
260
281
|
const dispatch = validateInputShape(parsed);
|
|
261
282
|
if (dispatch.kind === 'bulk') {
|
|
262
283
|
await runBulk({
|
|
@@ -266,6 +287,7 @@ export const itemUpdateCommand = {
|
|
|
266
287
|
apiVersion,
|
|
267
288
|
ctx,
|
|
268
289
|
programOpts: program.opts(),
|
|
290
|
+
multipart,
|
|
269
291
|
});
|
|
270
292
|
return;
|
|
271
293
|
}
|
|
@@ -284,49 +306,150 @@ export const itemUpdateCommand = {
|
|
|
284
306
|
explicit: parsed.board,
|
|
285
307
|
});
|
|
286
308
|
const { dateResolution, peopleResolution, tagResolution, relationResolution } = buildResolutionContexts({ client, ctx, globalFlags });
|
|
287
|
-
|
|
288
|
-
|
|
309
|
+
// v0.6-M38 mutex check at the column-resolution boundary
|
|
310
|
+
// (cli-design §5.3 step 5 "File-column dispatch leg —
|
|
311
|
+
// mutex rules"; D2/D5/D6 closures). Pre-checks setEntries'
|
|
312
|
+
// column types BEFORE calling `planChanges` /
|
|
313
|
+
// `resolveAndTranslate`, so the mutex rules fire upfront
|
|
314
|
+
// independent of translator-order side effects. The
|
|
315
|
+
// pre-check operates on setEntries only — `--set-raw
|
|
316
|
+
// <file-col>=<json>` rejection stays at
|
|
317
|
+
// `translateRawColumnValue` per D3 (permanent rejection;
|
|
318
|
+
// M38 dispatch never hijacks the --set-raw path).
|
|
319
|
+
const m38 = await preCheckM38FileDispatch({
|
|
320
|
+
client,
|
|
321
|
+
boardId,
|
|
322
|
+
setEntries,
|
|
323
|
+
setRawCount: rawEntries.length,
|
|
324
|
+
hasName: parsed.name !== undefined,
|
|
325
|
+
callShape: 'item_update_single',
|
|
326
|
+
env: ctx.env,
|
|
327
|
+
noCache: globalFlags.noCache,
|
|
328
|
+
});
|
|
329
|
+
if (m38.kind === 'file') {
|
|
330
|
+
// M38 dispatch path. The dry-run / live branches emit the
|
|
331
|
+
// D4 envelope / M31-shaped envelope respectively, threading
|
|
332
|
+
// the pre-check's resolver warnings + source aggregation
|
|
333
|
+
// into the final envelope (P3-1 — IMPL round-1 fix).
|
|
334
|
+
await runItemUpdateSingleFileDispatch({
|
|
289
335
|
client,
|
|
336
|
+
multipart,
|
|
337
|
+
ctx,
|
|
338
|
+
programOpts: program.opts(),
|
|
339
|
+
apiVersion,
|
|
290
340
|
boardId,
|
|
291
341
|
itemId: dispatch.itemId,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
peopleResolution,
|
|
297
|
-
tagResolution,
|
|
298
|
-
relationResolution,
|
|
299
|
-
env: ctx.env,
|
|
300
|
-
noCache: globalFlags.noCache,
|
|
342
|
+
m38,
|
|
343
|
+
isDryRun: globalFlags.dryRun,
|
|
344
|
+
retries: globalFlags.retry,
|
|
345
|
+
toEmit,
|
|
301
346
|
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (globalFlags.dryRun) {
|
|
350
|
+
// m38.kind === 'json' — standard JSON translator path
|
|
351
|
+
// applies. The pre-check's resolver warnings + source
|
|
352
|
+
// aggregation seed the downstream planChanges run
|
|
353
|
+
// (downstream resolveAndTranslate hits cache for the
|
|
354
|
+
// already-resolved setEntries; source aggregation is
|
|
355
|
+
// correct per §6.1 — both legs counted).
|
|
356
|
+
let result;
|
|
357
|
+
try {
|
|
358
|
+
result = await planChanges({
|
|
359
|
+
client,
|
|
360
|
+
boardId,
|
|
361
|
+
itemId: dispatch.itemId,
|
|
362
|
+
setEntries,
|
|
363
|
+
...(rawEntries.length === 0 ? {} : { rawEntries }),
|
|
364
|
+
...(parsed.name === undefined ? {} : { nameChange: parsed.name }),
|
|
365
|
+
dateResolution,
|
|
366
|
+
peopleResolution,
|
|
367
|
+
tagResolution,
|
|
368
|
+
relationResolution,
|
|
369
|
+
env: ctx.env,
|
|
370
|
+
noCache: globalFlags.noCache,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
// Round-3 P3-1 fix: fold M38 pre-check warnings into
|
|
375
|
+
// the failure envelope's `details.resolver_warnings`
|
|
376
|
+
// slot. The pre-check may have emitted
|
|
377
|
+
// `stale_cache_refreshed` / `column_token_collision`;
|
|
378
|
+
// if downstream `planChanges` throws (translator
|
|
379
|
+
// error, archived column, etc.), the error's own
|
|
380
|
+
// fold doesn't include the pre-check leg.
|
|
381
|
+
if (err instanceof MondayCliError && m38.warnings.length > 0) {
|
|
382
|
+
throw mergeResolverWarningsIntoError(err, m38.warnings);
|
|
383
|
+
}
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
// Round-2 P3-1 fix: thread the pre-check's resolver
|
|
387
|
+
// warnings into the dry-run envelope. A
|
|
388
|
+
// `stale_cache_refreshed` or `column_token_collision`
|
|
389
|
+
// emitted by the pre-check would otherwise be lost —
|
|
390
|
+
// downstream `planChanges` re-resolves against the
|
|
391
|
+
// now-warm cache and doesn't re-emit `stale_cache_refreshed`
|
|
392
|
+
// (the refresh already ran). Dedupe by code+message+token
|
|
393
|
+
// mirrors `dedupeWarnings` from the bulk path.
|
|
394
|
+
//
|
|
395
|
+
// **Round-3 P3-1 fix (error-path)**: the planChanges
|
|
396
|
+
// call itself is wrapped above (line 483); if it throws
|
|
397
|
+
// before returning, the error catch below folds
|
|
398
|
+
// `m38.warnings` into `details.resolver_warnings` so
|
|
399
|
+
// pre-check warnings ride into the failure envelope
|
|
400
|
+
// too.
|
|
302
401
|
emitDryRun({
|
|
303
402
|
ctx,
|
|
304
403
|
programOpts: program.opts(),
|
|
305
404
|
plannedChanges: result.plannedChanges,
|
|
306
|
-
source: result.source,
|
|
307
|
-
cacheAgeSeconds: result.cacheAgeSeconds,
|
|
308
|
-
warnings: result.warnings,
|
|
405
|
+
source: mergeSource(m38.source, result.source),
|
|
406
|
+
cacheAgeSeconds: mergeCacheAge(m38.cacheAgeSeconds, result.cacheAgeSeconds),
|
|
407
|
+
warnings: dedupeWarnings([...m38.warnings, ...result.warnings]),
|
|
309
408
|
apiVersion,
|
|
310
409
|
});
|
|
311
410
|
return;
|
|
312
411
|
}
|
|
313
412
|
// Live update path — three-pass resolution + translation
|
|
314
|
-
// through the shared helper (R20 lift).
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
413
|
+
// through the shared helper (R20 lift). Pre-check already
|
|
414
|
+
// resolved setEntries (cache hit downstream).
|
|
415
|
+
let resolutionResult;
|
|
416
|
+
try {
|
|
417
|
+
resolutionResult = await resolveAndTranslate({
|
|
418
|
+
client,
|
|
419
|
+
boardId,
|
|
420
|
+
setEntries,
|
|
421
|
+
rawEntries,
|
|
422
|
+
dateResolution,
|
|
423
|
+
peopleResolution,
|
|
424
|
+
tagResolution,
|
|
425
|
+
relationResolution,
|
|
426
|
+
env: ctx.env,
|
|
427
|
+
noCache: globalFlags.noCache,
|
|
428
|
+
...(m38.source === undefined ? {} : { initialSource: m38.source }),
|
|
429
|
+
initialCacheAgeSeconds: m38.cacheAgeSeconds,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
// Round-3 P3-1 fix: fold M38 pre-check warnings into the
|
|
434
|
+
// failure envelope's `details.resolver_warnings` slot. The
|
|
435
|
+
// pre-check may have emitted `stale_cache_refreshed` /
|
|
436
|
+
// `column_token_collision`; if downstream
|
|
437
|
+
// `resolveAndTranslate` throws (translator error, archived
|
|
438
|
+
// column post-cache-warm, etc.), the thrown error's own
|
|
439
|
+
// resolver-warnings fold doesn't include the pre-check leg.
|
|
440
|
+
if (err instanceof MondayCliError && m38.warnings.length > 0) {
|
|
441
|
+
throw mergeResolverWarningsIntoError(err, m38.warnings);
|
|
442
|
+
}
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
// Round-2 P3-1 fix: thread pre-check's resolver warnings
|
|
446
|
+
// alongside downstream warnings, deduped by code+message+
|
|
447
|
+
// token — pre-check's `stale_cache_refreshed` would
|
|
448
|
+
// otherwise be lost (warm cache suppresses re-emit).
|
|
449
|
+
const collectedWarnings = dedupeWarnings([
|
|
450
|
+
...m38.warnings,
|
|
328
451
|
...resolutionResult.warnings,
|
|
329
|
-
];
|
|
452
|
+
]);
|
|
330
453
|
const resolvedIds = resolutionResult.resolvedIds;
|
|
331
454
|
const sourceAgg = new SourceAggregator();
|
|
332
455
|
if (resolutionResult.source !== undefined) {
|
|
@@ -489,7 +612,7 @@ const bulkLiveDataSchema = z.object({
|
|
|
489
612
|
* `--concurrency` flag is the future extension point.
|
|
490
613
|
*/
|
|
491
614
|
const runBulk = async (inputs) => {
|
|
492
|
-
const { parsed, client, globalFlags, apiVersion, ctx, programOpts } = inputs;
|
|
615
|
+
const { parsed, client, globalFlags, apiVersion, ctx, programOpts, multipart } = inputs;
|
|
493
616
|
/* c8 ignore next 6 — defensive: validateInputShape guarantees
|
|
494
617
|
parsed.board is non-undefined when shape is bulk; the type
|
|
495
618
|
guard exists for TS. */
|
|
@@ -514,6 +637,63 @@ const runBulk = async (inputs) => {
|
|
|
514
637
|
env: ctx.env,
|
|
515
638
|
noCache: globalFlags.noCache,
|
|
516
639
|
});
|
|
640
|
+
// 1.5) File-column dispatch pre-check at the column-resolution
|
|
641
|
+
// boundary, BEFORE the items_page walker + confirmation
|
|
642
|
+
// gate. The pre-check resolves setEntries against the now-
|
|
643
|
+
// warm metadata cache and runs
|
|
644
|
+
// `enforceSingleFileColumnSet({callShape: 'item_update_bulk'})`:
|
|
645
|
+
//
|
|
646
|
+
// - Multi-file `--set` → throws `'multi_file_set_unsupported'`
|
|
647
|
+
// (universal rule; single-column-per-wire-call).
|
|
648
|
+
// - File `--set` + value `--set` / `--set-raw` / `--name`
|
|
649
|
+
// → throws `'mixed_file_and_value_sets'` (universal rule;
|
|
650
|
+
// mixing forces non-atomic multi-leg dispatch).
|
|
651
|
+
// - Clean single file `--set` → returns
|
|
652
|
+
// `kind: 'file_bulk'` (v0.7-M42 D5 carve-out fold;
|
|
653
|
+
// action body branches into the per-item multipart
|
|
654
|
+
// fan-out helper `runItemUpdateBulkFileDispatch` —
|
|
655
|
+
// shipped at v0.7-M42 IMPL).
|
|
656
|
+
// - No file `--set` → returns `kind: 'json'` (standard
|
|
657
|
+
// JSON-translator path continues).
|
|
658
|
+
//
|
|
659
|
+
// At v0.6-M38 the `'item_update_bulk'` callShape rejected
|
|
660
|
+
// ALL bulk file `--set` paths with
|
|
661
|
+
// `'file_set_on_bulk_unsupported'`; v0.7-M42 pre-flight
|
|
662
|
+
// contract diff (`160330b`) carved that out per D5 fold,
|
|
663
|
+
// and IMPL (`22df2fa` + R1 fix-up `968b154`) shipped the
|
|
664
|
+
// runtime body. `--set-raw <file-col>=<json>` stays at
|
|
665
|
+
// `translateRawColumnValue`'s D3 permanent rejection (the
|
|
666
|
+
// pre-check only inspects setEntries; the standard path's
|
|
667
|
+
// `translateRawColumnValue` handles --set-raw rejection
|
|
668
|
+
// unchanged).
|
|
669
|
+
let m38Warnings = [];
|
|
670
|
+
let m38FileBulk;
|
|
671
|
+
if (setEntries.length > 0) {
|
|
672
|
+
const m38 = await preCheckM38FileDispatch({
|
|
673
|
+
client,
|
|
674
|
+
boardId,
|
|
675
|
+
setEntries,
|
|
676
|
+
setRawCount: rawEntries.length,
|
|
677
|
+
hasName: parsed.name !== undefined,
|
|
678
|
+
callShape: 'item_update_bulk',
|
|
679
|
+
env: ctx.env,
|
|
680
|
+
noCache: globalFlags.noCache,
|
|
681
|
+
});
|
|
682
|
+
// Round-2 P3-1 fix carry-forward: capture pre-check warnings
|
|
683
|
+
// to thread them into the final envelope. Pre-check resolution
|
|
684
|
+
// warnings (column_token_collision / stale_cache_refreshed)
|
|
685
|
+
// survive even though downstream cache hits suppress
|
|
686
|
+
// re-emission.
|
|
687
|
+
m38Warnings = m38.warnings;
|
|
688
|
+
if (m38.kind === 'file_bulk') {
|
|
689
|
+
// v0.7-M42 D5 carve-out fold. Hold the file_bulk slot for
|
|
690
|
+
// the items_page-walked dispatch leg below; the items_page
|
|
691
|
+
// walker still runs (collects target item IDs) + the
|
|
692
|
+
// confirmation gate still applies + the dispatch loop fans
|
|
693
|
+
// `executeFileColumnSet` across matched items.
|
|
694
|
+
m38FileBulk = m38;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
517
697
|
const onColumnNotFound = meta.source === 'cache'
|
|
518
698
|
? async () => {
|
|
519
699
|
const refreshed = await refreshBoardMetadata({
|
|
@@ -576,6 +756,13 @@ const runBulk = async (inputs) => {
|
|
|
576
756
|
// aggregate to `mixed` when metadata was cache-served.
|
|
577
757
|
const emptyEnvelopeSource = meta.source === 'cache' ? 'mixed' : 'live';
|
|
578
758
|
if (matchedItemIds.length === 0) {
|
|
759
|
+
// Round-2 P3-1 fix: thread M38 pre-check warnings into the
|
|
760
|
+
// empty-match envelope so a pre-check `stale_cache_refreshed`
|
|
761
|
+
// / `column_token_collision` survives the no-op short-circuit.
|
|
762
|
+
const emptyWarnings = dedupeWarnings([
|
|
763
|
+
...filterResult.warnings,
|
|
764
|
+
...m38Warnings,
|
|
765
|
+
]);
|
|
579
766
|
if (globalFlags.dryRun) {
|
|
580
767
|
emitDryRun({
|
|
581
768
|
ctx,
|
|
@@ -583,7 +770,7 @@ const runBulk = async (inputs) => {
|
|
|
583
770
|
plannedChanges: [],
|
|
584
771
|
source: emptyEnvelopeSource,
|
|
585
772
|
cacheAgeSeconds: meta.cacheAgeSeconds,
|
|
586
|
-
warnings:
|
|
773
|
+
warnings: emptyWarnings,
|
|
587
774
|
apiVersion,
|
|
588
775
|
});
|
|
589
776
|
return;
|
|
@@ -596,7 +783,7 @@ const runBulk = async (inputs) => {
|
|
|
596
783
|
},
|
|
597
784
|
schema: bulkLiveDataSchema,
|
|
598
785
|
programOpts,
|
|
599
|
-
warnings:
|
|
786
|
+
warnings: emptyWarnings,
|
|
600
787
|
source: emptyEnvelopeSource,
|
|
601
788
|
cacheAgeSeconds: meta.cacheAgeSeconds,
|
|
602
789
|
apiVersion,
|
|
@@ -627,6 +814,40 @@ const runBulk = async (inputs) => {
|
|
|
627
814
|
// fail-fast invariant. (See step 0 above — moving the parse there
|
|
628
815
|
// means a malformed --set / --set-raw doesn't pay for the metadata
|
|
629
816
|
// load + items_page walk first.)
|
|
817
|
+
// v0.7-M42 D5 carve-out fold — bulk file `--set` dispatch leg.
|
|
818
|
+
// When the pre-check returned `kind: 'file_bulk'`, branch into
|
|
819
|
+
// the per-item multipart fan-out helper here. The branch fires
|
|
820
|
+
// AFTER the items_page walker + confirmation gate (so an agent
|
|
821
|
+
// sees the matched-count via `confirmation_required` before
|
|
822
|
+
// the bulk file dispatch fans out) and BEFORE the JSON
|
|
823
|
+
// translator path's `resolveAndTranslate` call (which has
|
|
824
|
+
// nothing to translate when every `--set` is a file column).
|
|
825
|
+
//
|
|
826
|
+
// The helper runs single upfront `precheckLocalFile` + per-item
|
|
827
|
+
// `executeFileColumnSet` (fail-fast vs `--continue-on-error`
|
|
828
|
+
// per `parsed.continueOnError`; sequential vs parallel per
|
|
829
|
+
// `parsed.concurrency`) + post-dispatch `invalidateBoard` +
|
|
830
|
+
// envelope emit (`operation: 'item_update_bulk_file_set'`).
|
|
831
|
+
if (m38FileBulk !== undefined) {
|
|
832
|
+
await runItemUpdateBulkFileDispatch({
|
|
833
|
+
parsed,
|
|
834
|
+
client,
|
|
835
|
+
multipart,
|
|
836
|
+
ctx,
|
|
837
|
+
programOpts,
|
|
838
|
+
apiVersion,
|
|
839
|
+
boardId,
|
|
840
|
+
matchedItemIds,
|
|
841
|
+
m38: m38FileBulk,
|
|
842
|
+
metaSource: meta.source,
|
|
843
|
+
metaCacheAgeSeconds: meta.cacheAgeSeconds,
|
|
844
|
+
filterWarnings: filterResult.warnings,
|
|
845
|
+
retries: globalFlags.retry,
|
|
846
|
+
isDryRun: globalFlags.dryRun,
|
|
847
|
+
noCache: globalFlags.noCache,
|
|
848
|
+
});
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
630
851
|
const { dateResolution, peopleResolution, tagResolution, relationResolution } = buildResolutionContexts({ client, ctx, globalFlags });
|
|
631
852
|
// 5) Dry-run path: per-item planChanges. Column resolution is
|
|
632
853
|
// cached after the first call; per-item state read fires per
|
|
@@ -639,26 +860,53 @@ const runBulk = async (inputs) => {
|
|
|
639
860
|
// the resolver-warning preservation pattern is meant to keep.
|
|
640
861
|
if (globalFlags.dryRun) {
|
|
641
862
|
const allPlanned = [];
|
|
642
|
-
|
|
863
|
+
// Round-2 P3-1 fix: seed `aggregatedWarnings` with pre-check
|
|
864
|
+
// warnings so `stale_cache_refreshed` survives even though
|
|
865
|
+
// downstream per-item planChanges cache-hits suppress
|
|
866
|
+
// re-emission.
|
|
867
|
+
const aggregatedWarnings = [
|
|
868
|
+
...filterResult.warnings,
|
|
869
|
+
...m38Warnings,
|
|
870
|
+
];
|
|
643
871
|
const sourceAgg = new SourceAggregator({
|
|
644
872
|
source: meta.source,
|
|
645
873
|
cacheAgeSeconds: meta.cacheAgeSeconds,
|
|
646
874
|
});
|
|
647
875
|
for (const itemId of matchedItemIds) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
876
|
+
// v0.7-M42 IMPL: clean bulk file `--set` paths branched into
|
|
877
|
+
// `runItemUpdateBulkFileDispatch` above (the `m38FileBulk !==
|
|
878
|
+
// undefined` arm) and returned before reaching this loop, so
|
|
879
|
+
// this dry-run body only ever sees JSON-shaped paths.
|
|
880
|
+
// `--set-raw <file-col>=<json>` still rejects normally via
|
|
881
|
+
// `translateRawColumnValue`'s D3 permanent rejection.
|
|
882
|
+
let result;
|
|
883
|
+
try {
|
|
884
|
+
result = await planChanges({
|
|
885
|
+
client,
|
|
886
|
+
boardId,
|
|
887
|
+
itemId,
|
|
888
|
+
setEntries,
|
|
889
|
+
...(rawEntries.length === 0 ? {} : { rawEntries }),
|
|
890
|
+
...(parsed.name === undefined ? {} : { nameChange: parsed.name }),
|
|
891
|
+
dateResolution,
|
|
892
|
+
peopleResolution,
|
|
893
|
+
tagResolution,
|
|
894
|
+
relationResolution,
|
|
895
|
+
env: ctx.env,
|
|
896
|
+
noCache: globalFlags.noCache,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
// Round-3 P3-1 fix: fold M38 pre-check warnings into the
|
|
901
|
+
// per-item failure envelope so a pre-check
|
|
902
|
+
// `stale_cache_refreshed` rides into the error's
|
|
903
|
+
// `details.resolver_warnings` even when the per-item
|
|
904
|
+
// planChanges throws.
|
|
905
|
+
if (err instanceof MondayCliError && m38Warnings.length > 0) {
|
|
906
|
+
throw mergeResolverWarningsIntoError(err, m38Warnings);
|
|
907
|
+
}
|
|
908
|
+
throw err;
|
|
909
|
+
}
|
|
662
910
|
for (const plan of result.plannedChanges) {
|
|
663
911
|
allPlanned.push(plan);
|
|
664
912
|
}
|
|
@@ -703,25 +951,54 @@ const runBulk = async (inputs) => {
|
|
|
703
951
|
// is the narrowed subset used by foldResolverWarningsIntoError —
|
|
704
952
|
// the helper's contract is to fold collision / stale_cache_refreshed
|
|
705
953
|
// signals, not generic Warning types.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
954
|
+
// v0.6-M38 D5 file-set rejection already fired at the pre-check
|
|
955
|
+
// above (step 1.5). resolveAndTranslate processes only non-file
|
|
956
|
+
// setEntries here.
|
|
957
|
+
let resolutionResult;
|
|
958
|
+
try {
|
|
959
|
+
resolutionResult = await resolveAndTranslate({
|
|
960
|
+
client,
|
|
961
|
+
boardId,
|
|
962
|
+
setEntries,
|
|
963
|
+
rawEntries,
|
|
964
|
+
dateResolution,
|
|
965
|
+
peopleResolution,
|
|
966
|
+
tagResolution,
|
|
967
|
+
relationResolution,
|
|
968
|
+
env: ctx.env,
|
|
969
|
+
noCache: globalFlags.noCache,
|
|
970
|
+
initialSource: meta.source,
|
|
971
|
+
initialCacheAgeSeconds: meta.cacheAgeSeconds,
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
// Round-3 P3-1 fix: fold M38 pre-check warnings into the
|
|
976
|
+
// bulk-live failure envelope's `details.resolver_warnings`.
|
|
977
|
+
if (err instanceof MondayCliError && m38Warnings.length > 0) {
|
|
978
|
+
throw mergeResolverWarningsIntoError(err, m38Warnings);
|
|
979
|
+
}
|
|
980
|
+
throw err;
|
|
981
|
+
}
|
|
982
|
+
// Round-2 P3-1 fix: include M38 pre-check warnings in the live
|
|
983
|
+
// bulk envelope's aggregated warnings. Pre-check warnings are
|
|
984
|
+
// deduped against downstream resolveAndTranslate warnings so
|
|
985
|
+
// `stale_cache_refreshed` surfaces exactly once.
|
|
986
|
+
const collectedWarnings = dedupeWarnings([
|
|
721
987
|
...filterResult.warnings,
|
|
988
|
+
...m38Warnings,
|
|
722
989
|
...resolutionResult.warnings,
|
|
723
|
-
];
|
|
724
|
-
|
|
990
|
+
]);
|
|
991
|
+
// Round-3 P3-2 fix: include M38 pre-check warnings in
|
|
992
|
+
// `resolverWarnings`. This is the slot threaded into
|
|
993
|
+
// `foldAndRemap` for fail-fast bulk errors + partial-success
|
|
994
|
+
// results — without the pre-check leg, a pre-check
|
|
995
|
+
// `stale_cache_refreshed` is absent from per-item failure
|
|
996
|
+
// envelopes (and per-item partial-success records' error
|
|
997
|
+
// details).
|
|
998
|
+
const resolverWarnings = dedupeWarnings([
|
|
999
|
+
...m38Warnings,
|
|
1000
|
+
...resolutionResult.warnings,
|
|
1001
|
+
]);
|
|
725
1002
|
const resolvedIds = resolutionResult.resolvedIds;
|
|
726
1003
|
// resolveAndTranslate was seeded with meta.source / meta.cacheAge
|
|
727
1004
|
// above, so resolutionResult.source is always defined post-helper.
|
|
@@ -932,4 +1209,674 @@ const runBulk = async (inputs) => {
|
|
|
932
1209
|
resolvedIds,
|
|
933
1210
|
});
|
|
934
1211
|
};
|
|
1212
|
+
const runItemUpdateSingleFileDispatch = async (inputs) => {
|
|
1213
|
+
const precheck = await precheckLocalFile(inputs.m38.rawValue);
|
|
1214
|
+
if (inputs.isDryRun) {
|
|
1215
|
+
emitDryRun({
|
|
1216
|
+
ctx: inputs.ctx,
|
|
1217
|
+
programOpts: inputs.programOpts,
|
|
1218
|
+
plannedChanges: [
|
|
1219
|
+
{
|
|
1220
|
+
operation: 'add_file_to_column',
|
|
1221
|
+
item_id: inputs.itemId,
|
|
1222
|
+
column_id: inputs.m38.columnId,
|
|
1223
|
+
file_path: inputs.m38.rawValue,
|
|
1224
|
+
filename: precheck.filename,
|
|
1225
|
+
file_size_bytes: precheck.fileSizeBytes,
|
|
1226
|
+
},
|
|
1227
|
+
],
|
|
1228
|
+
source: 'none',
|
|
1229
|
+
cacheAgeSeconds: null,
|
|
1230
|
+
warnings: inputs.m38.warnings,
|
|
1231
|
+
apiVersion: inputs.apiVersion,
|
|
1232
|
+
});
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const result = await executeFileColumnSet({
|
|
1236
|
+
client: inputs.client,
|
|
1237
|
+
multipart: inputs.multipart,
|
|
1238
|
+
itemId: inputs.itemId,
|
|
1239
|
+
entry: {
|
|
1240
|
+
columnId: inputs.m38.columnId,
|
|
1241
|
+
columnType: 'file',
|
|
1242
|
+
rawValue: inputs.m38.rawValue,
|
|
1243
|
+
filePath: precheck.filePath,
|
|
1244
|
+
filename: precheck.filename,
|
|
1245
|
+
fileSizeBytes: precheck.fileSizeBytes,
|
|
1246
|
+
},
|
|
1247
|
+
signal: inputs.ctx.signal,
|
|
1248
|
+
retries: inputs.retries,
|
|
1249
|
+
});
|
|
1250
|
+
// §8 single-leg cache invalidation BEFORE emit (mirrors M31).
|
|
1251
|
+
await invalidateBoard(inputs.boardId, inputs.ctx.env);
|
|
1252
|
+
const data = {
|
|
1253
|
+
operation: 'add_file_to_column',
|
|
1254
|
+
item_id: inputs.itemId,
|
|
1255
|
+
column_id: inputs.m38.columnId,
|
|
1256
|
+
filename: precheck.filename,
|
|
1257
|
+
file_size_bytes: precheck.fileSizeBytes,
|
|
1258
|
+
asset: result.asset,
|
|
1259
|
+
};
|
|
1260
|
+
emitMutation({
|
|
1261
|
+
ctx: inputs.ctx,
|
|
1262
|
+
data,
|
|
1263
|
+
schema: fileColumnSetOutputSchema,
|
|
1264
|
+
programOpts: inputs.programOpts,
|
|
1265
|
+
warnings: inputs.m38.warnings.map((w) => ({
|
|
1266
|
+
code: w.code,
|
|
1267
|
+
message: w.message,
|
|
1268
|
+
details: w.details,
|
|
1269
|
+
})),
|
|
1270
|
+
...inputs.toEmit({
|
|
1271
|
+
data: result.asset,
|
|
1272
|
+
complexity: result.complexity,
|
|
1273
|
+
stats: { attempts: 1, totalBackoffMs: 0 },
|
|
1274
|
+
}),
|
|
1275
|
+
source: 'live',
|
|
1276
|
+
cacheAgeSeconds: null,
|
|
1277
|
+
complexity: result.complexity,
|
|
1278
|
+
resolvedIds: { [inputs.m38.token]: inputs.m38.columnId },
|
|
1279
|
+
});
|
|
1280
|
+
};
|
|
1281
|
+
// ============================================================
|
|
1282
|
+
// v0.7-M42 bulk file `--set` carve-out fold (D5 closure from
|
|
1283
|
+
// v0.6-M38). Per-item multipart fan-out over the `--where`-resolved
|
|
1284
|
+
// item-id set, reusing v0.4-M31's `executeFileColumnSet` runtime
|
|
1285
|
+
// body verbatim under v0.4-M30's `dispatchParallel` /
|
|
1286
|
+
// `dispatchSequential` selector for `--concurrency` semantics.
|
|
1287
|
+
//
|
|
1288
|
+
// **Status: runtime body shipped at v0.7-M42 IMPL.** The pre-flight
|
|
1289
|
+
// contract diff (commit `160330b`) shipped the argv + pre-check +
|
|
1290
|
+
// items_page + confirmation-gate surface plus the per-item dispatch
|
|
1291
|
+
// stub that threw `internal_error` with `details.reason:
|
|
1292
|
+
// 'm42_preflight_stub'`; IMPL replaces the stub with the runtime
|
|
1293
|
+
// body below. The `'m42_preflight_stub'` literal is RESERVED across
|
|
1294
|
+
// the codebase (regression-guarded by an integration test that
|
|
1295
|
+
// asserts the literal does not appear in stdout/stderr); a
|
|
1296
|
+
// historical `'file_set_on_bulk_unsupported'` literal stays
|
|
1297
|
+
// reserved alongside it.
|
|
1298
|
+
//
|
|
1299
|
+
// **D-list closures (v0.7-plan §3 M42 entry):**
|
|
1300
|
+
//
|
|
1301
|
+
// - **D1 — `--concurrency` semantics for file dispatch.** Reuse
|
|
1302
|
+
// v0.4-M30's `dispatchParallel` over a shared
|
|
1303
|
+
// `MultipartTransport`. Each parallel worker constructs its
|
|
1304
|
+
// own `MultipartTransportRequest` per call; the transport
|
|
1305
|
+
// itself is connection-pool-shared per-token. Closes by
|
|
1306
|
+
// inheritance from M30 (concurrency probe pinned at
|
|
1307
|
+
// `scripts/probe/m30-concurrency.report.txt`) + M31 (multipart
|
|
1308
|
+
// wire pinned at `scripts/probe/m31-asset-upload.report.txt`).
|
|
1309
|
+
// No new probe required.
|
|
1310
|
+
//
|
|
1311
|
+
// - **D2 — Per-item asset slot in envelope.** Per-item
|
|
1312
|
+
// `data.results[i].asset: { id, name, ... }` echo on success
|
|
1313
|
+
// (mirrors M31's `itemUploadOutputSchema`'s `asset` slot);
|
|
1314
|
+
// per-item failure surfaces as
|
|
1315
|
+
// `data.results[i].error: { code, message }` per M25
|
|
1316
|
+
// partial-success. Aggregate `data.summary.{matched_count,
|
|
1317
|
+
// applied_count, failed_count, board_id, column_id, filename,
|
|
1318
|
+
// file_size_bytes}` extends M25's
|
|
1319
|
+
// `partialSuccessBulkUpdateDataSchema` with file-dispatch slots
|
|
1320
|
+
// so an agent reading the envelope sees which file was
|
|
1321
|
+
// dispatched + where.
|
|
1322
|
+
//
|
|
1323
|
+
// - **D3 — Per-item file pre-check timing.** Single upfront
|
|
1324
|
+
// `precheckLocalFile` call BEFORE the per-item dispatch loop —
|
|
1325
|
+
// the bulk shape has ONE file path (the value of the file
|
|
1326
|
+
// `--set`) shared across N matched items. A failed pre-check
|
|
1327
|
+
// surfaces upfront as `usage_error` with `details.reason:
|
|
1328
|
+
// 'file_not_readable'` / `'file_empty'` (mirrors M31 single-
|
|
1329
|
+
// item shape) — this is whole-call-abort regardless of
|
|
1330
|
+
// `--continue-on-error`, per cli-design §5.8's "pre-checks
|
|
1331
|
+
// MUST fire BEFORE any wire round-trip" atomicity discipline.
|
|
1332
|
+
// The `--continue-on-error` flag partitions ONLY the wire-
|
|
1333
|
+
// dispatch failures (per-item `add_file_to_column` rejections
|
|
1334
|
+
// from Monday), never the local file pre-check.
|
|
1335
|
+
//
|
|
1336
|
+
// - **D4 — ERROR_CODES delta.** Zero. Registry stays at 29.
|
|
1337
|
+
// Per-item dispatch failures route through the existing
|
|
1338
|
+
// `m25-shaped` per-record `error: { code, message }` shape
|
|
1339
|
+
// under `--continue-on-error`, or through the v0.1 fail-fast
|
|
1340
|
+
// decoration (`details.applied_to` + `applied_count` +
|
|
1341
|
+
// `failed_at_item` + `matched_count`) on the default path;
|
|
1342
|
+
// no new top-level code surfaces.
|
|
1343
|
+
//
|
|
1344
|
+
// **R-class watch-items.**
|
|
1345
|
+
//
|
|
1346
|
+
// - R-v0.6-NEW-1 (file pre-check + Blob-construction helper) —
|
|
1347
|
+
// `precheckLocalFile` consumer count goes 3 → 4 at IMPL (M31
|
|
1348
|
+
// item upload + M31 update upload + M38 item set / item update
|
|
1349
|
+
// single + M42 bulk item update). Already-shipped helper
|
|
1350
|
+
// scales cleanly to consumer 4; graduation candidate at the
|
|
1351
|
+
// 5th consumer (v0.7-M43 create-time fold likely tips it).
|
|
1352
|
+
// - R-v0.6-NEW-2 (`details.reason` discriminator pattern) — 4
|
|
1353
|
+
// instances at v0.6-M38; M42 IMPL adds zero new reasons (per
|
|
1354
|
+
// D4 zero-delta closure — the existing `'file_not_readable'`
|
|
1355
|
+
// / `'file_empty'` / `'multi_file_set_unsupported'` /
|
|
1356
|
+
// `'mixed_file_and_value_sets'` reasons cover every M42
|
|
1357
|
+
// rejection shape).
|
|
1358
|
+
// - R-NEW-76 (parseArgv-BEFORE-c8) — applied at pre-flight; the
|
|
1359
|
+
// c8-ignore boundary dropped at IMPL (runtime body fully
|
|
1360
|
+
// covered).
|
|
1361
|
+
// - R-NEW-72 (post-fix-up cross-doc grep) — apply at every
|
|
1362
|
+
// Codex IMPL fix-up round that flips a contract surface.
|
|
1363
|
+
//
|
|
1364
|
+
// **Future lift candidate.** The fail-fast error-decoration block
|
|
1365
|
+
// (`if (err.code === 'usage_error') { throw new UsageError(...) } else
|
|
1366
|
+
// { throw new ApiError(...) }`) is byte-equivalent across this
|
|
1367
|
+
// helper and the JSON-bulk action body (R-NEW-58 2-consumer
|
|
1368
|
+
// trigger). Lift candidate fires at the 3rd consumer (M43 create-
|
|
1369
|
+
// time fold may add one if its rollback / orphan-warn shape
|
|
1370
|
+
// re-uses the same decoration).
|
|
1371
|
+
// ============================================================
|
|
1372
|
+
/**
|
|
1373
|
+
* Per-item dispatch result for v0.7-M42 bulk file `--set` carve-out
|
|
1374
|
+
* fold. Mirrors the M25 `partialSuccessBulkUpdateResultSchema` shape
|
|
1375
|
+
* with the file-dispatch's `asset` slot replacing the JSON path's
|
|
1376
|
+
* `item` projection:
|
|
1377
|
+
*
|
|
1378
|
+
* - Success: `{ item_id, ok: true, asset: { id, name, ... } }`
|
|
1379
|
+
* - Failure: `{ item_id, ok: false, error: { code, message } }`
|
|
1380
|
+
*
|
|
1381
|
+
* Schema landed at the v0.7-M42 pre-flight contract diff
|
|
1382
|
+
* (`160330b`); runtime body shipped at v0.7-M42 IMPL (`22df2fa`)
|
|
1383
|
+
* + R1 fix-up (`968b154`).
|
|
1384
|
+
*/
|
|
1385
|
+
export const bulkFileSetResultSchema = z.object({
|
|
1386
|
+
item_id: z.string().min(1),
|
|
1387
|
+
ok: z.boolean(),
|
|
1388
|
+
asset: z
|
|
1389
|
+
.object({
|
|
1390
|
+
id: z.string().min(1),
|
|
1391
|
+
name: z.string().min(1),
|
|
1392
|
+
})
|
|
1393
|
+
.loose()
|
|
1394
|
+
.optional(),
|
|
1395
|
+
error: z
|
|
1396
|
+
.object({
|
|
1397
|
+
code: z.string().min(1),
|
|
1398
|
+
message: z.string().min(1),
|
|
1399
|
+
})
|
|
1400
|
+
.optional(),
|
|
1401
|
+
});
|
|
1402
|
+
/**
|
|
1403
|
+
* Output `data` shape for the v0.7-M42 bulk file `--set` envelope.
|
|
1404
|
+
* Mirrors M25's `partialSuccessBulkUpdateDataSchema` structure —
|
|
1405
|
+
* `operation: 'item_update_bulk_file_set'` literal discriminator
|
|
1406
|
+
* + `summary.{matched_count, applied_count, failed_count,
|
|
1407
|
+
* board_id}` aggregate slots + per-item `results[]` array.
|
|
1408
|
+
*
|
|
1409
|
+
* Invariant: `matched_count === applied_count + failed_count` for
|
|
1410
|
+
* every emitted envelope (mirrors M25's invariant).
|
|
1411
|
+
*/
|
|
1412
|
+
export const bulkFileSetDataSchema = z.object({
|
|
1413
|
+
operation: z.literal('item_update_bulk_file_set'),
|
|
1414
|
+
summary: z.object({
|
|
1415
|
+
matched_count: z.number().int().nonnegative(),
|
|
1416
|
+
applied_count: z.number().int().nonnegative(),
|
|
1417
|
+
failed_count: z.number().int().nonnegative(),
|
|
1418
|
+
board_id: z.string().min(1),
|
|
1419
|
+
column_id: z.string().min(1),
|
|
1420
|
+
filename: z.string().min(1),
|
|
1421
|
+
file_size_bytes: z.number().int().nonnegative(),
|
|
1422
|
+
}),
|
|
1423
|
+
results: z.array(bulkFileSetResultSchema),
|
|
1424
|
+
});
|
|
1425
|
+
/**
|
|
1426
|
+
* Bulk file `--set` per-item dispatch helper (v0.7-M42 D5 carve-out
|
|
1427
|
+
* fold).
|
|
1428
|
+
*
|
|
1429
|
+
* **Status: runtime body shipped at v0.7-M42 IMPL.** argv parse,
|
|
1430
|
+
* shape validation, board-metadata load, pre-check (which returned
|
|
1431
|
+
* `kind: 'file_bulk'` for callers reaching this helper), items_page
|
|
1432
|
+
* walk, and the confirmation gate run upstream — this helper takes
|
|
1433
|
+
* the resolved file-column dispatch slot + matched item-IDs and
|
|
1434
|
+
* fans the multipart wire across them under the partial-success vs
|
|
1435
|
+
* fail-fast contract.
|
|
1436
|
+
*
|
|
1437
|
+
* **Execution shape:**
|
|
1438
|
+
*
|
|
1439
|
+
* 1. Single upfront `precheckLocalFile(inputs.m38.rawValue)` —
|
|
1440
|
+
* one file path shared across all matched items. Failure
|
|
1441
|
+
* surfaces as `usage_error` (`file_not_readable` /
|
|
1442
|
+
* `file_empty`) whole-call-abort regardless of
|
|
1443
|
+
* `--continue-on-error` per D3 + cli-design §5.8 atomicity
|
|
1444
|
+
* discipline (pre-checks MUST fire BEFORE any wire round-trip).
|
|
1445
|
+
* 2. Dry-run branch — emits the D4-shaped envelope with one
|
|
1446
|
+
* `add_file_to_column` planned_change per matched item-ID
|
|
1447
|
+
* (no file bytes loaded; no multipart wire). Unlike M38
|
|
1448
|
+
* single-item which pins `source: 'none'` (its dry-run is
|
|
1449
|
+
* pure-local), bulk dry-run carries the upstream legs'
|
|
1450
|
+
* aggregated `source` (metadata load + items_page walk —
|
|
1451
|
+
* `mixed` when metadata was cache-served, `live` otherwise);
|
|
1452
|
+
* reaching this branch already paid for those wire legs.
|
|
1453
|
+
* 3. Live dispatch — two shapes per `parsed.continueOnError`:
|
|
1454
|
+
* - **Fail-fast (default)** — sequential loop over matched
|
|
1455
|
+
* items (no `--concurrency` per M30 D2 closure:
|
|
1456
|
+
* `--concurrency requires --continue-on-error`); first
|
|
1457
|
+
* per-item failure aborts whole-call with the v0.1-shaped
|
|
1458
|
+
* `details.applied_to` / `applied_count` / `failed_at_item`
|
|
1459
|
+
* / `matched_count` decoration so agents see how many
|
|
1460
|
+
* items applied before the failure.
|
|
1461
|
+
* - **`--continue-on-error`** — routes through
|
|
1462
|
+
* {@link dispatchSequential} (concurrency `undefined`/`1`)
|
|
1463
|
+
* or {@link dispatchParallel} (concurrency `> 1`) over a
|
|
1464
|
+
* shared {@link MultipartTransport}; per-item failures
|
|
1465
|
+
* land as `data.results[i].error: {code, message}` records,
|
|
1466
|
+
* successes carry `data.results[i].asset` via a side-map
|
|
1467
|
+
* fold keyed by `item_id`. `internal_error` re-throws
|
|
1468
|
+
* whole-call via the shared dispatchers' escape hatch
|
|
1469
|
+
* (M14 round-2 F1 precedent) so schema drift surfaces as
|
|
1470
|
+
* top-level `ok: false` rather than per-record.
|
|
1471
|
+
* 4. Cache invalidation — single `invalidateBoard(boardId, env)`
|
|
1472
|
+
* after the dispatch loop completes (mirrors M38's single-
|
|
1473
|
+
* leg invalidate timing; one board covers every matched
|
|
1474
|
+
* item's mutated `asset` slot).
|
|
1475
|
+
* 5. Envelope emit — `data: BulkFileSetData` with the
|
|
1476
|
+
* `operation: 'item_update_bulk_file_set'` literal
|
|
1477
|
+
* discriminator + per-item `results[]` + aggregate
|
|
1478
|
+
* `summary.{matched_count, applied_count, failed_count,
|
|
1479
|
+
* board_id, column_id, filename, file_size_bytes}`. Warnings
|
|
1480
|
+
* threaded as `dedupeWarnings([...filterWarnings,
|
|
1481
|
+
* ...m38.warnings])` (mirrors the JSON-bulk path); source
|
|
1482
|
+
* derives from `metaSource` (cache-served metadata + live
|
|
1483
|
+
* wire calls → `mixed`).
|
|
1484
|
+
*/
|
|
1485
|
+
export const runItemUpdateBulkFileDispatch = async (inputs) => {
|
|
1486
|
+
// 1) Single upfront pre-check (D3 closure). Whole-call abort
|
|
1487
|
+
// regardless of `--continue-on-error` per cli-design §5.8 —
|
|
1488
|
+
// the local file pre-check is shared across N matched items,
|
|
1489
|
+
// so failure aborts the whole call before any multipart wire
|
|
1490
|
+
// leg fires. `precheckLocalFile` throws `usage_error` with
|
|
1491
|
+
// `details.reason: 'file_not_readable'` / `'file_empty'`
|
|
1492
|
+
// (M31 single-item discriminators reused per D4 zero-delta
|
|
1493
|
+
// closure).
|
|
1494
|
+
const precheck = await precheckLocalFile(inputs.m38.rawValue);
|
|
1495
|
+
// Combined warnings list threaded into every envelope emit below.
|
|
1496
|
+
// Mirrors the JSON-bulk `dedupeWarnings(filter ∪ m38)` pattern at
|
|
1497
|
+
// the action body; key is `code+message+token` so a pre-check
|
|
1498
|
+
// `stale_cache_refreshed` plus a filter-time `stale_cache_refreshed`
|
|
1499
|
+
// for the SAME token collapse to one entry.
|
|
1500
|
+
const combinedWarnings = dedupeWarnings([
|
|
1501
|
+
...inputs.filterWarnings,
|
|
1502
|
+
...inputs.m38.warnings,
|
|
1503
|
+
]);
|
|
1504
|
+
// Source/cache-age aggregator. Seeded with the metadata leg, then
|
|
1505
|
+
// folds the M38 pre-check leg + a synthetic 'live' leg representing
|
|
1506
|
+
// the items_page walker (always live, fired upstream before this
|
|
1507
|
+
// helper). On dry-run the dispatch leg never fires; on live the
|
|
1508
|
+
// dispatch leg adds another 'live' record (idempotent under
|
|
1509
|
+
// `mergeSource` since 'live' + 'live' = 'live'). Codex IMPL R1
|
|
1510
|
+
// P2-1 fix — pre-fix the helper dropped `inputs.m38.source` +
|
|
1511
|
+
// `inputs.m38.cacheAgeSeconds`, so a cache-served file-column
|
|
1512
|
+
// resolution after a live metadata fetch surfaced `'live'`
|
|
1513
|
+
// instead of `'mixed'`. Mirrors the JSON-bulk path's
|
|
1514
|
+
// SourceAggregator pattern at runBulk's dry-run + live legs.
|
|
1515
|
+
const sourceAgg = new SourceAggregator({
|
|
1516
|
+
source: inputs.metaSource,
|
|
1517
|
+
cacheAgeSeconds: inputs.metaCacheAgeSeconds,
|
|
1518
|
+
});
|
|
1519
|
+
if (inputs.m38.source !== undefined) {
|
|
1520
|
+
sourceAgg.record(inputs.m38.source, inputs.m38.cacheAgeSeconds);
|
|
1521
|
+
}
|
|
1522
|
+
// items_page walker always fires live before reaching the helper
|
|
1523
|
+
// (the empty-match short-circuit is the only path that skips it,
|
|
1524
|
+
// and that path emits the envelope upstream — this helper never
|
|
1525
|
+
// sees matchedItemIds.length === 0). Record one synthetic 'live'
|
|
1526
|
+
// leg so the aggregate reflects the wire round-trip cost the
|
|
1527
|
+
// caller already paid.
|
|
1528
|
+
sourceAgg.record('live', null);
|
|
1529
|
+
// 2) Dry-run branch — D4-shaped envelope. One
|
|
1530
|
+
// `add_file_to_column` planned_change per matched item-ID;
|
|
1531
|
+
// no file bytes loaded, no multipart wire round-trip. Unlike
|
|
1532
|
+
// the M38 single-item dry-run (which pins `source: 'none'`
|
|
1533
|
+
// because its dry-run is pure-local), bulk dry-run carries
|
|
1534
|
+
// the aggregated upstream `source` — metadata load + items_page
|
|
1535
|
+
// walk + M38 pre-check already paid for wire legs to reach
|
|
1536
|
+
// here.
|
|
1537
|
+
if (inputs.isDryRun) {
|
|
1538
|
+
const plannedChanges = inputs.matchedItemIds.map((itemId) => ({
|
|
1539
|
+
operation: 'add_file_to_column',
|
|
1540
|
+
item_id: itemId,
|
|
1541
|
+
column_id: inputs.m38.columnId,
|
|
1542
|
+
file_path: inputs.m38.rawValue,
|
|
1543
|
+
filename: precheck.filename,
|
|
1544
|
+
file_size_bytes: precheck.fileSizeBytes,
|
|
1545
|
+
}));
|
|
1546
|
+
const dryRunAgg = sourceAgg.result();
|
|
1547
|
+
emitDryRun({
|
|
1548
|
+
ctx: inputs.ctx,
|
|
1549
|
+
programOpts: inputs.programOpts,
|
|
1550
|
+
plannedChanges,
|
|
1551
|
+
source: dryRunAgg.source,
|
|
1552
|
+
cacheAgeSeconds: dryRunAgg.cacheAgeSeconds,
|
|
1553
|
+
warnings: combinedWarnings,
|
|
1554
|
+
apiVersion: inputs.apiVersion,
|
|
1555
|
+
});
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
// 3) Live dispatch. Build the shared `FileColumnSetEntry` once —
|
|
1559
|
+
// every per-item leg uses the SAME local file (one path × N
|
|
1560
|
+
// items), so `buildBlobFromPath` (inside `executeFileColumnSet`)
|
|
1561
|
+
// re-reads the bytes per leg rather than sharing one Blob
|
|
1562
|
+
// instance. Re-reading per-leg is the simpler shape; a future
|
|
1563
|
+
// optimisation could memoise the bytes if profiling motivates
|
|
1564
|
+
// it (~bytes × N reads vs ~bytes × 1 read + held in memory).
|
|
1565
|
+
const entry = {
|
|
1566
|
+
columnId: inputs.m38.columnId,
|
|
1567
|
+
columnType: 'file',
|
|
1568
|
+
rawValue: inputs.m38.rawValue,
|
|
1569
|
+
filePath: precheck.filePath,
|
|
1570
|
+
filename: precheck.filename,
|
|
1571
|
+
fileSizeBytes: precheck.fileSizeBytes,
|
|
1572
|
+
};
|
|
1573
|
+
// Live dispatch is always 'live' — fold one more leg into the
|
|
1574
|
+
// aggregator. Idempotent if metadata + pre-check were also live
|
|
1575
|
+
// ('live' + 'live' = 'live'); promotes 'cache' to 'mixed' when
|
|
1576
|
+
// metadata/pre-check served from cache.
|
|
1577
|
+
sourceAgg.record('live', null);
|
|
1578
|
+
const liveAgg = sourceAgg.result();
|
|
1579
|
+
// resolved_ids slot — pre-check returned the resolved column ID
|
|
1580
|
+
// for the file token; echo it into the envelope's
|
|
1581
|
+
// `meta.resolved_ids` so agents can confirm token-to-ID resolution
|
|
1582
|
+
// (mirrors the M38 single-item envelope at `runItemUpdateSingleFileDispatch`).
|
|
1583
|
+
const resolvedIds = { [inputs.m38.token]: inputs.m38.columnId };
|
|
1584
|
+
// Resolution source for foldAndRemap — defaults to 'live' when
|
|
1585
|
+
// pre-check didn't record one (no resolveColumnWithRefresh leg
|
|
1586
|
+
// fired; the only path that's possible is the no-setEntries
|
|
1587
|
+
// shortcut, which doesn't reach this helper). The remap probe
|
|
1588
|
+
// refreshes board metadata + re-checks the file column's
|
|
1589
|
+
// `archived` flag when a Monday-side `validation_failed`
|
|
1590
|
+
// surfaces against cache-served file-column resolution. Codex
|
|
1591
|
+
// IMPL R1 P1-1 fix.
|
|
1592
|
+
const remapSource = inputs.m38.source ?? 'live';
|
|
1593
|
+
// Discriminator for the dispatch shape: fail-fast (default,
|
|
1594
|
+
// applied to the v0.1 fail-fast bulk path's `applied_to` decoration)
|
|
1595
|
+
// vs `--continue-on-error` (partial-success per-record envelope).
|
|
1596
|
+
// M30 D2 closure pins `--concurrency requires --continue-on-error`,
|
|
1597
|
+
// so fail-fast bulk file dispatch is always sequential N=1.
|
|
1598
|
+
const continueOnError = inputs.parsed.continueOnError === true;
|
|
1599
|
+
if (!continueOnError) {
|
|
1600
|
+
// Fail-fast bulk file dispatch. Sequential loop; first per-item
|
|
1601
|
+
// failure aborts whole-call. Track applied (item_id, asset)
|
|
1602
|
+
// pairs so the failure decoration can echo `applied_to` (matches
|
|
1603
|
+
// the JSON-bulk fail-fast pattern at runBulk's main loop).
|
|
1604
|
+
const appliedAssets = [];
|
|
1605
|
+
for (const itemId of inputs.matchedItemIds) {
|
|
1606
|
+
try {
|
|
1607
|
+
const result = await executeFileColumnSet({
|
|
1608
|
+
client: inputs.client,
|
|
1609
|
+
multipart: inputs.multipart,
|
|
1610
|
+
itemId,
|
|
1611
|
+
entry,
|
|
1612
|
+
signal: inputs.ctx.signal,
|
|
1613
|
+
retries: inputs.retries,
|
|
1614
|
+
});
|
|
1615
|
+
appliedAssets.push({ itemId, asset: result.asset });
|
|
1616
|
+
}
|
|
1617
|
+
catch (err) {
|
|
1618
|
+
if (err instanceof MondayCliError) {
|
|
1619
|
+
// Codex IMPL R1 P2-2 fix: if any prior item applied
|
|
1620
|
+
// successfully, the board's asset state already mutated
|
|
1621
|
+
// wire-side — invalidate the cache BEFORE re-throwing the
|
|
1622
|
+
// fail-fast error so a follow-up read doesn't serve stale
|
|
1623
|
+
// metadata. Mirrors the M38 single-item invalidate-on-
|
|
1624
|
+
// success pattern; the JSON-bulk fail-fast path has the
|
|
1625
|
+
// same gap (unchanged by this commit — separate lift
|
|
1626
|
+
// candidate per the future-lift-candidate note in the
|
|
1627
|
+
// module docstring).
|
|
1628
|
+
if (appliedAssets.length > 0) {
|
|
1629
|
+
await invalidateBoard(inputs.boardId, inputs.ctx.env);
|
|
1630
|
+
}
|
|
1631
|
+
// Codex IMPL R1 P1-1 fix: apply `foldAndRemap` BEFORE
|
|
1632
|
+
// building the decoration so per-item failures inherit
|
|
1633
|
+
// the SAME `validation_failed` → `column_archived`
|
|
1634
|
+
// stale-cache remap the JSON-bulk fail-fast path applies
|
|
1635
|
+
// (cli-design §6.5 stable-code rule). Without this, an
|
|
1636
|
+
// archived file column surfaces `validation_failed` on
|
|
1637
|
+
// file-bulk dispatch but `column_archived` on JSON-bulk
|
|
1638
|
+
// dispatch — agents keying on the stable code see
|
|
1639
|
+
// inconsistent outcomes for the same root cause.
|
|
1640
|
+
const remapped = await foldAndRemap({
|
|
1641
|
+
err,
|
|
1642
|
+
warnings: inputs.m38.warnings,
|
|
1643
|
+
client: inputs.client,
|
|
1644
|
+
boardId: inputs.boardId,
|
|
1645
|
+
columnIds: [inputs.m38.columnId],
|
|
1646
|
+
env: inputs.ctx.env,
|
|
1647
|
+
noCache: inputs.noCache,
|
|
1648
|
+
resolutionSource: remapSource,
|
|
1649
|
+
});
|
|
1650
|
+
// Same decoration shape as the JSON-bulk fail-fast path
|
|
1651
|
+
// (lines ~1334-1361 above). Preserves the existing error
|
|
1652
|
+
// class' fields and grafts `applied_count` / `applied_to`
|
|
1653
|
+
// / `failed_at_item` / `matched_count` onto `details`.
|
|
1654
|
+
const existing = remapped.details ?? {};
|
|
1655
|
+
const decoration = {
|
|
1656
|
+
...existing,
|
|
1657
|
+
applied_count: appliedAssets.length,
|
|
1658
|
+
applied_to: appliedAssets.map((a) => a.itemId),
|
|
1659
|
+
failed_at_item: itemId,
|
|
1660
|
+
matched_count: inputs.matchedItemIds.length,
|
|
1661
|
+
};
|
|
1662
|
+
if (remapped.code === 'usage_error') {
|
|
1663
|
+
throw new UsageError(remapped.message, {
|
|
1664
|
+
...(remapped.cause === undefined
|
|
1665
|
+
? {}
|
|
1666
|
+
: { cause: remapped.cause }),
|
|
1667
|
+
details: decoration,
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
throw new ApiError(remapped.code, remapped.message, {
|
|
1671
|
+
...(remapped.cause === undefined
|
|
1672
|
+
? {}
|
|
1673
|
+
: { cause: remapped.cause }),
|
|
1674
|
+
...(remapped.httpStatus === undefined
|
|
1675
|
+
? {}
|
|
1676
|
+
: { httpStatus: remapped.httpStatus }),
|
|
1677
|
+
...(remapped.mondayCode === undefined
|
|
1678
|
+
? {}
|
|
1679
|
+
: { mondayCode: remapped.mondayCode }),
|
|
1680
|
+
...(remapped.requestId === undefined
|
|
1681
|
+
? {}
|
|
1682
|
+
: { requestId: remapped.requestId }),
|
|
1683
|
+
retryable: remapped.retryable,
|
|
1684
|
+
...(remapped.retryAfterSeconds === undefined
|
|
1685
|
+
? {}
|
|
1686
|
+
: { retryAfterSeconds: remapped.retryAfterSeconds }),
|
|
1687
|
+
details: decoration,
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
// Non-CliError programmer bug — re-throw to the runner's
|
|
1691
|
+
// catch-all (surfaces as `internal_error` whole-call;
|
|
1692
|
+
// mirrors the JSON-bulk fail-fast path). The partial-
|
|
1693
|
+
// success invalidate above doesn't fire for this branch
|
|
1694
|
+
// because non-CliError throws indicate broken contract,
|
|
1695
|
+
// not partial-mutation state worth preserving.
|
|
1696
|
+
throw err;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
// Every item applied — single board-cache invalidate before emit
|
|
1700
|
+
// (mirrors M38 single-leg invalidate timing).
|
|
1701
|
+
await invalidateBoard(inputs.boardId, inputs.ctx.env);
|
|
1702
|
+
const results = appliedAssets.map(({ itemId, asset }) => ({
|
|
1703
|
+
item_id: itemId,
|
|
1704
|
+
ok: true,
|
|
1705
|
+
asset,
|
|
1706
|
+
}));
|
|
1707
|
+
const data = {
|
|
1708
|
+
operation: 'item_update_bulk_file_set',
|
|
1709
|
+
summary: {
|
|
1710
|
+
matched_count: inputs.matchedItemIds.length,
|
|
1711
|
+
applied_count: appliedAssets.length,
|
|
1712
|
+
failed_count: 0,
|
|
1713
|
+
board_id: inputs.boardId,
|
|
1714
|
+
column_id: inputs.m38.columnId,
|
|
1715
|
+
filename: precheck.filename,
|
|
1716
|
+
file_size_bytes: precheck.fileSizeBytes,
|
|
1717
|
+
},
|
|
1718
|
+
results,
|
|
1719
|
+
};
|
|
1720
|
+
emitMutation({
|
|
1721
|
+
ctx: inputs.ctx,
|
|
1722
|
+
data,
|
|
1723
|
+
schema: bulkFileSetDataSchema,
|
|
1724
|
+
programOpts: inputs.programOpts,
|
|
1725
|
+
warnings: combinedWarnings,
|
|
1726
|
+
source: liveAgg.source,
|
|
1727
|
+
cacheAgeSeconds: liveAgg.cacheAgeSeconds,
|
|
1728
|
+
apiVersion: inputs.apiVersion,
|
|
1729
|
+
resolvedIds,
|
|
1730
|
+
});
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
// `--continue-on-error` path. Per-item failures land as per-record
|
|
1734
|
+
// `error: {code, message}` slots; the envelope is `ok: true`
|
|
1735
|
+
// regardless of how many items failed (universal partial-success
|
|
1736
|
+
// rule per cli-design §6.4). `internal_error` re-throws whole-call
|
|
1737
|
+
// via the shared dispatcher's escape hatch — schema drift in the
|
|
1738
|
+
// multipart response surfaces as top-level `ok: false`, not
|
|
1739
|
+
// papered over as a per-record slot.
|
|
1740
|
+
const assetById = new Map();
|
|
1741
|
+
const perTargetDispatch = async ({ targetId, }) => {
|
|
1742
|
+
try {
|
|
1743
|
+
const result = await executeFileColumnSet({
|
|
1744
|
+
client: inputs.client,
|
|
1745
|
+
multipart: inputs.multipart,
|
|
1746
|
+
itemId: targetId,
|
|
1747
|
+
entry,
|
|
1748
|
+
signal: inputs.ctx.signal,
|
|
1749
|
+
retries: inputs.retries,
|
|
1750
|
+
});
|
|
1751
|
+
assetById.set(targetId, result.asset);
|
|
1752
|
+
}
|
|
1753
|
+
catch (err) {
|
|
1754
|
+
if (err instanceof MondayCliError) {
|
|
1755
|
+
// Codex IMPL R1 P1-1 fix: apply `foldAndRemap` BEFORE
|
|
1756
|
+
// re-throwing into the shared dispatcher so per-record
|
|
1757
|
+
// `error.code` carries the SAME `column_archived` stable
|
|
1758
|
+
// code the JSON-bulk partial-success path emits. Mirrors
|
|
1759
|
+
// `runPartialSuccessBulkUpdate`'s perTargetDispatch
|
|
1760
|
+
// closure (src/api/partial-success-bulk.ts:431-475).
|
|
1761
|
+
// `foldAndRemap` NEVER converts a non-`internal_error`
|
|
1762
|
+
// into `internal_error`, so the dispatcher's
|
|
1763
|
+
// `internal_error` re-throw escape hatch (M14 round-2 F1)
|
|
1764
|
+
// stays intact: schema drift still surfaces as top-level
|
|
1765
|
+
// `ok: false` rather than papered over as a per-record
|
|
1766
|
+
// slot.
|
|
1767
|
+
const remapped = await foldAndRemap({
|
|
1768
|
+
err,
|
|
1769
|
+
warnings: inputs.m38.warnings,
|
|
1770
|
+
client: inputs.client,
|
|
1771
|
+
boardId: inputs.boardId,
|
|
1772
|
+
columnIds: [inputs.m38.columnId],
|
|
1773
|
+
env: inputs.ctx.env,
|
|
1774
|
+
noCache: inputs.noCache,
|
|
1775
|
+
resolutionSource: remapSource,
|
|
1776
|
+
});
|
|
1777
|
+
throw remapped;
|
|
1778
|
+
}
|
|
1779
|
+
// Non-CliError — programmer bug. Re-throw through
|
|
1780
|
+
// dispatchSequential / dispatchParallel's non-CliError
|
|
1781
|
+
// branch so the runner's catch-all surfaces as
|
|
1782
|
+
// internal_error (whole-call, not per-record).
|
|
1783
|
+
throw err;
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
// Routing — `--concurrency > 1` routes through dispatchParallel
|
|
1787
|
+
// (bounded async-pool); absent / `=== 1` routes through
|
|
1788
|
+
// dispatchSequential. Both dispatchers thread the optional
|
|
1789
|
+
// `signal` so SIGINT-aware callers see consistent cooperative-
|
|
1790
|
+
// abort semantics (R-NEW-28 axis 6 — identical between routes).
|
|
1791
|
+
let dispatchResults;
|
|
1792
|
+
if (inputs.parsed.concurrency !== undefined &&
|
|
1793
|
+
inputs.parsed.concurrency > 1) {
|
|
1794
|
+
dispatchResults = await dispatchParallel(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.parsed.concurrency, inputs.ctx.signal);
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
dispatchResults = await dispatchSequential(inputs.matchedItemIds, 'item_id', perTargetDispatch, inputs.ctx.signal);
|
|
1798
|
+
}
|
|
1799
|
+
// Single post-dispatch invalidate. Fires even when every per-item
|
|
1800
|
+
// dispatch failed (failed_count === matched_count) — wire calls
|
|
1801
|
+
// still fired against Monday, so the metadata cache's view of the
|
|
1802
|
+
// file column's content count may be stale even if the asset
|
|
1803
|
+
// didn't land. Cheap to always fire; expensive if missed.
|
|
1804
|
+
await invalidateBoard(inputs.boardId, inputs.ctx.env);
|
|
1805
|
+
// Fold dispatcher results + side-map assets into the
|
|
1806
|
+
// BulkFileSetResult[] shape. Mirrors `foldPartialSuccessBulkResult`
|
|
1807
|
+
// (`src/api/partial-success-bulk.ts`) but with the file-dispatch's
|
|
1808
|
+
// `asset` slot replacing the JSON path's `item` projection.
|
|
1809
|
+
const results = dispatchResults.map((row) => {
|
|
1810
|
+
const itemIdSlot = row.item_id;
|
|
1811
|
+
/* c8 ignore next 8 — dispatcher contract: every result row
|
|
1812
|
+
carries the id-field slot (populated by the dispatch helper);
|
|
1813
|
+
this guard catches a contract violation that would surface as
|
|
1814
|
+
a programmer bug, not a Monday-side failure. */
|
|
1815
|
+
if (typeof itemIdSlot !== 'string' || itemIdSlot.length === 0) {
|
|
1816
|
+
throw new ApiError('internal_error', 'bulk file dispatch result row is missing the `item_id` field — dispatcher contract violation.', { details: { record_keys: Object.keys(row) } });
|
|
1817
|
+
}
|
|
1818
|
+
if (row.ok) {
|
|
1819
|
+
const asset = assetById.get(itemIdSlot);
|
|
1820
|
+
/* c8 ignore next 8 — side-map invariant: every successful
|
|
1821
|
+
per-target dispatch records the asset; this guard catches a
|
|
1822
|
+
wrapper-layer miss (programmer bug). */
|
|
1823
|
+
if (asset === undefined) {
|
|
1824
|
+
throw new ApiError('internal_error', `bulk file dispatch result row for item_id ${itemIdSlot} reported ok: true but no Asset was captured — wrapper-layer side-map miss.`, { details: { item_id: itemIdSlot } });
|
|
1825
|
+
}
|
|
1826
|
+
return { item_id: itemIdSlot, ok: true, asset };
|
|
1827
|
+
}
|
|
1828
|
+
/* c8 ignore next 8 — dispatcher contract: every `ok: false` row
|
|
1829
|
+
carries the `error` slot (populated by the shared dispatcher's
|
|
1830
|
+
per-target error decoration). */
|
|
1831
|
+
if (row.error === undefined) {
|
|
1832
|
+
throw new ApiError('internal_error', `bulk file dispatch result row for item_id ${itemIdSlot} reported ok: false but no error payload was captured — dispatcher contract violation.`, { details: { item_id: itemIdSlot } });
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
item_id: itemIdSlot,
|
|
1836
|
+
ok: false,
|
|
1837
|
+
error: { code: row.error.code, message: row.error.message },
|
|
1838
|
+
};
|
|
1839
|
+
});
|
|
1840
|
+
const appliedCount = results.filter((r) => r.ok).length;
|
|
1841
|
+
const failedCount = results.filter((r) => !r.ok).length;
|
|
1842
|
+
/* c8 ignore next 11 — invariant: every matched item produces
|
|
1843
|
+
exactly one result row (success or failure) under both
|
|
1844
|
+
dispatchers; mismatch would indicate a programmer bug in the
|
|
1845
|
+
dispatcher or the fold. Mirrors `buildPartialSuccessBulkSummary`'s
|
|
1846
|
+
defensive check. */
|
|
1847
|
+
if (appliedCount + failedCount !== inputs.matchedItemIds.length) {
|
|
1848
|
+
throw new ApiError('internal_error', `bulk file dispatch summary invariant violated — matched_count (${String(inputs.matchedItemIds.length)}) !== applied_count (${String(appliedCount)}) + failed_count (${String(failedCount)}).`, {
|
|
1849
|
+
details: {
|
|
1850
|
+
matched_count: inputs.matchedItemIds.length,
|
|
1851
|
+
applied_count: appliedCount,
|
|
1852
|
+
failed_count: failedCount,
|
|
1853
|
+
board_id: inputs.boardId,
|
|
1854
|
+
},
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
const data = {
|
|
1858
|
+
operation: 'item_update_bulk_file_set',
|
|
1859
|
+
summary: {
|
|
1860
|
+
matched_count: inputs.matchedItemIds.length,
|
|
1861
|
+
applied_count: appliedCount,
|
|
1862
|
+
failed_count: failedCount,
|
|
1863
|
+
board_id: inputs.boardId,
|
|
1864
|
+
column_id: inputs.m38.columnId,
|
|
1865
|
+
filename: precheck.filename,
|
|
1866
|
+
file_size_bytes: precheck.fileSizeBytes,
|
|
1867
|
+
},
|
|
1868
|
+
results,
|
|
1869
|
+
};
|
|
1870
|
+
emitMutation({
|
|
1871
|
+
ctx: inputs.ctx,
|
|
1872
|
+
data,
|
|
1873
|
+
schema: bulkFileSetDataSchema,
|
|
1874
|
+
programOpts: inputs.programOpts,
|
|
1875
|
+
warnings: combinedWarnings,
|
|
1876
|
+
source: liveAgg.source,
|
|
1877
|
+
cacheAgeSeconds: liveAgg.cacheAgeSeconds,
|
|
1878
|
+
apiVersion: inputs.apiVersion,
|
|
1879
|
+
resolvedIds,
|
|
1880
|
+
});
|
|
1881
|
+
};
|
|
935
1882
|
//# sourceMappingURL=update.js.map
|