kalup 0.5.0 → 0.6.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/dist/{commands-D_Q8-jc5.mjs → commands-DdogKJKg.mjs} +824 -357
- package/dist/commands.d.mts +1 -1
- package/dist/commands.mjs +1 -1
- package/dist/{context-0mrva_6H.d.mts → context-BgFzBPj9.d.mts} +10 -0
- package/dist/{host-BkBYlh_7.mjs → host-BSYg3I66.mjs} +1 -1
- package/dist/host.d.mts +1 -1
- package/dist/host.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/schemas/ir-1.schema.json +67 -98
- package/dist/schemas/plan-1.schema.json +15 -6
- package/dist/schemas/state-1.schema.json +17 -5
- package/docs/compare.md +1 -1
- package/docs/errors/E_PLAN_INVALID.md +2 -0
- package/docs/errors/W_SETTLING.md +19 -0
- package/docs/plan.md +5 -3
- package/docs/pull.md +2 -0
- package/docs/snapshot.md +1 -1
- package/docs/state.md +4 -3
- package/package.json +2 -2
|
@@ -108,7 +108,7 @@ var ir_1_schema_default = {
|
|
|
108
108
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
109
109
|
$id: "https://kalup.dev/schemas/ir-1.schema.json",
|
|
110
110
|
title: "Kalup IR, version 1",
|
|
111
|
-
description: "What a Kalup project's config files mean, derived by `kalup ir`, or what a portal holds, captured by `kalup snapshot` with an observation block.
|
|
111
|
+
description: "What a Kalup project's config files mean, derived by `kalup ir`, or what a portal holds, captured by `kalup snapshot` with an observation block. Open for additions within ir/1 (docs/compatibility.md): the document, each resource and everything below it accept fields this schema does not list, and readers keep or ignore them; a later 1.x may add a resource type, and a reader reports an address of a type it does not handle as unknown. Closed, because a new value changes what a reader may conclude: generator, a $ref, a tombstone, the read statuses in coverage, lifecycle.options, and a target's drift and adopt.",
|
|
112
112
|
type: "object",
|
|
113
113
|
required: [
|
|
114
114
|
"irVersion",
|
|
@@ -264,29 +264,14 @@ var ir_1_schema_default = {
|
|
|
264
264
|
"properties": {
|
|
265
265
|
"label": { "type": "string" },
|
|
266
266
|
"group": { "$ref": "#/$defs/ref" },
|
|
267
|
-
"type": {
|
|
268
|
-
"string",
|
|
269
|
-
"number"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
] },
|
|
276
|
-
"fieldType": { "enum": [
|
|
277
|
-
"text",
|
|
278
|
-
"textarea",
|
|
279
|
-
"file",
|
|
280
|
-
"phonenumber",
|
|
281
|
-
"number",
|
|
282
|
-
"booleancheckbox",
|
|
283
|
-
"select",
|
|
284
|
-
"radio",
|
|
285
|
-
"checkbox",
|
|
286
|
-
"date",
|
|
287
|
-
"html",
|
|
288
|
-
"calculation_equation"
|
|
289
|
-
] },
|
|
267
|
+
"type": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"description": "A value HubSpot defines; known values: string, number, bool, date, datetime, enumeration, phone_number. A later version may record one HubSpot adds."
|
|
270
|
+
},
|
|
271
|
+
"fieldType": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "A value HubSpot defines; known values: text, textarea, file, phonenumber, number, booleancheckbox, select, radio, checkbox, date, html, calculation_equation. A later version may record one HubSpot adds."
|
|
274
|
+
},
|
|
290
275
|
"description": { "type": "string" },
|
|
291
276
|
"options": {
|
|
292
277
|
"type": "array",
|
|
@@ -299,36 +284,25 @@ var ir_1_schema_default = {
|
|
|
299
284
|
"type": "integer",
|
|
300
285
|
"minimum": -1
|
|
301
286
|
},
|
|
302
|
-
"numberDisplayHint": {
|
|
303
|
-
"
|
|
304
|
-
"duration"
|
|
305
|
-
|
|
306
|
-
"percentage",
|
|
307
|
-
"probability",
|
|
308
|
-
"unformatted"
|
|
309
|
-
] },
|
|
287
|
+
"numberDisplayHint": {
|
|
288
|
+
"type": "string",
|
|
289
|
+
"description": "A value HubSpot defines; known values: currency, duration, formatted, percentage, probability, unformatted. A later version may record one HubSpot adds."
|
|
290
|
+
},
|
|
310
291
|
"showCurrencySymbol": { "type": "boolean" },
|
|
311
292
|
"currencyPropertyName": { "type": "string" },
|
|
312
|
-
"textDisplayHint": {
|
|
313
|
-
"
|
|
314
|
-
"email"
|
|
315
|
-
|
|
316
|
-
"multi_line",
|
|
317
|
-
"phone_number",
|
|
318
|
-
"physical_address",
|
|
319
|
-
"postal_code",
|
|
320
|
-
"unformatted_single_line"
|
|
321
|
-
] },
|
|
293
|
+
"textDisplayHint": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"description": "A value HubSpot defines; known values: domain_name, email, ip_address, multi_line, phone_number, physical_address, postal_code, unformatted_single_line. A later version may record one HubSpot adds."
|
|
296
|
+
},
|
|
322
297
|
"calculationFormula": { "type": "string" },
|
|
323
|
-
"dataSensitivity": {
|
|
324
|
-
"
|
|
325
|
-
"sensitive"
|
|
326
|
-
|
|
327
|
-
] },
|
|
298
|
+
"dataSensitivity": {
|
|
299
|
+
"type": "string",
|
|
300
|
+
"description": "A value HubSpot defines; known values: non_sensitive, sensitive, highly_sensitive. A later version may record one HubSpot adds."
|
|
301
|
+
},
|
|
328
302
|
"externalOptions": { "const": true },
|
|
329
303
|
"referencedObjectType": { "const": "OWNER" }
|
|
330
304
|
},
|
|
331
|
-
"additionalProperties":
|
|
305
|
+
"additionalProperties": true
|
|
332
306
|
},
|
|
333
307
|
"option": {
|
|
334
308
|
"description": "One enumeration option. Array index is display order. The app alias lives in binding.aliases, never here.",
|
|
@@ -340,13 +314,14 @@ var ir_1_schema_default = {
|
|
|
340
314
|
"hidden": { "type": "boolean" },
|
|
341
315
|
"description": { "type": "string" }
|
|
342
316
|
},
|
|
343
|
-
"additionalProperties":
|
|
317
|
+
"additionalProperties": true,
|
|
318
|
+
"patternProperties": { "^as$": false }
|
|
344
319
|
},
|
|
345
320
|
"groupDefinition": {
|
|
346
321
|
"type": "object",
|
|
347
322
|
"required": ["label"],
|
|
348
323
|
"properties": { "label": { "type": "string" } },
|
|
349
|
-
"additionalProperties":
|
|
324
|
+
"additionalProperties": true
|
|
350
325
|
},
|
|
351
326
|
"objectDefinition": {
|
|
352
327
|
"description": "A custom object schema. Its properties and groups are resources of their own.",
|
|
@@ -360,7 +335,7 @@ var ir_1_schema_default = {
|
|
|
360
335
|
"singular": { "type": "string" },
|
|
361
336
|
"plural": { "type": "string" }
|
|
362
337
|
},
|
|
363
|
-
"additionalProperties":
|
|
338
|
+
"additionalProperties": true
|
|
364
339
|
},
|
|
365
340
|
"description": { "type": "string" },
|
|
366
341
|
"primaryDisplayProperty": { "type": "string" },
|
|
@@ -377,7 +352,7 @@ var ir_1_schema_default = {
|
|
|
377
352
|
"items": { "type": "string" }
|
|
378
353
|
}
|
|
379
354
|
},
|
|
380
|
-
"additionalProperties":
|
|
355
|
+
"additionalProperties": true
|
|
381
356
|
},
|
|
382
357
|
"pipelineDefinition": {
|
|
383
358
|
"description": "A pipeline. Its stages are resources of their own; stages lists their IDs in display order.",
|
|
@@ -398,7 +373,7 @@ var ir_1_schema_default = {
|
|
|
398
373
|
"items": { "type": "string" }
|
|
399
374
|
}
|
|
400
375
|
},
|
|
401
|
-
"additionalProperties":
|
|
376
|
+
"additionalProperties": true
|
|
402
377
|
},
|
|
403
378
|
"stageDefinition": {
|
|
404
379
|
"description": "A pipeline stage. Its metadata field depends on the object: probability on deals, ticketState on tickets, state on custom objects.",
|
|
@@ -411,10 +386,16 @@ var ir_1_schema_default = {
|
|
|
411
386
|
"minimum": 0,
|
|
412
387
|
"maximum": 1
|
|
413
388
|
},
|
|
414
|
-
"ticketState": {
|
|
415
|
-
|
|
389
|
+
"ticketState": {
|
|
390
|
+
"type": "string",
|
|
391
|
+
"description": "A value HubSpot defines; known values: OPEN, CLOSED. A later version may record one HubSpot adds."
|
|
392
|
+
},
|
|
393
|
+
"state": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"description": "A value HubSpot defines; known values: OPEN, CLOSED. A later version may record one HubSpot adds."
|
|
396
|
+
}
|
|
416
397
|
},
|
|
417
|
-
"additionalProperties":
|
|
398
|
+
"additionalProperties": true
|
|
418
399
|
},
|
|
419
400
|
"associationDefinition": {
|
|
420
401
|
"description": "An association label between two objects: label as the from object shows it, inverseLabel as the to object does. With neither, the plain association of a pair that has no HubSpot-defined one.",
|
|
@@ -423,26 +404,18 @@ var ir_1_schema_default = {
|
|
|
423
404
|
"label": { "type": "string" },
|
|
424
405
|
"inverseLabel": { "type": "string" }
|
|
425
406
|
},
|
|
426
|
-
"additionalProperties":
|
|
407
|
+
"additionalProperties": true
|
|
427
408
|
},
|
|
428
409
|
"binding": {
|
|
429
410
|
"description": "App terms, filled in by the loader. key and codec for properties, export for objects.",
|
|
430
411
|
"type": "object",
|
|
431
412
|
"properties": {
|
|
432
413
|
"key": { "type": "string" },
|
|
433
|
-
"codec": {
|
|
434
|
-
"string",
|
|
435
|
-
"
|
|
436
|
-
"boolean"
|
|
437
|
-
|
|
438
|
-
"datetime",
|
|
439
|
-
"enum",
|
|
440
|
-
"multiEnum",
|
|
441
|
-
"stringArray",
|
|
442
|
-
"json",
|
|
443
|
-
"phoneNumber",
|
|
444
|
-
"owner"
|
|
445
|
-
] },
|
|
414
|
+
"codec": {
|
|
415
|
+
"type": "string",
|
|
416
|
+
"pattern": "^[a-zA-Z]+$",
|
|
417
|
+
"description": "The builder. Known: string, number, boolean, date, datetime, enum, multiEnum, stringArray, json, phoneNumber, owner. A later version may add a builder."
|
|
418
|
+
},
|
|
446
419
|
"aliases": {
|
|
447
420
|
"type": "object",
|
|
448
421
|
"additionalProperties": { "type": "string" }
|
|
@@ -452,7 +425,7 @@ var ir_1_schema_default = {
|
|
|
452
425
|
"strict": { "type": "boolean" },
|
|
453
426
|
"export": { "type": "string" }
|
|
454
427
|
},
|
|
455
|
-
"additionalProperties":
|
|
428
|
+
"additionalProperties": true
|
|
456
429
|
},
|
|
457
430
|
"lifecycle": {
|
|
458
431
|
"type": "object",
|
|
@@ -469,7 +442,7 @@ var ir_1_schema_default = {
|
|
|
469
442
|
},
|
|
470
443
|
"preventDestroy": { "type": "boolean" }
|
|
471
444
|
},
|
|
472
|
-
"additionalProperties":
|
|
445
|
+
"additionalProperties": true
|
|
473
446
|
},
|
|
474
447
|
"provenance": {
|
|
475
448
|
"description": "Merged from blueprints.lock.json in the folder of object files by the loader. Absent means authored by hand.",
|
|
@@ -488,7 +461,7 @@ var ir_1_schema_default = {
|
|
|
488
461
|
"prefix": { "type": "string" },
|
|
489
462
|
"hash": { "type": "string" }
|
|
490
463
|
},
|
|
491
|
-
"additionalProperties":
|
|
464
|
+
"additionalProperties": true
|
|
492
465
|
},
|
|
493
466
|
"target": {
|
|
494
467
|
"description": "One named portal. Credentials never enter the IR.",
|
|
@@ -521,7 +494,8 @@ var ir_1_schema_default = {
|
|
|
521
494
|
"additionalProperties": false
|
|
522
495
|
}
|
|
523
496
|
},
|
|
524
|
-
"additionalProperties":
|
|
497
|
+
"additionalProperties": true,
|
|
498
|
+
"patternProperties": { "^credentials$": false }
|
|
525
499
|
},
|
|
526
500
|
"override": {
|
|
527
501
|
"type": "object",
|
|
@@ -534,7 +508,7 @@ var ir_1_schema_default = {
|
|
|
534
508
|
"additionalProperties": { "type": "string" }
|
|
535
509
|
}
|
|
536
510
|
},
|
|
537
|
-
"additionalProperties":
|
|
511
|
+
"additionalProperties": true
|
|
538
512
|
},
|
|
539
513
|
"observation": {
|
|
540
514
|
"description": "What a snapshot read: the target, when, and how completely. A portal-frontend document requires it. observedAt is the one timestamp an ir/1 document carries.",
|
|
@@ -564,7 +538,7 @@ var ir_1_schema_default = {
|
|
|
564
538
|
},
|
|
565
539
|
"coverage": { "$ref": "#/$defs/coverage" }
|
|
566
540
|
},
|
|
567
|
-
"additionalProperties":
|
|
541
|
+
"additionalProperties": true
|
|
568
542
|
},
|
|
569
543
|
"coverage": {
|
|
570
544
|
"description": "What the read covered. Only a complete read proves that a resource is absent.",
|
|
@@ -589,44 +563,38 @@ var ir_1_schema_default = {
|
|
|
589
563
|
"if": { "type": "string" },
|
|
590
564
|
"then": { "const": "unknown" }
|
|
591
565
|
},
|
|
566
|
+
"settling": {
|
|
567
|
+
"description": "Resources the read cannot be trusted on yet, by address: apply wrote them minutes ago and HubSpot still serves an older copy (stale), or does not list one Kalup created (missing). Unknown until the time given, never absent or drifted.",
|
|
568
|
+
"type": "object",
|
|
569
|
+
"additionalProperties": {
|
|
570
|
+
"type": "object",
|
|
571
|
+
"required": ["reason", "until"],
|
|
572
|
+
"properties": {
|
|
573
|
+
"reason": {
|
|
574
|
+
"type": "string",
|
|
575
|
+
"description": "Why it settles. Known: stale, missing. A later version may add a reason."
|
|
576
|
+
},
|
|
577
|
+
"until": { "type": "string" }
|
|
578
|
+
},
|
|
579
|
+
"additionalProperties": true
|
|
580
|
+
}
|
|
581
|
+
},
|
|
592
582
|
"notCaptured": {
|
|
593
|
-
"description": "Documented response fields Kalup
|
|
583
|
+
"description": "Documented response fields Kalup drops, by resource type. A later version may add a type.",
|
|
594
584
|
"type": "object",
|
|
595
585
|
"required": [
|
|
596
586
|
"property",
|
|
597
587
|
"group",
|
|
598
588
|
"object"
|
|
599
589
|
],
|
|
600
|
-
"
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
"group": {
|
|
606
|
-
"type": "array",
|
|
607
|
-
"items": { "type": "string" }
|
|
608
|
-
},
|
|
609
|
-
"object": {
|
|
610
|
-
"type": "array",
|
|
611
|
-
"items": { "type": "string" }
|
|
612
|
-
},
|
|
613
|
-
"pipeline": {
|
|
614
|
-
"type": "array",
|
|
615
|
-
"items": { "type": "string" }
|
|
616
|
-
},
|
|
617
|
-
"stage": {
|
|
618
|
-
"type": "array",
|
|
619
|
-
"items": { "type": "string" }
|
|
620
|
-
},
|
|
621
|
-
"association": {
|
|
622
|
-
"type": "array",
|
|
623
|
-
"items": { "type": "string" }
|
|
624
|
-
}
|
|
625
|
-
},
|
|
626
|
-
"additionalProperties": false
|
|
590
|
+
"additionalProperties": false,
|
|
591
|
+
"patternProperties": { "^[a-z]+$": {
|
|
592
|
+
"type": "array",
|
|
593
|
+
"items": { "type": "string" }
|
|
594
|
+
} }
|
|
627
595
|
}
|
|
628
596
|
},
|
|
629
|
-
"additionalProperties":
|
|
597
|
+
"additionalProperties": true
|
|
630
598
|
},
|
|
631
599
|
"objectCoverage": {
|
|
632
600
|
"type": "object",
|
|
@@ -697,7 +665,7 @@ var ir_1_schema_default = {
|
|
|
697
665
|
"items": { "type": "string" }
|
|
698
666
|
}
|
|
699
667
|
},
|
|
700
|
-
"additionalProperties":
|
|
668
|
+
"additionalProperties": true
|
|
701
669
|
},
|
|
702
670
|
"excluded": {
|
|
703
671
|
"description": "Addresses a skip override leaves out.",
|
|
@@ -718,7 +686,7 @@ var ir_1_schema_default = {
|
|
|
718
686
|
"status": { "enum": ["read", "unreadable"] },
|
|
719
687
|
"missingScope": { "type": "string" }
|
|
720
688
|
},
|
|
721
|
-
"additionalProperties":
|
|
689
|
+
"additionalProperties": true
|
|
722
690
|
},
|
|
723
691
|
"associations": {
|
|
724
692
|
"description": "The associations of the pairs this object is in, pair by pair: per other object, whether the pair was read, and the user-defined types of this direction no name reached; and the HubSpot type IDs of each association addressed from this object. Absent when no pair with this object is in scope.",
|
|
@@ -737,10 +705,10 @@ var ir_1_schema_default = {
|
|
|
737
705
|
}
|
|
738
706
|
}
|
|
739
707
|
},
|
|
740
|
-
"additionalProperties":
|
|
708
|
+
"additionalProperties": true
|
|
741
709
|
}
|
|
742
710
|
},
|
|
743
|
-
"additionalProperties":
|
|
711
|
+
"additionalProperties": true
|
|
744
712
|
},
|
|
745
713
|
"unsupportedProperty": {
|
|
746
714
|
"description": "A present property Kalup does not write: no builder carries its type or fieldType, or HubSpot fills its options (owner or externalOptions). It is compared like any property but is not a resource.",
|
|
@@ -768,7 +736,7 @@ var ir_1_schema_default = {
|
|
|
768
736
|
"externalOptions": { "type": "boolean" },
|
|
769
737
|
"referencedObjectType": { "type": "string" }
|
|
770
738
|
},
|
|
771
|
-
"additionalProperties":
|
|
739
|
+
"additionalProperties": true
|
|
772
740
|
},
|
|
773
741
|
"tombstone": {
|
|
774
742
|
"description": "Written by `kalup rm`. destroy archives or deletes in the portal; release stops managing and leaves it.",
|
|
@@ -801,7 +769,7 @@ var ir_1_schema_default = {
|
|
|
801
769
|
}
|
|
802
770
|
}
|
|
803
771
|
},
|
|
804
|
-
"additionalProperties":
|
|
772
|
+
"additionalProperties": true
|
|
805
773
|
}
|
|
806
774
|
}
|
|
807
775
|
};
|
|
@@ -825,6 +793,11 @@ function sortKeys(value) {
|
|
|
825
793
|
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, sortKeys(record[key])]));
|
|
826
794
|
}
|
|
827
795
|
const IR_SCHEMA = ir_1_schema_default;
|
|
796
|
+
/** The fields of a read's coverage and of an object's coverage this version knows: any other is a later version's. */
|
|
797
|
+
const COVERAGE_FIELDS = {
|
|
798
|
+
coverage: new Set(Object.keys((IR_SCHEMA.$defs?.coverage)?.properties ?? {})),
|
|
799
|
+
object: new Set(Object.keys((IR_SCHEMA.$defs?.objectCoverage)?.properties ?? {}))
|
|
800
|
+
};
|
|
828
801
|
/** Checks a document against ir-1.schema.json. Empty when it conforms. */
|
|
829
802
|
function validateIR(document) {
|
|
830
803
|
return validateSchema(IR_SCHEMA, document).map(({ path, message }) => ({
|
|
@@ -865,7 +838,11 @@ function validateSchema(root, document) {
|
|
|
865
838
|
const own = ownEntry(schema.properties, name);
|
|
866
839
|
const patterns = Object.entries(schema.patternProperties ?? {}).filter(([pattern]) => new RegExp(pattern, "u").test(name));
|
|
867
840
|
if (own) check(own, field, path, out);
|
|
868
|
-
for (const [, pattern] of patterns)
|
|
841
|
+
for (const [, pattern] of patterns) if (pattern === false) out.push({
|
|
842
|
+
path,
|
|
843
|
+
message: `unexpected field "${name}"`
|
|
844
|
+
});
|
|
845
|
+
else check(pattern, field, path, out);
|
|
869
846
|
if (own || patterns.length > 0) return;
|
|
870
847
|
if (schema.additionalProperties === false) out.push({
|
|
871
848
|
path,
|
|
@@ -943,11 +920,14 @@ function assertSupported(value, at, root = false) {
|
|
|
943
920
|
if (part !== void 0 && !(keyword === "additionalProperties" && typeof part === "boolean")) assertSupported(part, `${at}/${keyword}`);
|
|
944
921
|
}
|
|
945
922
|
for (const [index, part] of (schema.allOf ?? []).entries()) assertSupported(part, `${at}/allOf/${index}`);
|
|
923
|
+
assertNamed(schema, at);
|
|
924
|
+
}
|
|
925
|
+
function assertNamed(schema, at) {
|
|
946
926
|
for (const keyword of [
|
|
947
927
|
"$defs",
|
|
948
928
|
"properties",
|
|
949
929
|
"patternProperties"
|
|
950
|
-
]) for (const [name, part] of Object.entries(schema[keyword] ?? {})) assertSupported(part, `${at}/${keyword}/${name}`);
|
|
930
|
+
]) for (const [name, part] of Object.entries(schema[keyword] ?? {})) if (!(keyword === "patternProperties" && part === false)) assertSupported(part, `${at}/${keyword}/${name}`);
|
|
951
931
|
}
|
|
952
932
|
function ownEntry(record, key) {
|
|
953
933
|
return record && Object.hasOwn(record, key) ? record[key] : void 0;
|
|
@@ -2492,7 +2472,7 @@ function parseProperty(s, path, key, cs) {
|
|
|
2492
2472
|
};
|
|
2493
2473
|
if (kind === "json") {
|
|
2494
2474
|
expect(s, "punct", ",", "add the validator as the second argument", path);
|
|
2495
|
-
prop.json = { validatorSource: opaque(s, path) };
|
|
2475
|
+
prop.json = { validatorSource: opaque$1(s, path) };
|
|
2496
2476
|
}
|
|
2497
2477
|
if (skip(s, ",") && !is(peek(s), "punct", ")")) {
|
|
2498
2478
|
prop.definition = definition(s, path);
|
|
@@ -2525,7 +2505,7 @@ function chainCall(s, m, path) {
|
|
|
2525
2505
|
if (!is(next(s), "punct", ")")) bad(`.${flag}() takes ${managed ? "only false" : "no argument"}`, call);
|
|
2526
2506
|
return flag;
|
|
2527
2507
|
}
|
|
2528
|
-
function opaque(s, path) {
|
|
2508
|
+
function opaque$1(s, path) {
|
|
2529
2509
|
const t = next(s);
|
|
2530
2510
|
if (t.kind !== "opaque" || !t.value) fail$1(s, "E_NOT_DATA", t, "p.json needs a validator", "write p.json(name, validator, {...})", path);
|
|
2531
2511
|
return t.value;
|
|
@@ -4493,7 +4473,7 @@ var state_1_schema_default = {
|
|
|
4493
4473
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4494
4474
|
$id: "https://kalup.dev/schemas/state-1.schema.json",
|
|
4495
4475
|
title: "Kalup portal state, version 1",
|
|
4496
|
-
description: ".kalup/state/portal-<portalId>.json. Describes one verified portal, not the code, and holds no target name. Covered by the compatibility policy (docs/compatibility.md):
|
|
4476
|
+
description: ".kalup/state/portal-<portalId>.json. Describes one verified portal, not the code, and holds no target name. Covered by the compatibility policy (docs/compatibility.md). Open for additions within kalup.state/1: a later 1.x may add top-level fields, resource entries of types this version does not plan, and fields on an entry. A reader keeps what it does not know: it never drops an entry of a type it does not handle or a top-level field it does not know, and it drops an entry's unknown fields only when it rewrites that entry. lastApply, an entry's attested and rewrites, and the base's option members stay closed. Kalup never writes a key, record data, unowned fields or unmanaged resources: its state writer refuses a file holding anything shaped like a key.",
|
|
4497
4477
|
type: "object",
|
|
4498
4478
|
required: [
|
|
4499
4479
|
"format",
|
|
@@ -4554,20 +4534,16 @@ var state_1_schema_default = {
|
|
|
4554
4534
|
"additionalProperties": false
|
|
4555
4535
|
}
|
|
4556
4536
|
},
|
|
4557
|
-
additionalProperties:
|
|
4537
|
+
additionalProperties: true,
|
|
4558
4538
|
$defs: {
|
|
4559
4539
|
"resourceState": {
|
|
4560
4540
|
"type": "object",
|
|
4561
4541
|
"required": ["origin", "id"],
|
|
4562
4542
|
"properties": {
|
|
4563
4543
|
"origin": {
|
|
4564
|
-
"description": "created and adopted are owned. pulled owns nothing: pull recorded the base of a resource no entry owned, and a plan still adopts it.",
|
|
4565
|
-
"
|
|
4566
|
-
|
|
4567
|
-
"adopted",
|
|
4568
|
-
"reference",
|
|
4569
|
-
"pulled"
|
|
4570
|
-
]
|
|
4544
|
+
"description": "created and adopted are owned. pulled owns nothing: pull recorded the base of a resource no entry owned, and a plan still adopts it. reference owns nothing. Another value, from a later 1.x, owns nothing here.",
|
|
4545
|
+
"type": "string",
|
|
4546
|
+
"pattern": "^[a-z]+$"
|
|
4571
4547
|
},
|
|
4572
4548
|
"id": {
|
|
4573
4549
|
"description": "The portal name the entry owns. It owns the resource only while this equals the name the address resolves to. null for runbook-only types.",
|
|
@@ -4612,9 +4588,18 @@ var state_1_schema_default = {
|
|
|
4612
4588
|
"description": "An association's HubSpot type IDs in this portal, its direction's first: they name it while HubSpot's schema read does not list its name yet.",
|
|
4613
4589
|
"type": "array",
|
|
4614
4590
|
"items": { "type": "integer" }
|
|
4591
|
+
},
|
|
4592
|
+
"written": {
|
|
4593
|
+
"description": "The units apply wrote within the settling window before its last write, each with when it verified the write. For a few minutes HubSpot may serve an older copy, so a read that disagrees on such a unit is settling: unknown, never drift.",
|
|
4594
|
+
"type": "object",
|
|
4595
|
+
"additionalProperties": { "type": "string" }
|
|
4596
|
+
},
|
|
4597
|
+
"writtenAt": {
|
|
4598
|
+
"description": "When apply last verified a write to the resource, whatever units it named. A read that does not show the resource within the settling window after it is settling: unknown, never absence.",
|
|
4599
|
+
"type": "string"
|
|
4615
4600
|
}
|
|
4616
4601
|
},
|
|
4617
|
-
"additionalProperties":
|
|
4602
|
+
"additionalProperties": true
|
|
4618
4603
|
},
|
|
4619
4604
|
"base": {
|
|
4620
4605
|
"description": "Per owned unit, the value config and portal last agreed on; possibly partial. Scalar units by field name. options: a map keyed by option value whose members hold the agreed label, hidden and description; a member with no fields means only its membership is agreed. optionsOrder: the agreed order of the members both sides held.",
|
|
@@ -4660,118 +4645,6 @@ var KalupError = class extends Error {
|
|
|
4660
4645
|
this.exitCode = exitCode;
|
|
4661
4646
|
}
|
|
4662
4647
|
};
|
|
4663
|
-
const ansi = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
4664
|
-
const control = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
4665
|
-
/** Strips ANSI sequences, control and bidirectional formatting characters and newlines, and caps the length. */
|
|
4666
|
-
function sanitize(text, max = 120) {
|
|
4667
|
-
const clean = text.replace(ansi, "").replace(control, "");
|
|
4668
|
-
const chars = Array.from(clean);
|
|
4669
|
-
return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : clean;
|
|
4670
|
-
}
|
|
4671
|
-
const STATE_SCHEMA = state_1_schema_default;
|
|
4672
|
-
/** Checks a document against state-1.schema.json. Empty when it conforms. */
|
|
4673
|
-
function validateState(document) {
|
|
4674
|
-
return validateSchema(STATE_SCHEMA, document).map(({ path, message }) => ({
|
|
4675
|
-
code: "E_STATE_SCHEMA",
|
|
4676
|
-
message,
|
|
4677
|
-
...path ? { configPath: path } : {}
|
|
4678
|
-
}));
|
|
4679
|
-
}
|
|
4680
|
-
const FORMAT = "kalup.state/1";
|
|
4681
|
-
const notJson$3 = Symbol("not JSON");
|
|
4682
|
-
/**
|
|
4683
|
-
* The state of `portalId` from the text of `file`, where the host keeps it. E_STATE_INVALID when the text is not JSON,
|
|
4684
|
-
* names a format other than kalup.state/1, does not match its schema, or describes another portal. `target` names the
|
|
4685
|
-
* target in the fix.
|
|
4686
|
-
*/
|
|
4687
|
-
function parseState(text, file, portalId, target) {
|
|
4688
|
-
const document = parseJson$3(text);
|
|
4689
|
-
if (document === notJson$3) throw invalid$1(file, "is not JSON", target);
|
|
4690
|
-
const format = document?.format;
|
|
4691
|
-
if (typeof format === "string" && format !== FORMAT) throw new KalupError({
|
|
4692
|
-
code: "E_STATE_INVALID",
|
|
4693
|
-
message: `${file} is ${sanitize(format)}, and this version of ${bin} reads ${FORMAT}.`,
|
|
4694
|
-
file,
|
|
4695
|
-
fix: `use the version of ${bin} that wrote it, or a newer one`
|
|
4696
|
-
});
|
|
4697
|
-
const issues = validateState(document);
|
|
4698
|
-
if (issues.length > 0) {
|
|
4699
|
-
const [first] = issues;
|
|
4700
|
-
const at = first?.configPath ? ` at ${first.configPath}` : "";
|
|
4701
|
-
throw invalid$1(file, `does not match ${FORMAT}${at}: ${first?.message}`, target);
|
|
4702
|
-
}
|
|
4703
|
-
const state = document;
|
|
4704
|
-
if (state.portalId !== portalId) throw invalid$1(file, `describes portal ${state.portalId}, not portal ${portalId}`, target);
|
|
4705
|
-
return state;
|
|
4706
|
-
}
|
|
4707
|
-
function parseJson$3(text) {
|
|
4708
|
-
try {
|
|
4709
|
-
return JSON.parse(text);
|
|
4710
|
-
} catch {
|
|
4711
|
-
return notJson$3;
|
|
4712
|
-
}
|
|
4713
|
-
}
|
|
4714
|
-
function invalid$1(file, why, target = "<target>") {
|
|
4715
|
-
const name = file.slice(Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + 1);
|
|
4716
|
-
return new KalupError({
|
|
4717
|
-
code: "E_STATE_INVALID",
|
|
4718
|
-
message: `${file} ${why}.`,
|
|
4719
|
-
file,
|
|
4720
|
-
fix: `rename ${name}.bak, the state before its last save, into its place if it reads; else move the file away and run ${bin} state rebuild --target ${target}`
|
|
4721
|
-
});
|
|
4722
|
-
}
|
|
4723
|
-
/** An association address written from the other side: `association:<b>/<a>/<name>` for `association:<a>/<b>/<name>`. */
|
|
4724
|
-
function reversedAssociation(address) {
|
|
4725
|
-
if (!address.startsWith("association:")) return;
|
|
4726
|
-
const [from, to, ...name] = address.slice(12).split("/");
|
|
4727
|
-
return `association:${to}/${from}/${name.join("/")}`;
|
|
4728
|
-
}
|
|
4729
|
-
/**
|
|
4730
|
-
* State with each association entry under the address `held` gives the association. An association's identity is its
|
|
4731
|
-
* name, and its direction is only how config writes it, so an entry at the reversed address moves to the held one, its
|
|
4732
|
-
* labels and type IDs swapped; one the held address has an entry of the same name for already is dropped. Every save
|
|
4733
|
-
* of the state so read keeps the move.
|
|
4734
|
-
*/
|
|
4735
|
-
function followAssociations(state, held) {
|
|
4736
|
-
if (state === null) return state;
|
|
4737
|
-
const resources = { ...state.resources };
|
|
4738
|
-
let moved = false;
|
|
4739
|
-
for (const [address, entry] of Object.entries(state.resources)) {
|
|
4740
|
-
const reversed = reversedAssociation(address);
|
|
4741
|
-
if (reversed === void 0 || held(address) || !held(reversed)) continue;
|
|
4742
|
-
const there = Object.hasOwn(resources, reversed) ? resources[reversed] : void 0;
|
|
4743
|
-
if (there !== void 0 && there.id !== entry.id) continue;
|
|
4744
|
-
delete resources[address];
|
|
4745
|
-
resources[reversed] = there ?? reversedEntry(entry);
|
|
4746
|
-
moved = true;
|
|
4747
|
-
}
|
|
4748
|
-
return moved ? {
|
|
4749
|
-
...state,
|
|
4750
|
-
resources
|
|
4751
|
-
} : state;
|
|
4752
|
-
}
|
|
4753
|
-
function reversedEntry(entry) {
|
|
4754
|
-
const { base, rewrites, typeIds, ...rest } = entry;
|
|
4755
|
-
const out = { ...rest };
|
|
4756
|
-
if (base !== void 0) out.base = swapLabels(base);
|
|
4757
|
-
if (rewrites !== void 0) out.rewrites = swapLabels(rewrites);
|
|
4758
|
-
if (typeIds !== void 0) out.typeIds = [typeIds[1], typeIds[0]];
|
|
4759
|
-
return out;
|
|
4760
|
-
}
|
|
4761
|
-
function swapLabels(units) {
|
|
4762
|
-
const { label, inverseLabel, ...rest } = units;
|
|
4763
|
-
return {
|
|
4764
|
-
...rest,
|
|
4765
|
-
...inverseLabel === void 0 ? {} : { label: inverseLabel },
|
|
4766
|
-
...label === void 0 ? {} : { inverseLabel: label }
|
|
4767
|
-
};
|
|
4768
|
-
}
|
|
4769
|
-
/** The type IDs state records per association address, which name a label HubSpot's schema read does not list yet. */
|
|
4770
|
-
function associationIds(state) {
|
|
4771
|
-
const out = {};
|
|
4772
|
-
for (const [address, entry] of Object.entries(state?.resources ?? {})) if (entry.typeIds !== void 0 && address.startsWith("association:")) out[address] = entry.typeIds;
|
|
4773
|
-
return out;
|
|
4774
|
-
}
|
|
4775
4648
|
const registry = {
|
|
4776
4649
|
property: {
|
|
4777
4650
|
family: "crm.properties",
|
|
@@ -5076,6 +4949,14 @@ const NORM_VERSIONS = {
|
|
|
5076
4949
|
association: 1
|
|
5077
4950
|
};
|
|
5078
4951
|
/**
|
|
4952
|
+
* Whether this version plans and applies resources of `type`: every such type has a normalizer version. A later 1.x
|
|
4953
|
+
* may add a type within the format versions; this version keeps such entries in state, reports them as not handled,
|
|
4954
|
+
* and refuses a plan step of one (docs/compatibility.md).
|
|
4955
|
+
*/
|
|
4956
|
+
function handledType(type) {
|
|
4957
|
+
return Object.hasOwn(NORM_VERSIONS, type);
|
|
4958
|
+
}
|
|
4959
|
+
/**
|
|
5079
4960
|
* Standard objects whose properties and groups read under a scope other than `crm.schemas.<object>.read`, checked
|
|
5080
4961
|
* against HubSpot's scopes reference and the scope lists of the 2026-09 properties and property groups list paths.
|
|
5081
4962
|
* Commerce payments drop the underscore. Products have no read scope on those lists but the legacy e-commerce, which
|
|
@@ -5114,51 +4995,218 @@ const standardName = /^[a-z_]+$/;
|
|
|
5114
4995
|
function readScope(row, objectType = "") {
|
|
5115
4996
|
return scopeFor$1(row.scopes?.read[0], scopeExceptions, objectType);
|
|
5116
4997
|
}
|
|
5117
|
-
function writeScope(row, objectType = "") {
|
|
5118
|
-
return scopeFor$1(row.scopes?.write[0], writeScopeExceptions, objectType);
|
|
4998
|
+
function writeScope(row, objectType = "") {
|
|
4999
|
+
return scopeFor$1(row.scopes?.write[0], writeScopeExceptions, objectType);
|
|
5000
|
+
}
|
|
5001
|
+
/**
|
|
5002
|
+
* The scope one request needs: its row's for the object its path names. A labels path names two objects, so it needs
|
|
5003
|
+
* both their scopes, named in code-unit order: plan and apply name the same whichever direction they read.
|
|
5004
|
+
*/
|
|
5005
|
+
function requestScope(type, params, mode) {
|
|
5006
|
+
const row = Object.hasOwn(registry, type) ? registry[type] : void 0;
|
|
5007
|
+
if (row === void 0) return;
|
|
5008
|
+
const scope = (objectType) => mode === "read" ? readScope(row, objectType) : writeScope(row, objectType);
|
|
5009
|
+
const { fromObjectType, toObjectType } = params ?? {};
|
|
5010
|
+
if (fromObjectType !== void 0 && toObjectType !== void 0) {
|
|
5011
|
+
const both = [scope(fromObjectType), scope(toObjectType)].filter((s) => s !== void 0);
|
|
5012
|
+
return [...new Set(both)].sort().join(" and ");
|
|
5013
|
+
}
|
|
5014
|
+
return scope(params?.objectType);
|
|
5015
|
+
}
|
|
5016
|
+
const LIMIT_OBJECTS = /* @__PURE__ */ new Set([
|
|
5017
|
+
"companies",
|
|
5018
|
+
"contacts",
|
|
5019
|
+
"deals"
|
|
5020
|
+
]);
|
|
5021
|
+
/**
|
|
5022
|
+
* The crm.objects read scope Kalup recommends so plan can read the property limit: on the first of `objects` that is
|
|
5023
|
+
* companies, contacts or deals, else on companies. HubSpot's Limits Tracking custom-properties answered 403 to a key
|
|
5024
|
+
* with crm.schemas scopes only, and 200 once crm.objects.companies.read was added, on a developer test account (runs
|
|
5025
|
+
* 89b45da9 and fb6155db, 2026-09-29).
|
|
5026
|
+
*/
|
|
5027
|
+
function limitScope(objects) {
|
|
5028
|
+
return `crm.objects.${objects.find((o) => LIMIT_OBJECTS.has(o)) ?? "companies"}.read`;
|
|
5029
|
+
}
|
|
5030
|
+
function scopeFor$1(template, exceptions, objectType) {
|
|
5031
|
+
if (!template?.includes("{object}")) return template;
|
|
5032
|
+
const object = standardName.test(objectType) ? objectType : "custom";
|
|
5033
|
+
if (Object.hasOwn(exceptions, object)) return exceptions[object];
|
|
5034
|
+
return template.replace("{object}", object);
|
|
5035
|
+
}
|
|
5036
|
+
/** Fills every `{placeholder}` in a path template, for example `{objectType}` and `{name}`. */
|
|
5037
|
+
function fillPath(template, params) {
|
|
5038
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
5039
|
+
const value = params[key];
|
|
5040
|
+
if (value === void 0) throw new Error(`Path ${template} needs {${key}}`);
|
|
5041
|
+
return encodeURIComponent(value);
|
|
5042
|
+
});
|
|
5043
|
+
}
|
|
5044
|
+
const ansi = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
5045
|
+
const control = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
5046
|
+
/** Strips ANSI sequences, control and bidirectional formatting characters and newlines, and caps the length. */
|
|
5047
|
+
function sanitize(text, max = 120) {
|
|
5048
|
+
const clean = text.replace(ansi, "").replace(control, "");
|
|
5049
|
+
const chars = Array.from(clean);
|
|
5050
|
+
return chars.length > max ? `${chars.slice(0, max - 1).join("")}…` : clean;
|
|
5051
|
+
}
|
|
5052
|
+
const KEY_SHAPE = /pat-[a-z0-9]+-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
5053
|
+
/** Whether `value` holds one of `keys`, the keys the run holds, or anything shaped like a HubSpot key. */
|
|
5054
|
+
function holdsKey(value, keys = []) {
|
|
5055
|
+
return KEY_SHAPE.test(value) || keys.some((key) => key !== "" && value.includes(key));
|
|
5056
|
+
}
|
|
5057
|
+
const STATE_SCHEMA = state_1_schema_default;
|
|
5058
|
+
const STATE_FIELDS = new Set(Object.keys(STATE_SCHEMA.properties ?? {}));
|
|
5059
|
+
const ENTRY_FIELDS = new Set(Object.keys((STATE_SCHEMA.$defs?.resourceState)?.properties ?? {}));
|
|
5060
|
+
/** An entry with only the fields this version knows: what a rewrite keeps of it. */
|
|
5061
|
+
function knownFields(entry) {
|
|
5062
|
+
return Object.fromEntries(Object.entries(entry).filter(([field]) => ENTRY_FIELDS.has(field)));
|
|
5063
|
+
}
|
|
5064
|
+
/** Checks a document against state-1.schema.json. Empty when it conforms. */
|
|
5065
|
+
function validateState(document) {
|
|
5066
|
+
return validateSchema(STATE_SCHEMA, document).map(({ path, message }) => ({
|
|
5067
|
+
code: "E_STATE_SCHEMA",
|
|
5068
|
+
message,
|
|
5069
|
+
...path ? { configPath: path } : {}
|
|
5070
|
+
}));
|
|
5071
|
+
}
|
|
5072
|
+
const FORMAT = "kalup.state/1";
|
|
5073
|
+
const notJson$3 = Symbol("not JSON");
|
|
5074
|
+
/**
|
|
5075
|
+
* The state of `portalId` from the text of `file`, where the host keeps it. E_STATE_INVALID when the text is not JSON,
|
|
5076
|
+
* names a format other than kalup.state/1, does not match its schema, or describes another portal. `target` names the
|
|
5077
|
+
* target in the fix.
|
|
5078
|
+
*/
|
|
5079
|
+
function parseState(text, file, portalId, target) {
|
|
5080
|
+
const document = parseJson$3(text);
|
|
5081
|
+
if (document === notJson$3) throw invalid$1(file, "is not JSON", target);
|
|
5082
|
+
const format = document?.format;
|
|
5083
|
+
if (typeof format === "string" && format !== FORMAT) throw new KalupError({
|
|
5084
|
+
code: "E_STATE_INVALID",
|
|
5085
|
+
message: `${file} is ${sanitize(format)}, and this version of ${bin} reads ${FORMAT}.`,
|
|
5086
|
+
file,
|
|
5087
|
+
fix: `use the version of ${bin} that wrote it, or a newer one`
|
|
5088
|
+
});
|
|
5089
|
+
const issues = validateState(document);
|
|
5090
|
+
if (issues.length > 0) {
|
|
5091
|
+
const [first] = issues;
|
|
5092
|
+
const at = first?.configPath ? ` at ${first.configPath}` : "";
|
|
5093
|
+
throw invalid$1(file, `does not match ${FORMAT}${at}: ${first?.message}`, target);
|
|
5094
|
+
}
|
|
5095
|
+
const state = document;
|
|
5096
|
+
if (state.portalId !== portalId) throw invalid$1(file, `describes portal ${state.portalId}, not portal ${portalId}`, target);
|
|
5097
|
+
return withLater(state);
|
|
5098
|
+
}
|
|
5099
|
+
function withLater(document) {
|
|
5100
|
+
const fields = Object.fromEntries(Object.entries(document).filter(([key]) => !STATE_FIELDS.has(key)));
|
|
5101
|
+
const entries = Object.entries(document.resources);
|
|
5102
|
+
const resources = Object.fromEntries(entries.filter(([address]) => !handled(address)));
|
|
5103
|
+
const state = {
|
|
5104
|
+
...Object.fromEntries(Object.entries(document).filter(([key]) => STATE_FIELDS.has(key))),
|
|
5105
|
+
resources: Object.fromEntries(entries.filter(([a]) => handled(a)))
|
|
5106
|
+
};
|
|
5107
|
+
if (Object.keys(fields).length + Object.keys(resources).length > 0) state.later = {
|
|
5108
|
+
fields,
|
|
5109
|
+
resources
|
|
5110
|
+
};
|
|
5111
|
+
return state;
|
|
5112
|
+
}
|
|
5113
|
+
function handled(address) {
|
|
5114
|
+
return handledType(parseAddress(address).type);
|
|
5119
5115
|
}
|
|
5120
5116
|
/**
|
|
5121
|
-
* The
|
|
5122
|
-
*
|
|
5117
|
+
* The text a host writes for `state`: what a later version wrote merged back as it was, checked against kalup.state/1,
|
|
5118
|
+
* one JSON document and a newline. Throws, writing nothing, when the document does not match the schema or a string in
|
|
5119
|
+
* it, a key or a value, holds anything shaped like a HubSpot key.
|
|
5123
5120
|
*/
|
|
5124
|
-
function
|
|
5125
|
-
const
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5121
|
+
function stateText(state) {
|
|
5122
|
+
const { later, ...known } = state;
|
|
5123
|
+
const document = {
|
|
5124
|
+
...later?.fields,
|
|
5125
|
+
...known,
|
|
5126
|
+
resources: {
|
|
5127
|
+
...later?.resources,
|
|
5128
|
+
...known.resources
|
|
5129
|
+
}
|
|
5130
|
+
};
|
|
5131
|
+
const problems = validateState(document);
|
|
5132
|
+
if (problems.length > 0) throw new Error(`refusing to save state that does not match kalup.state/1: ${problems[0]?.message}`);
|
|
5133
|
+
if (strings(document).some((value) => holdsKey(value))) throw new Error("refusing to save state that holds a key");
|
|
5134
|
+
return `${stableStringify(document)}\n`;
|
|
5135
|
+
}
|
|
5136
|
+
function strings(value) {
|
|
5137
|
+
if (typeof value === "string") return [value];
|
|
5138
|
+
if (Array.isArray(value)) return value.flatMap(strings);
|
|
5139
|
+
if (typeof value === "object" && value !== null) return Object.entries(value).flatMap(([key, field]) => [key, ...strings(field)]);
|
|
5140
|
+
return [];
|
|
5141
|
+
}
|
|
5142
|
+
function parseJson$3(text) {
|
|
5143
|
+
try {
|
|
5144
|
+
return JSON.parse(text);
|
|
5145
|
+
} catch {
|
|
5146
|
+
return notJson$3;
|
|
5132
5147
|
}
|
|
5133
|
-
return scope(params?.objectType);
|
|
5134
5148
|
}
|
|
5135
|
-
|
|
5136
|
-
"
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5149
|
+
function invalid$1(file, why, target = "<target>") {
|
|
5150
|
+
const name = file.slice(Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + 1);
|
|
5151
|
+
return new KalupError({
|
|
5152
|
+
code: "E_STATE_INVALID",
|
|
5153
|
+
message: `${file} ${why}.`,
|
|
5154
|
+
file,
|
|
5155
|
+
fix: `rename ${name}.bak, the state before its last save, into its place if it reads; else move the file away and run ${bin} state rebuild --target ${target}`
|
|
5156
|
+
});
|
|
5157
|
+
}
|
|
5158
|
+
/** An association address written from the other side: `association:<b>/<a>/<name>` for `association:<a>/<b>/<name>`. */
|
|
5159
|
+
function reversedAssociation(address) {
|
|
5160
|
+
if (!address.startsWith("association:")) return;
|
|
5161
|
+
const [from, to, ...name] = address.slice(12).split("/");
|
|
5162
|
+
return `association:${to}/${from}/${name.join("/")}`;
|
|
5163
|
+
}
|
|
5140
5164
|
/**
|
|
5141
|
-
*
|
|
5142
|
-
*
|
|
5143
|
-
*
|
|
5144
|
-
*
|
|
5165
|
+
* State with each association entry under the address `held` gives the association. An association's identity is its
|
|
5166
|
+
* name, and its direction is only how config writes it, so an entry at the reversed address moves to the held one, its
|
|
5167
|
+
* labels and type IDs swapped; one the held address has an entry of the same name for already is dropped. Every save
|
|
5168
|
+
* of the state so read keeps the move.
|
|
5145
5169
|
*/
|
|
5146
|
-
function
|
|
5147
|
-
|
|
5170
|
+
function followAssociations(state, held) {
|
|
5171
|
+
if (state === null) return state;
|
|
5172
|
+
const resources = { ...state.resources };
|
|
5173
|
+
let moved = false;
|
|
5174
|
+
for (const [address, entry] of Object.entries(state.resources)) {
|
|
5175
|
+
const reversed = reversedAssociation(address);
|
|
5176
|
+
if (reversed === void 0 || held(address) || !held(reversed)) continue;
|
|
5177
|
+
const there = Object.hasOwn(resources, reversed) ? resources[reversed] : void 0;
|
|
5178
|
+
if (there !== void 0 && there.id !== entry.id) continue;
|
|
5179
|
+
delete resources[address];
|
|
5180
|
+
resources[reversed] = there ?? reversedEntry(entry);
|
|
5181
|
+
moved = true;
|
|
5182
|
+
}
|
|
5183
|
+
return moved ? {
|
|
5184
|
+
...state,
|
|
5185
|
+
resources
|
|
5186
|
+
} : state;
|
|
5148
5187
|
}
|
|
5149
|
-
function
|
|
5150
|
-
|
|
5151
|
-
const
|
|
5152
|
-
if (
|
|
5153
|
-
|
|
5188
|
+
function reversedEntry(entry) {
|
|
5189
|
+
const { base, rewrites, typeIds, written, ...rest } = entry;
|
|
5190
|
+
const out = { ...rest };
|
|
5191
|
+
if (base !== void 0) out.base = swapLabels(base);
|
|
5192
|
+
if (rewrites !== void 0) out.rewrites = swapLabels(rewrites);
|
|
5193
|
+
if (written !== void 0) out.written = swapLabels(written);
|
|
5194
|
+
if (typeIds !== void 0) out.typeIds = [typeIds[1], typeIds[0]];
|
|
5195
|
+
return out;
|
|
5154
5196
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
return
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
}
|
|
5197
|
+
function swapLabels(units) {
|
|
5198
|
+
const { label, inverseLabel, ...rest } = units;
|
|
5199
|
+
return {
|
|
5200
|
+
...rest,
|
|
5201
|
+
...inverseLabel === void 0 ? {} : { label: inverseLabel },
|
|
5202
|
+
...label === void 0 ? {} : { inverseLabel: label }
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
/** The type IDs state records per association address, which name a label HubSpot's schema read does not list yet. */
|
|
5206
|
+
function associationIds(state) {
|
|
5207
|
+
const out = {};
|
|
5208
|
+
for (const [address, entry] of Object.entries(state?.resources ?? {})) if (entry.typeIds !== void 0 && address.startsWith("association:")) out[address] = entry.typeIds;
|
|
5209
|
+
return out;
|
|
5162
5210
|
}
|
|
5163
5211
|
const baseUrl = "https://api.hubapi.com";
|
|
5164
5212
|
const rateHeaders$1 = {
|
|
@@ -7029,6 +7077,20 @@ function classify(base, desired, observed, rules) {
|
|
|
7029
7077
|
if (desired.options && observed.options && !ignored.has("options")) units.push(...options(desired.options, observed.options, base, rules));
|
|
7030
7078
|
return units.sort((a, b) => byCodeUnit(a.unit, b.unit));
|
|
7031
7079
|
}
|
|
7080
|
+
/** A stored base as a Spec: its scalar units as fields, its members in the agreed order, then by value. */
|
|
7081
|
+
function specOfBase(base) {
|
|
7082
|
+
const { options: members, optionsOrder, ...fields } = base;
|
|
7083
|
+
if (members === void 0) return { fields };
|
|
7084
|
+
const order = optionsOrder ?? [];
|
|
7085
|
+
const rank = (value) => order.includes(value) ? order.indexOf(value) : order.length;
|
|
7086
|
+
return {
|
|
7087
|
+
fields,
|
|
7088
|
+
options: Object.entries(members).sort(([a], [b]) => rank(a) - rank(b) || byCodeUnit(a, b)).map(([value, member]) => ({
|
|
7089
|
+
value,
|
|
7090
|
+
...member
|
|
7091
|
+
}))
|
|
7092
|
+
};
|
|
7093
|
+
}
|
|
7032
7094
|
/**
|
|
7033
7095
|
* The base after an apply: every owned unit of `approved`, only those `units` names when given, whose live value
|
|
7034
7096
|
* equals the approved value takes that value; every other unit keeps its previous base value or stays absent, so a
|
|
@@ -7211,7 +7273,7 @@ const PLAN_SCHEMA = {
|
|
|
7211
7273
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
7212
7274
|
$id: "https://kalup.dev/schemas/plan-1.schema.json",
|
|
7213
7275
|
title: "Kalup plan, version 1",
|
|
7214
|
-
description: "What `kalup plan` would do to one target, with the context an approval binds to. Closed at every level, so a saved plan carries nothing this schema does not name, and
|
|
7276
|
+
description: "What `kalup plan` would do to one target, with the context an approval binds to. Closed at every level, apart from the free-form values in a step's desired and expect.values, so a saved plan carries nothing this schema does not name. A later 1.x may add fields, values and step types; an earlier 1.x refuses a plan that uses one, before any request (docs/compatibility.md).",
|
|
7215
7277
|
type: "object",
|
|
7216
7278
|
required: [
|
|
7217
7279
|
"format",
|
|
@@ -7846,7 +7908,8 @@ const PLAN_SCHEMA = {
|
|
|
7846
7908
|
"override",
|
|
7847
7909
|
"unsupported",
|
|
7848
7910
|
"not-owned",
|
|
7849
|
-
"policy"
|
|
7911
|
+
"policy",
|
|
7912
|
+
"settling"
|
|
7850
7913
|
] },
|
|
7851
7914
|
"detail": { "type": "string" },
|
|
7852
7915
|
"blocks": {
|
|
@@ -8671,8 +8734,12 @@ function capturedSpec(resource) {
|
|
|
8671
8734
|
const definition = resource.definition ?? {};
|
|
8672
8735
|
if (resource.type !== "property") return observedSpec(CAPTURED[resource.type], definition);
|
|
8673
8736
|
const options = definition.options ?? [];
|
|
8737
|
+
if (resource.managed && !buildable(definition.type, definition.fieldType)) return observedSpec(CAPTURED.unsupported, definition, options);
|
|
8674
8738
|
return observedSpec(resource.managed ? propertyCaptured(definition.type) : [], definition, options);
|
|
8675
8739
|
}
|
|
8740
|
+
function buildable(type, fieldType) {
|
|
8741
|
+
return Object.keys(HUBSPOT_TYPES).some((kind) => HUBSPOT_TYPES[kind] === type && FIELD_TYPES[kind].includes(fieldType));
|
|
8742
|
+
}
|
|
8676
8743
|
/** The fields config owns on a resource that exists: those it states, less those ignoreChanges released on create. */
|
|
8677
8744
|
function ownedFields(resource) {
|
|
8678
8745
|
const ignored = new Set(resource.lifecycle?.ignoreChanges);
|
|
@@ -8688,7 +8755,8 @@ function specOf(fields) {
|
|
|
8688
8755
|
}
|
|
8689
8756
|
function objectOf(address) {
|
|
8690
8757
|
const { type, path } = parseAddress(address);
|
|
8691
|
-
|
|
8758
|
+
const at = path.indexOf("/");
|
|
8759
|
+
return type === "object" || at === -1 ? path : path.slice(0, at);
|
|
8692
8760
|
}
|
|
8693
8761
|
/** The path after the object: a stage's is `<pipeline>/<stage>`; see stageIdOf for its own ID. */
|
|
8694
8762
|
function nameOf(address) {
|
|
@@ -8724,6 +8792,14 @@ function shownName(address) {
|
|
|
8724
8792
|
const { type } = parseAddress(address);
|
|
8725
8793
|
return type === "stage" || type === "association" ? ownId(address) : nameOf(address);
|
|
8726
8794
|
}
|
|
8795
|
+
/**
|
|
8796
|
+
* The portal name an address resolves to on a target: its name override, else its own name. A stage's is its stage ID;
|
|
8797
|
+
* its pipeline's ID is the pipeline address's.
|
|
8798
|
+
*/
|
|
8799
|
+
function resolvedName(overrides, address) {
|
|
8800
|
+
const override = Object.hasOwn(overrides, address) ? overrides[address] : void 0;
|
|
8801
|
+
return (override?.skip === true ? void 0 : override?.name) ?? shownName(address);
|
|
8802
|
+
}
|
|
8727
8803
|
/** The resource types HubSpot purges on delete, with no archive and no restore (observed 2026-10-01 and 2026-10-05). */
|
|
8728
8804
|
const PURGED_TYPES = /* @__PURE__ */ new Set([
|
|
8729
8805
|
"pipeline",
|
|
@@ -8915,6 +8991,87 @@ function baseFor(entry, address, id) {
|
|
|
8915
8991
|
if (!Object.hasOwn(NORM_VERSIONS, kind) || (entry.normVersion ?? NORM_VERSIONS[kind]) !== NORM_VERSIONS[kind]) return;
|
|
8916
8992
|
return entry.base;
|
|
8917
8993
|
}
|
|
8994
|
+
/** How long after a verified write a read that disagrees with it is not trusted: longer than every lag observed. */
|
|
8995
|
+
const SETTLE_MS = 3e5;
|
|
8996
|
+
/** The addresses the read cannot be trusted on yet, each with why and until when. Sorted. */
|
|
8997
|
+
function settlingOf(input) {
|
|
8998
|
+
const entries = Object.entries(input.state?.resources ?? {});
|
|
8999
|
+
const out = /* @__PURE__ */ new Map();
|
|
9000
|
+
for (const [address, entry] of entries) {
|
|
9001
|
+
const settling = settlingFor(input, address, entry);
|
|
9002
|
+
if (settling !== void 0) out.set(address, settling);
|
|
9003
|
+
}
|
|
9004
|
+
const unlisted = new Map([...out].filter(([a, s]) => s.reason === "missing" && parseAddress(a).type === "object").map(([a, s]) => [objectOf(a), s]));
|
|
9005
|
+
for (const [address, entry] of entries) {
|
|
9006
|
+
const [parent] = objectsOf$1(address).flatMap((key) => unlisted.get(key) ?? []);
|
|
9007
|
+
const unseen = UNSEEN.has(input.status(address));
|
|
9008
|
+
if (parent !== void 0 && !out.has(address) && unseen && owns(input, address, entry)) out.set(address, {
|
|
9009
|
+
reason: "missing",
|
|
9010
|
+
until: parent.until
|
|
9011
|
+
});
|
|
9012
|
+
}
|
|
9013
|
+
return Object.fromEntries([...out].sort(([a], [b]) => byCodeUnit(a, b)));
|
|
9014
|
+
}
|
|
9015
|
+
/**
|
|
9016
|
+
* The units an entry records apply wrote, each with when: `units` at `now`, and those of an earlier write whose window
|
|
9017
|
+
* is still open, since a read may still serve the copy from before them. Undefined when none is open.
|
|
9018
|
+
*/
|
|
9019
|
+
function writtenAfter(entry, units, now) {
|
|
9020
|
+
const at = now.toISOString();
|
|
9021
|
+
const kept = Object.entries(entry?.written ?? {}).filter(([, when]) => settlesUntil(when, now) !== void 0);
|
|
9022
|
+
const written = new Map([...kept, ...units.map((unit) => [unit, at])]);
|
|
9023
|
+
return written.size === 0 ? void 0 : Object.fromEntries([...written].sort(([a], [b]) => byCodeUnit(a, b)));
|
|
9024
|
+
}
|
|
9025
|
+
function settlingFor(input, address, entry) {
|
|
9026
|
+
if (!owns(input, address, entry)) return;
|
|
9027
|
+
const status = input.status(address);
|
|
9028
|
+
if (status === "absent") {
|
|
9029
|
+
const until = entry.writtenAt === void 0 ? void 0 : settlesUntil(entry.writtenAt, input.now);
|
|
9030
|
+
return until === void 0 ? void 0 : {
|
|
9031
|
+
reason: "missing",
|
|
9032
|
+
until
|
|
9033
|
+
};
|
|
9034
|
+
}
|
|
9035
|
+
const open = Object.entries(entry.written ?? {}).flatMap(([unit, at]) => {
|
|
9036
|
+
const until = settlesUntil(at, input.now);
|
|
9037
|
+
return until === void 0 ? [] : [[unit, until]];
|
|
9038
|
+
});
|
|
9039
|
+
if (open.length === 0) return;
|
|
9040
|
+
const observed = Object.hasOwn(input.resources, address) ? input.resources[address] : void 0;
|
|
9041
|
+
const base = observed && baseFor(entry, address, entry.id);
|
|
9042
|
+
if (status !== "present" || !observed?.managed || base === void 0) return;
|
|
9043
|
+
const until = new Map(open);
|
|
9044
|
+
const moved = classify(base, specOfBase(base), capturedSpec(observed), { options: "additive" }).flatMap((u) => {
|
|
9045
|
+
const end = u.class === "converged" ? void 0 : until.get(u.unit);
|
|
9046
|
+
return end === void 0 ? [] : [end];
|
|
9047
|
+
});
|
|
9048
|
+
return moved.length === 0 ? void 0 : {
|
|
9049
|
+
reason: "stale",
|
|
9050
|
+
until: latest(moved)
|
|
9051
|
+
};
|
|
9052
|
+
}
|
|
9053
|
+
const UNSEEN = /* @__PURE__ */ new Set([
|
|
9054
|
+
"absent",
|
|
9055
|
+
"not-observed",
|
|
9056
|
+
"unreadable"
|
|
9057
|
+
]);
|
|
9058
|
+
function owns(input, address, entry) {
|
|
9059
|
+
return (entry.origin === "created" || entry.origin === "adopted") && entry.id === resolvedName(input.overrides, address);
|
|
9060
|
+
}
|
|
9061
|
+
function objectsOf$1(address) {
|
|
9062
|
+
const { type } = parseAddress(address);
|
|
9063
|
+
if (type === "object") return [];
|
|
9064
|
+
return type === "association" ? pairOf(address) : [objectOf(address)];
|
|
9065
|
+
}
|
|
9066
|
+
function settlesUntil(at, now) {
|
|
9067
|
+
const written = Date.parse(at);
|
|
9068
|
+
if (!Number.isFinite(written) || written > now.getTime() + 3e5) return;
|
|
9069
|
+
const end = Math.min(written, now.getTime()) + SETTLE_MS;
|
|
9070
|
+
return now.getTime() < end ? new Date(end).toISOString() : void 0;
|
|
9071
|
+
}
|
|
9072
|
+
function latest(times) {
|
|
9073
|
+
return [...times].sort(byCodeUnit).at(-1);
|
|
9074
|
+
}
|
|
8918
9075
|
/**
|
|
8919
9076
|
* The fields the 2026-09 properties, groups and schemas lists document that no captured resource carries. `archived`
|
|
8920
9077
|
* is not listed: an archived resource is not captured at all. `dateDisplayHint` is read-only in practice: HubSpot
|
|
@@ -8979,6 +9136,8 @@ const OBSERVED_TYPES = /* @__PURE__ */ new Set([
|
|
|
8979
9136
|
/** The resource types read from an object's pipelines list. */
|
|
8980
9137
|
const PIPELINE_TYPES = /* @__PURE__ */ new Set(["pipeline", "stage"]);
|
|
8981
9138
|
const NAME_FIX = "rename it in HubSpot to a name without spaces";
|
|
9139
|
+
const SETTLING_SHOWN = 5;
|
|
9140
|
+
const TEXT_MAX$3 = 400;
|
|
8982
9141
|
/**
|
|
8983
9142
|
* The config IR as the side of a comparison, as target `target` sees it when one is given: its definition overrides
|
|
8984
9143
|
* applied. It has no coverage: an address is present or absent.
|
|
@@ -8996,13 +9155,15 @@ function configObservation(loaded, target) {
|
|
|
8996
9155
|
async function observeTarget(http, loaded, targetName, options = {}) {
|
|
8997
9156
|
const target = loaded.config.targets[targetName];
|
|
8998
9157
|
const issues = [];
|
|
8999
|
-
return observePortal(await readPortal(http, loaded, target, issues, options), loaded, targetName, issues);
|
|
9158
|
+
return observePortal(await readPortal(http, loaded, target, issues, options), loaded, targetName, issues, options.settle);
|
|
9000
9159
|
}
|
|
9001
9160
|
/**
|
|
9002
9161
|
* What a read of target `targetName` holds for the config's objects, as observeTarget records it. pull records its own
|
|
9003
9162
|
* read through here, so it classifies against the base exactly as plan does. Issues about the read go to `issues`.
|
|
9163
|
+
* With `settle`, a resource apply wrote minutes ago that the read shows otherwise is settling: unreadable, never
|
|
9164
|
+
* absent.
|
|
9004
9165
|
*/
|
|
9005
|
-
function observePortal(portal, loaded, targetName, issues) {
|
|
9166
|
+
function observePortal(portal, loaded, targetName, issues, settle) {
|
|
9006
9167
|
const target = loaded.config.targets[targetName];
|
|
9007
9168
|
const resources = [];
|
|
9008
9169
|
const meta = [];
|
|
@@ -9028,27 +9189,66 @@ function observePortal(portal, loaded, targetName, issues) {
|
|
|
9028
9189
|
});
|
|
9029
9190
|
}
|
|
9030
9191
|
const coverage = {
|
|
9031
|
-
complete: Object.values(objects).every((o) =>
|
|
9192
|
+
complete: listsRead(objects) && Object.values(objects).every((o) => Object.values(o.associations?.with ?? {}).every((p) => !p.unnamed)),
|
|
9032
9193
|
objects,
|
|
9033
9194
|
otherObjects: portal.customObjects === void 0 ? "unknown" : [...portal.otherObjects].sort(byCodeUnit),
|
|
9034
9195
|
notCaptured: NOT_CAPTURED
|
|
9035
9196
|
};
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
portalId: target.portalId
|
|
9042
|
-
},
|
|
9043
|
-
resources: Object.fromEntries(resources.sort(([a], [b]) => byCodeUnit(a, b))),
|
|
9044
|
-
coverage,
|
|
9045
|
-
meta: Object.fromEntries(meta.sort(([a], [b]) => byCodeUnit(a, b))),
|
|
9046
|
-
members,
|
|
9047
|
-
listed
|
|
9197
|
+
const observation = {
|
|
9198
|
+
side: {
|
|
9199
|
+
kind: "target",
|
|
9200
|
+
name: targetName,
|
|
9201
|
+
portalId: target.portalId
|
|
9048
9202
|
},
|
|
9203
|
+
resources: Object.fromEntries(resources.sort(([a], [b]) => byCodeUnit(a, b))),
|
|
9204
|
+
coverage,
|
|
9205
|
+
meta: Object.fromEntries(meta.sort(([a], [b]) => byCodeUnit(a, b))),
|
|
9206
|
+
members,
|
|
9207
|
+
listed
|
|
9208
|
+
};
|
|
9209
|
+
issues.push(...unnamedIssues(objects));
|
|
9210
|
+
if (settle) {
|
|
9211
|
+
const followed = followAssociations(settle.state, (address) => Object.hasOwn(loaded.ir.resources, address));
|
|
9212
|
+
settleCoverage(observation, coverage, target.overrides ?? {}, {
|
|
9213
|
+
...settle,
|
|
9214
|
+
state: followed
|
|
9215
|
+
}, issues);
|
|
9216
|
+
}
|
|
9217
|
+
return {
|
|
9218
|
+
observation,
|
|
9049
9219
|
issues
|
|
9050
9220
|
};
|
|
9051
9221
|
}
|
|
9222
|
+
function unnamedIssues(objects) {
|
|
9223
|
+
const both = Object.entries(objects).flatMap(([key, o]) => Object.entries(o.associations?.with ?? {}).flatMap(([other, p]) => p.unnamed ? [[key, other].sort(byCodeUnit).join(" and ")] : []));
|
|
9224
|
+
const pairs = [...new Set(both)].sort(byCodeUnit);
|
|
9225
|
+
if (pairs.length === 0) return [];
|
|
9226
|
+
return [{
|
|
9227
|
+
code: "W_SETTLING",
|
|
9228
|
+
message: sanitize(`HubSpot lists association types between ${pairs.join(", ")} that its schema read does not name yet, so this read is not trusted on the associations there`, TEXT_MAX$3),
|
|
9229
|
+
fix: "run the command again in a few minutes: HubSpot names a new association in its schema read some minutes after it is made"
|
|
9230
|
+
}];
|
|
9231
|
+
}
|
|
9232
|
+
function settleCoverage(observation, coverage, overrides, settle, issues) {
|
|
9233
|
+
const settling = settlingOf({
|
|
9234
|
+
...settle,
|
|
9235
|
+
overrides,
|
|
9236
|
+
resources: observation.resources,
|
|
9237
|
+
status: (address) => statusOf(observation, address)
|
|
9238
|
+
});
|
|
9239
|
+
const addresses = Object.keys(settling);
|
|
9240
|
+
if (addresses.length === 0) return;
|
|
9241
|
+
coverage.settling = settling;
|
|
9242
|
+
coverage.complete = false;
|
|
9243
|
+
const until = addresses.map((a) => settling[a].until).sort(byCodeUnit)[addresses.length - 1];
|
|
9244
|
+
const shown = sanitize(addresses.slice(0, SETTLING_SHOWN).join(", "), TEXT_MAX$3);
|
|
9245
|
+
const more = addresses.length > SETTLING_SHOWN ? ` and ${addresses.length - SETTLING_SHOWN} more` : "";
|
|
9246
|
+
issues.push({
|
|
9247
|
+
code: "W_SETTLING",
|
|
9248
|
+
message: `HubSpot still serves an older copy of ${plural(addresses.length, "resource")} apply wrote minutes ago, so this read is not trusted on ${addresses.length === 1 ? "it" : "them"} until ${until}: ${shown}${more}`,
|
|
9249
|
+
fix: `run the command again after ${until}`
|
|
9250
|
+
});
|
|
9251
|
+
}
|
|
9052
9252
|
/**
|
|
9053
9253
|
* Whether an observation holds an address. Config: present or absent. A target or snapshot: the object's status
|
|
9054
9254
|
* first, then a skip override, then the resource itself, then an unsupported property, then one config names that its
|
|
@@ -9056,9 +9256,18 @@ function observePortal(portal, loaded, targetName, issues) {
|
|
|
9056
9256
|
* so it is never out of scope.
|
|
9057
9257
|
*/
|
|
9058
9258
|
function statusOf(observation, address) {
|
|
9259
|
+
const status = readStatus(observation, address);
|
|
9260
|
+
return status === "absent" && observation.opaque ? "unreadable" : status;
|
|
9261
|
+
}
|
|
9262
|
+
/** Whether `address` is unknown only because a later version's incomplete read may have missed it (opaque). */
|
|
9263
|
+
function opaqueGap(observation, address) {
|
|
9264
|
+
return observation.opaque === true && readStatus(observation, address) === "absent";
|
|
9265
|
+
}
|
|
9266
|
+
function readStatus(observation, address) {
|
|
9059
9267
|
const { coverage, resources } = observation;
|
|
9060
9268
|
const held = Object.hasOwn(resources, address);
|
|
9061
9269
|
if (!coverage) return held ? "present" : "absent";
|
|
9270
|
+
if (settlingAt(coverage, address) !== void 0) return "unreadable";
|
|
9062
9271
|
const { type } = parseAddress(address);
|
|
9063
9272
|
if (type === "association") return associationStatus(coverage, address, held);
|
|
9064
9273
|
const key = objectOf(address);
|
|
@@ -9104,6 +9313,46 @@ function associationStatus(coverage, address, held) {
|
|
|
9104
9313
|
if (held) return "present";
|
|
9105
9314
|
return unnamedOf(coverage, from, to).length > 0 ? "unreadable" : "absent";
|
|
9106
9315
|
}
|
|
9316
|
+
/** Why and until when the read cannot be trusted on an address yet, or undefined when it can. */
|
|
9317
|
+
function settlingAt(coverage, address) {
|
|
9318
|
+
const settling = coverage.settling ?? {};
|
|
9319
|
+
return Object.hasOwn(settling, address) ? settling[address] : void 0;
|
|
9320
|
+
}
|
|
9321
|
+
/**
|
|
9322
|
+
* Whether a read covered every list it needed and captured every property config names: the read is complete but for
|
|
9323
|
+
* what waits on HubSpot (waitingOn). Takeover, state rebuild and target rebind refuse a read that is not.
|
|
9324
|
+
*/
|
|
9325
|
+
function listsRead(objects) {
|
|
9326
|
+
return Object.values(objects).every((o) => o.status !== "unreadable" && o.unaddressable === void 0 && o.pipelines?.status !== "unreadable" && Object.values(o.associations?.with ?? {}).every((p) => p.status !== "unreadable"));
|
|
9327
|
+
}
|
|
9328
|
+
/**
|
|
9329
|
+
* What the read leaves unknown among `addresses` only for the minutes HubSpot takes to settle: those settling after an
|
|
9330
|
+
* apply, and the associations a type HubSpot's schema read does not name yet may be. A command acting on these
|
|
9331
|
+
* addresses alone waits on this, never on what the read leaves unknown elsewhere.
|
|
9332
|
+
*/
|
|
9333
|
+
function waitingOn(observation, addresses) {
|
|
9334
|
+
const { coverage } = observation;
|
|
9335
|
+
const settling = [];
|
|
9336
|
+
const unnamed = /* @__PURE__ */ new Set();
|
|
9337
|
+
if (coverage === void 0) return {
|
|
9338
|
+
settling,
|
|
9339
|
+
unnamed: []
|
|
9340
|
+
};
|
|
9341
|
+
for (const address of [...new Set(addresses)].sort(byCodeUnit)) {
|
|
9342
|
+
const settles = settlingAt(coverage, address);
|
|
9343
|
+
if (settles !== void 0) {
|
|
9344
|
+
settling.push([address, settles]);
|
|
9345
|
+
continue;
|
|
9346
|
+
}
|
|
9347
|
+
const [from, to] = parseAddress(address).type === "association" ? pairOf(address) : [];
|
|
9348
|
+
const unknown = statusOf(observation, address) === "unreadable";
|
|
9349
|
+
if (from !== void 0 && to !== void 0 && unknown && unnamedOf(coverage, from, to).length > 0) unnamed.add([from, to].sort(byCodeUnit).join(" and "));
|
|
9350
|
+
}
|
|
9351
|
+
return {
|
|
9352
|
+
settling,
|
|
9353
|
+
unnamed: [...unnamed].sort(byCodeUnit)
|
|
9354
|
+
};
|
|
9355
|
+
}
|
|
9107
9356
|
/** One object's coverage of its pair with `other`, or undefined when that pair was not in scope. */
|
|
9108
9357
|
function pairRead(object, other) {
|
|
9109
9358
|
const pairs = object?.associations?.with;
|
|
@@ -10134,6 +10383,8 @@ function stepFor(context, address, resource, status) {
|
|
|
10134
10383
|
}, context.policy, void 0);
|
|
10135
10384
|
}
|
|
10136
10385
|
function unread$1(context, address, status) {
|
|
10386
|
+
const settling = settlingAt(context.coverage, address);
|
|
10387
|
+
if (settling !== void 0) return settlingStep(address, settling);
|
|
10137
10388
|
const key = objectOf(address);
|
|
10138
10389
|
const object = own$2(context.coverage.objects, key);
|
|
10139
10390
|
const kind = kindOf$3(address);
|
|
@@ -10155,6 +10406,9 @@ function unread$1(context, address, status) {
|
|
|
10155
10406
|
const fix = scope === void 0 ? "check the scopes of the key" : `add the scope ${scope} to the key`;
|
|
10156
10407
|
return blocked(address, "unknown", "scope", `the key cannot read ${key}`, detail, fix);
|
|
10157
10408
|
}
|
|
10409
|
+
function settlingStep(address, settling) {
|
|
10410
|
+
return blocked(address, "unknown", "settling", "HubSpot is still settling", settling.reason === "missing" ? "apply created it minutes ago, and HubSpot does not list it yet" : "apply wrote it minutes ago, and HubSpot still serves an older copy of it", `plan again after ${settling.until}`);
|
|
10411
|
+
}
|
|
10158
10412
|
function blockedParents(context, address, resource) {
|
|
10159
10413
|
if (kindOf$3(address) === "object") return [];
|
|
10160
10414
|
const group = (resource.definition?.group)?.$ref;
|
|
@@ -10168,7 +10422,8 @@ function blockedParents(context, address, resource) {
|
|
|
10168
10422
|
].flatMap((parent) => {
|
|
10169
10423
|
const found = parent === void 0 ? void 0 : context.decided.get(parent);
|
|
10170
10424
|
const update = found?.action === "update" && kindOf$3(found.address) === "object";
|
|
10171
|
-
|
|
10425
|
+
const stale = found !== void 0 && settlingAt(context.coverage, found.address)?.reason === "stale";
|
|
10426
|
+
return found?.risk === "blocked" && !update && !stale ? [found] : [];
|
|
10172
10427
|
});
|
|
10173
10428
|
}
|
|
10174
10429
|
function missingGroup(context, resource) {
|
|
@@ -10299,7 +10554,9 @@ function unheldSteps(context) {
|
|
|
10299
10554
|
const holds = (property) => {
|
|
10300
10555
|
const address = `property:${key}/${property}`;
|
|
10301
10556
|
const decided = context.decided.get(address);
|
|
10302
|
-
|
|
10557
|
+
const creates = decided?.action === "create" && decided.risk !== "blocked";
|
|
10558
|
+
const stale = settlingAt(context.coverage, address)?.reason === "stale";
|
|
10559
|
+
return creates || stale || statusOf(observation, address) === "present" || outOfScope.includes(property);
|
|
10303
10560
|
};
|
|
10304
10561
|
const fields = schemaWrites(step);
|
|
10305
10562
|
for (const field of OBJECT_DISPLAY_FIELDS) {
|
|
@@ -10895,6 +11152,24 @@ function uncovered(address, tombstone, cover) {
|
|
|
10895
11152
|
const fix = `set the action of ${cover} to release to keep it with what it holds, or this one's to destroy`;
|
|
10896
11153
|
return blocked(address, "release", "unsupported", `${kindOf$3(cover)} deleted`, detail, fix);
|
|
10897
11154
|
}
|
|
11155
|
+
function takeoverWait(context, key) {
|
|
11156
|
+
const { coverage } = context;
|
|
11157
|
+
if (!listsRead(coverage.objects)) return {
|
|
11158
|
+
reason: "scope",
|
|
11159
|
+
short: "read incomplete",
|
|
11160
|
+
why: `the read of target ${context.input.target} was incomplete`,
|
|
11161
|
+
fix: INCOMPLETE_FIX
|
|
11162
|
+
};
|
|
11163
|
+
const until = Object.entries(coverage.settling ?? {}).filter(([address]) => objectOf(address) === key && TAKEOVER_KINDS.has(kindOf$3(address))).map(([, settling]) => settling.until).sort(byCodeUnit).at(-1);
|
|
11164
|
+
if (until === void 0) return;
|
|
11165
|
+
return {
|
|
11166
|
+
reason: "settling",
|
|
11167
|
+
short: "HubSpot is still settling",
|
|
11168
|
+
why: `what apply wrote on ${key} minutes ago has not settled in HubSpot yet`,
|
|
11169
|
+
fix: `plan again after ${until}`
|
|
11170
|
+
};
|
|
11171
|
+
}
|
|
11172
|
+
const TAKEOVER_KINDS = /* @__PURE__ */ new Set(["property", "group"]);
|
|
10898
11173
|
function takeoverOf$1(context, kind) {
|
|
10899
11174
|
if (kind === "property") return context.takeover.properties;
|
|
10900
11175
|
return kind === "group" ? context.takeover.groups : [];
|
|
@@ -10922,7 +11197,11 @@ function takeoverDelete(context, address, deleted) {
|
|
|
10922
11197
|
...refused,
|
|
10923
11198
|
...notes
|
|
10924
11199
|
});
|
|
10925
|
-
|
|
11200
|
+
const wait = takeoverWait(context, key);
|
|
11201
|
+
if (wait) {
|
|
11202
|
+
const detail = `takeover would archive ${name}, and ${wait.why}, so takeover removes nothing there`;
|
|
11203
|
+
return noted(blocked(address, "delete", wait.reason, wait.short, detail, wait.fix));
|
|
11204
|
+
}
|
|
10926
11205
|
const members = kindOf$3(address) === "group" ? {
|
|
10927
11206
|
active: own$2(observation.members?.[key] ?? {}, name) ?? [],
|
|
10928
11207
|
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
@@ -10968,10 +11247,14 @@ function optionList(units) {
|
|
|
10968
11247
|
function takeoverBlock(context, address, action, units) {
|
|
10969
11248
|
const { target } = context.input;
|
|
10970
11249
|
const notes = { notes: [optionsNote(context, address, units)] };
|
|
10971
|
-
|
|
10972
|
-
|
|
10973
|
-
|
|
10974
|
-
|
|
11250
|
+
const wait = takeoverWait(context, objectOf(address));
|
|
11251
|
+
if (wait) {
|
|
11252
|
+
const detail = `takeover would remove ${optionList(units)}, and ${wait.why}, so takeover removes nothing there`;
|
|
11253
|
+
return {
|
|
11254
|
+
...blocked(address, action, wait.reason, wait.short, detail, wait.fix),
|
|
11255
|
+
...notes
|
|
11256
|
+
};
|
|
11257
|
+
}
|
|
10975
11258
|
if (!context.policy.allowDestroy) return {
|
|
10976
11259
|
...blocked(address, action, "policy", "option removals not allowed", `takeover removes ${optionList(units)}, which only the portal holds, and target ${target} does not allow deletes`, `keep them in config: run ${pullCommand(target, address)}; or keep them unmanaged: set lifecycle: { options: 'additive' } on ${nameOf(address)}; or remove them: set allowDestroy: true under targets.${target} in kalup.config.ts`),
|
|
10977
11260
|
...notes
|
|
@@ -11053,7 +11336,7 @@ function unreadAssociation(context, address) {
|
|
|
11053
11336
|
}
|
|
11054
11337
|
if (pair.status === "unreadable") return blocked(address, "unknown", "scope", "labels list refused", `HubSpot refused the labels list of ${from} and ${to} (${pair.issue ?? "E_HTTP"}), so what it holds there is unknown`, "check that HubSpot keeps associations between these two objects, then plan again");
|
|
11055
11338
|
const unnamed = unnamedOf(context.coverage, from, to).length;
|
|
11056
|
-
return blocked(address, "unknown", "
|
|
11339
|
+
return blocked(address, "unknown", "settling", "types not named yet", `HubSpot lists ${plural(unnamed, "association type")} between ${from} and ${to} that its schema read does not name yet, and this association may be one of them`, "plan again in a few minutes: HubSpot names a new association in its schema read some minutes after it is made");
|
|
11057
11340
|
}
|
|
11058
11341
|
function objectTakes(context, key) {
|
|
11059
11342
|
const { observation } = context.input;
|
|
@@ -11115,7 +11398,13 @@ function release(address, title, expect) {
|
|
|
11115
11398
|
function orphansOf(input, steps) {
|
|
11116
11399
|
const { loaded, state, target } = input;
|
|
11117
11400
|
const overrides = loaded.config.targets[target]?.overrides ?? {};
|
|
11118
|
-
|
|
11401
|
+
const later = Object.keys(state?.later?.resources ?? {}).map((address) => {
|
|
11402
|
+
return {
|
|
11403
|
+
address,
|
|
11404
|
+
note: sanitize(`a later version of ${bin} manages ${parseAddress(address).type} resources: this version leaves the entry as it is`, TEXT_MAX$2)
|
|
11405
|
+
};
|
|
11406
|
+
});
|
|
11407
|
+
return [...Object.entries(state?.resources ?? {}).filter(([address, entry]) => (entry.origin === "created" || entry.origin === "adopted") && !Object.hasOwn(loaded.ir.resources, address) && !Object.hasOwn(loaded.ir.tombstones, address) && coverOf(loaded.ir.tombstones, address) === void 0 && !heldReversed(loaded.ir, address) && !steps.some((s) => s.address === address)).sort(([a], [b]) => byCodeUnit(a, b)).map(([address, entry]) => {
|
|
11119
11408
|
const name = resolvedName(overrides, address);
|
|
11120
11409
|
const rm = `${bin} rm ${shellWord(address)}`;
|
|
11121
11410
|
let note = `no longer in config: run ${rm} to delete it in HubSpot, or ${rm} --release to stop managing it`;
|
|
@@ -11127,7 +11416,7 @@ function orphansOf(input, steps) {
|
|
|
11127
11416
|
address,
|
|
11128
11417
|
note: sanitize(note, TEXT_MAX$2)
|
|
11129
11418
|
};
|
|
11130
|
-
});
|
|
11419
|
+
}), ...later].sort((a, b) => byCodeUnit(a.address, b.address));
|
|
11131
11420
|
}
|
|
11132
11421
|
function heldReversed(ir, address) {
|
|
11133
11422
|
const reversed = reversedAssociation(address);
|
|
@@ -11149,14 +11438,6 @@ function staleNote(context, address, stale) {
|
|
|
11149
11438
|
function portalName$1(context, address) {
|
|
11150
11439
|
return resolvedName(context.overrides, address);
|
|
11151
11440
|
}
|
|
11152
|
-
/**
|
|
11153
|
-
* The portal name an address resolves to on a target: its name override, else its own name. A stage's is its stage ID;
|
|
11154
|
-
* its pipeline's ID is the pipeline address's.
|
|
11155
|
-
*/
|
|
11156
|
-
function resolvedName(overrides, address) {
|
|
11157
|
-
const override = own$2(overrides, address);
|
|
11158
|
-
return (override?.skip === true ? void 0 : override?.name) ?? shownName(address);
|
|
11159
|
-
}
|
|
11160
11441
|
function archivedOf(lists, key, name) {
|
|
11161
11442
|
const list = own$2(lists, key);
|
|
11162
11443
|
return list === void 0 ? null : list.find((p) => p.name === name);
|
|
@@ -11961,9 +12242,15 @@ function fromSnapshot$1(text, file) {
|
|
|
11961
12242
|
observedAt
|
|
11962
12243
|
},
|
|
11963
12244
|
resources: sorted(resources),
|
|
11964
|
-
coverage
|
|
12245
|
+
coverage,
|
|
12246
|
+
...opaque(coverage) ? { opaque: true } : {}
|
|
11965
12247
|
};
|
|
11966
12248
|
}
|
|
12249
|
+
function opaque(coverage) {
|
|
12250
|
+
const unknown = (fields, known) => Object.keys(fields).some((f) => !known.has(f));
|
|
12251
|
+
const later = unknown(coverage, COVERAGE_FIELDS.coverage) || Object.values(coverage.objects).some((object) => unknown(object, COVERAGE_FIELDS.object));
|
|
12252
|
+
return !coverage.complete && later;
|
|
12253
|
+
}
|
|
11967
12254
|
const PLAIN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
11968
12255
|
const DEVICE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
|
|
11969
12256
|
const UNSAFE = /[^a-z0-9_-]/gu;
|
|
@@ -11983,7 +12270,8 @@ function planPath(targetName, planId) {
|
|
|
11983
12270
|
}
|
|
11984
12271
|
/**
|
|
11985
12272
|
* W_INCOMPLETE for a read that left objects unread, or config properties uncaptured because their portal group's name
|
|
11986
|
-
* no address can hold,
|
|
12273
|
+
* no address can hold, or that is not trusted yet on what settles after an apply or on a pair whose labels hold a type
|
|
12274
|
+
* HubSpot does not name yet, since what they hold is unknown. None for a complete read.
|
|
11987
12275
|
*/
|
|
11988
12276
|
function incompleteIssues(coverage, target) {
|
|
11989
12277
|
if (coverage.complete) return [];
|
|
@@ -11992,13 +12280,19 @@ function incompleteIssues(coverage, target) {
|
|
|
11992
12280
|
const names = unread.map(([key]) => sanitize(key));
|
|
11993
12281
|
const properties = objects.flatMap(([key, o]) => [...o.unaddressable ?? []].sort(byCodeUnit).map((name) => sanitize(`property:${key}/${name}`)));
|
|
11994
12282
|
const scopes = [...new Set(unread.flatMap(([, o]) => o.missingScope === void 0 ? [] : [o.missingScope]))].sort(byCodeUnit).map((scope) => sanitize(scope));
|
|
11995
|
-
const
|
|
11996
|
-
const
|
|
12283
|
+
const lists = objects.some(([, o]) => o.pipelines?.status === "unreadable" || Object.values(o.associations?.with ?? {}).some((p) => p.status === "unreadable"));
|
|
12284
|
+
const waiting = waitOf(coverage, objects);
|
|
12285
|
+
const parts = [
|
|
12286
|
+
...names.length > 0 ? [`${listed(names)} ${names.length === 1 ? "was" : "were"} not read`] : [],
|
|
12287
|
+
...properties.length > 0 ? [`${listed(properties)} ${properties.length === 1 ? "is" : "are"} in a portal group whose name no address can hold`] : [],
|
|
12288
|
+
...waiting.parts
|
|
12289
|
+
];
|
|
12290
|
+
const one = names.length + properties.length + waiting.count === 1;
|
|
11997
12291
|
const which = parts.length > 0 ? `: ${parts.join(", and ")}, so what ${one ? "it holds" : "they hold"} is unknown` : "";
|
|
11998
12292
|
const noun = scopes.length === 1 ? "the scope" : "the scopes";
|
|
11999
12293
|
const fixes = [];
|
|
12000
12294
|
if (scopes.length > 0) fixes.push(`add ${noun} ${listed(scopes)} to the read key of target ${sanitize(target)}`);
|
|
12001
|
-
else if (names.length > 0 ||
|
|
12295
|
+
else if (names.length > 0 || lists || parts.length === 0) fixes.push(`add the missing read scopes to the read key of target ${sanitize(target)}`);
|
|
12002
12296
|
if (properties.length > 0) {
|
|
12003
12297
|
const many = properties.length > 1;
|
|
12004
12298
|
fixes.push(`rename the group${many ? "s" : ""} of ${listed(properties)} in HubSpot to ${many ? "names" : "a name"} without spaces`);
|
|
@@ -12006,9 +12300,25 @@ function incompleteIssues(coverage, target) {
|
|
|
12006
12300
|
return [{
|
|
12007
12301
|
code: "W_INCOMPLETE",
|
|
12008
12302
|
message: `the snapshot of target ${sanitize(target)} is incomplete${which}`,
|
|
12009
|
-
fix: `${fixes.join(" and ")}, then take a new snapshot`
|
|
12303
|
+
fix: fixes.length > 0 ? `${fixes.join(" and ")}, then take a new snapshot${waiting.later}` : `take a new snapshot${waiting.later}`
|
|
12010
12304
|
}];
|
|
12011
12305
|
}
|
|
12306
|
+
function waitOf(coverage, objects) {
|
|
12307
|
+
const settling = Object.keys(coverage.settling ?? {}).sort(byCodeUnit).map((address) => sanitize(address));
|
|
12308
|
+
const until = Object.values(coverage.settling ?? {}).map((s) => s.until).sort(byCodeUnit).at(-1);
|
|
12309
|
+
const pairs = [...new Set(objects.flatMap(([key, o]) => Object.entries(o.associations?.with ?? {}).flatMap(([other, p]) => p.unnamed ? [[key, other].sort(byCodeUnit).join(" and ")] : [])))].sort(byCodeUnit).map((pair) => sanitize(pair));
|
|
12310
|
+
const parts = [];
|
|
12311
|
+
if (settling.length > 0) parts.push(`${listed(settling)} ${settling.length === 1 ? "is" : "are"} settling after an apply until ${until}`);
|
|
12312
|
+
if (pairs.length > 0) parts.push(`the associations between ${listed(pairs)} hold a type HubSpot's schema read does not name yet`);
|
|
12313
|
+
let later = "";
|
|
12314
|
+
if (until !== void 0) later = ` after ${until}`;
|
|
12315
|
+
else if (pairs.length > 0) later = " in a few minutes, once HubSpot names the new association";
|
|
12316
|
+
return {
|
|
12317
|
+
count: settling.length + 2 * pairs.length,
|
|
12318
|
+
later,
|
|
12319
|
+
parts
|
|
12320
|
+
};
|
|
12321
|
+
}
|
|
12012
12322
|
function targetDir(name) {
|
|
12013
12323
|
if (PLAIN.test(name) && !DEVICE.test(name)) return name;
|
|
12014
12324
|
return `${name.toLowerCase().replace(UNSAFE, "_").replace(EDGES, "").slice(0, 40).replace(EDGES, "") || "target"}-${createHash("sha256").update(name).digest("hex").slice(0, 8)}`;
|
|
@@ -12066,6 +12376,7 @@ function parsePlan(text, file, running) {
|
|
|
12066
12376
|
message: `${shown} ${other}. Nothing was sent.`,
|
|
12067
12377
|
fix: `plan again with this version: run ${bin} plan --target <name> --out <file>, review it and apply that file`
|
|
12068
12378
|
});
|
|
12379
|
+
laterStep(document, shown);
|
|
12069
12380
|
const problems = validatePlan(document);
|
|
12070
12381
|
if (problems.length > 0) {
|
|
12071
12382
|
const [first] = problems;
|
|
@@ -12223,7 +12534,7 @@ function checkVersions(plan, now) {
|
|
|
12223
12534
|
function normalizerProblems(plan) {
|
|
12224
12535
|
const known = NORM_VERSIONS;
|
|
12225
12536
|
const problems = [];
|
|
12226
|
-
for (const kind of [
|
|
12537
|
+
for (const kind of [...new Set(plan.steps.map((s) => parseAddress(s.address).type))].sort()) {
|
|
12227
12538
|
const planned = Object.hasOwn(plan.normVersions, kind) ? plan.normVersions[kind] : void 0;
|
|
12228
12539
|
const current = Object.hasOwn(known, kind) ? known[kind] : void 0;
|
|
12229
12540
|
if (planned !== current) problems.push(`the plan compared ${sanitize(kind)} under normalizer ${planned ?? "none"}, this version uses ${current ?? "none"}`);
|
|
@@ -12728,6 +13039,20 @@ function carriedDisagreement(step) {
|
|
|
12728
13039
|
function sameValue(a, b) {
|
|
12729
13040
|
return b !== void 0 && stableStringify(a) === stableStringify(b);
|
|
12730
13041
|
}
|
|
13042
|
+
function laterStep(document, shown) {
|
|
13043
|
+
const steps = document?.steps;
|
|
13044
|
+
for (const step of Array.isArray(steps) ? steps : []) {
|
|
13045
|
+
const address = typeof step?.address === "string" ? step.address : "";
|
|
13046
|
+
if (!isAddress(address) || handledType(parseAddress(address).type)) continue;
|
|
13047
|
+
const { generator } = document;
|
|
13048
|
+
const made = typeof generator?.version === "string" ? ` (${bin} ${sanitize(generator.version, 40)})` : "";
|
|
13049
|
+
throw new KalupError({
|
|
13050
|
+
code: "E_PLAN_INVALID",
|
|
13051
|
+
message: `${shown}: step ${typeof step.id === "string" ? sanitize(step.id, 20) : "a step"} is a ${sanitize(parseAddress(address).type, 40)} step, which this version of ${bin} does not apply: a later version made the plan${made}. Nothing was sent.`,
|
|
13052
|
+
fix: "apply the plan with the version that made it, or a later one"
|
|
13053
|
+
});
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
12731
13056
|
function invalid(message) {
|
|
12732
13057
|
return new KalupError({
|
|
12733
13058
|
code: "E_PLAN_INVALID",
|
|
@@ -13201,13 +13526,21 @@ function verified(run, step, seen) {
|
|
|
13201
13526
|
if (step.action === "create") run.created.add(step.address);
|
|
13202
13527
|
const typeId = seen.raw?.objectTypeId;
|
|
13203
13528
|
if (createsObject(step) && typeof typeId === "string" && !run.typeIds.has(objectOf(step.address))) run.typeIds.set(objectOf(step.address), typeId);
|
|
13204
|
-
const { rewrites: before, ...entry } = entryOf$1(run, step, verifiedBase(run, step, readBack));
|
|
13205
|
-
const
|
|
13206
|
-
const
|
|
13529
|
+
const { rewrites: before, written: _, writtenAt: earlier, ...entry } = entryOf$1(run, step, verifiedBase(run, step, readBack));
|
|
13530
|
+
const written = writtenUnits(step, readBack);
|
|
13531
|
+
const rewrites = rewritesAfter(before, written, own);
|
|
13532
|
+
const unsent = new Set(own.map((u) => u.unit));
|
|
13533
|
+
const prior = Object.hasOwn(run.state.resources, step.address) ? run.state.resources[step.address] : void 0;
|
|
13534
|
+
const now = run.deps.now();
|
|
13535
|
+
const times = writtenAfter(prior, written.filter((unit) => !unsent.has(unit)), now);
|
|
13536
|
+
const at = step.action === "create" || written.length > 0 ? now.toISOString() : earlier ?? prior?.writtenAt;
|
|
13537
|
+
const saved = {
|
|
13207
13538
|
...entry,
|
|
13208
|
-
rewrites
|
|
13539
|
+
...rewrites === void 0 ? {} : { rewrites },
|
|
13540
|
+
...times === void 0 ? {} : { written: times },
|
|
13541
|
+
...at === void 0 ? {} : { writtenAt: at }
|
|
13209
13542
|
};
|
|
13210
|
-
const also = carriedEntries(step, seen);
|
|
13543
|
+
const also = carriedEntries(step, seen, now);
|
|
13211
13544
|
if (bad.length === 0) return {
|
|
13212
13545
|
report: report$1(step, "done"),
|
|
13213
13546
|
entry: saved,
|
|
@@ -13230,19 +13563,22 @@ function verified(run, step, seen) {
|
|
|
13230
13563
|
issues: [issue]
|
|
13231
13564
|
};
|
|
13232
13565
|
}
|
|
13233
|
-
function carriedEntries(step, seen) {
|
|
13566
|
+
function carriedEntries(step, seen, now) {
|
|
13234
13567
|
return (step.stages ?? []).flatMap((st) => {
|
|
13235
13568
|
const live = seen.stages?.[st.address];
|
|
13236
13569
|
if (live === void 0) return [];
|
|
13237
13570
|
const units = classify(void 0, specOf(st.desired), capturedSpec(live), { options: "additive" });
|
|
13238
13571
|
const base = advanceBase(void 0, specOf(st.desired), capturedSpec(live));
|
|
13239
13572
|
const rewrites = rewritesAfter(void 0, units.map((u) => u.unit), units.filter((u) => u.class !== "converged"));
|
|
13573
|
+
const sent = units.filter((u) => u.class === "converged").map((u) => u.unit);
|
|
13240
13574
|
const entry = {
|
|
13241
13575
|
origin: "created",
|
|
13242
13576
|
id: ownId(st.address),
|
|
13243
13577
|
normVersion: NORM_VERSIONS.stage,
|
|
13244
13578
|
...base === void 0 ? {} : { base },
|
|
13245
|
-
...rewrites === void 0 ? {} : { rewrites }
|
|
13579
|
+
...rewrites === void 0 ? {} : { rewrites },
|
|
13580
|
+
...sent.length === 0 ? {} : { written: writtenAfter(void 0, sent, now) },
|
|
13581
|
+
writtenAt: now.toISOString()
|
|
13246
13582
|
};
|
|
13247
13583
|
return [[st.address, entry]];
|
|
13248
13584
|
});
|
|
@@ -13296,10 +13632,18 @@ function unsettled(run, step, acknowledged) {
|
|
|
13296
13632
|
report: outcome,
|
|
13297
13633
|
issues: [issue]
|
|
13298
13634
|
};
|
|
13635
|
+
const desired = specOf(step.desired ?? {});
|
|
13636
|
+
const sent = classify(void 0, desired, desired, { options: "additive" }).map((u) => u.unit);
|
|
13637
|
+
const now = run.deps.now();
|
|
13638
|
+
const entry = {
|
|
13639
|
+
...entryOf$1(run, step, void 0),
|
|
13640
|
+
...sent.length === 0 ? {} : { written: writtenAfter(void 0, sent, now) },
|
|
13641
|
+
writtenAt: now.toISOString()
|
|
13642
|
+
};
|
|
13299
13643
|
return {
|
|
13300
13644
|
report: outcome,
|
|
13301
13645
|
issues: [issue],
|
|
13302
|
-
entry
|
|
13646
|
+
entry
|
|
13303
13647
|
};
|
|
13304
13648
|
}
|
|
13305
13649
|
function uncertain(run, step, why) {
|
|
@@ -13945,7 +14289,7 @@ function entryOf$1(run, step, base) {
|
|
|
13945
14289
|
const made = kind === "group" && madeByRun(run, step);
|
|
13946
14290
|
origin = owning?.origin ?? (made ? "adopted" : "created");
|
|
13947
14291
|
} else if (step.action === "update") origin = owning?.origin ?? "adopted";
|
|
13948
|
-
const { base: _, ...kept } = step.action === "update" && owning ? owning : {};
|
|
14292
|
+
const { base: _, ...kept } = step.action === "update" && owning ? knownFields(owning) : {};
|
|
13949
14293
|
const entry = {
|
|
13950
14294
|
...kept,
|
|
13951
14295
|
origin,
|
|
@@ -14460,6 +14804,18 @@ function comparedSet(a, b) {
|
|
|
14460
14804
|
return [...set].sort(byCodeUnit);
|
|
14461
14805
|
}
|
|
14462
14806
|
function compareAddress(address, a, b, options) {
|
|
14807
|
+
const { type } = parseAddress(address);
|
|
14808
|
+
if (!handledType(type)) {
|
|
14809
|
+
if (!(a.coverage && b.coverage)) return {
|
|
14810
|
+
address,
|
|
14811
|
+
status: "unmanaged"
|
|
14812
|
+
};
|
|
14813
|
+
return {
|
|
14814
|
+
address,
|
|
14815
|
+
status: "unknown",
|
|
14816
|
+
reason: sanitize(`a later version of ${bin} handles ${type} resources; this version does not compare them`)
|
|
14817
|
+
};
|
|
14818
|
+
}
|
|
14463
14819
|
const statusA = statusOf(a, address);
|
|
14464
14820
|
const statusB = statusOf(b, address);
|
|
14465
14821
|
const unknown = [unknownReason(a, address, statusA, options.overridden?.a), unknownReason(b, address, statusB, options.overridden?.b)].filter((r) => r !== void 0);
|
|
@@ -14606,15 +14962,31 @@ function optionDefaults(spec) {
|
|
|
14606
14962
|
function unknownReason(side, address, status, overridden) {
|
|
14607
14963
|
const name = describeShort(side.side);
|
|
14608
14964
|
if (status !== "excluded" && overridden?.includes(address)) return `${name} has a lookup override for it; this version manages no lookup resources`;
|
|
14965
|
+
if (opaqueGap(side, address)) return `${name} was taken by a later version of ${bin}, whose read was incomplete in parts this version cannot place`;
|
|
14609
14966
|
const key = objectOf(address);
|
|
14967
|
+
const settling = settlingOn(side, address);
|
|
14968
|
+
if (settling !== void 0) return `${name} is settling after an apply until ${settling.until}`;
|
|
14610
14969
|
if (status === "unreadable") {
|
|
14611
14970
|
const object = coverageOf(side, key);
|
|
14612
|
-
if (object?.status === "read") return
|
|
14971
|
+
if (object?.status === "read") return readObjectReason(side, address, name);
|
|
14613
14972
|
const scope = object?.missingScope;
|
|
14614
14973
|
return `${name} could not read ${key}${scope === void 0 ? "" : `: the key lacks ${scope}`}`;
|
|
14615
14974
|
}
|
|
14616
14975
|
return status === "not-observed" ? `${name} did not read ${key}` : void 0;
|
|
14617
14976
|
}
|
|
14977
|
+
function readObjectReason(side, address, name) {
|
|
14978
|
+
const { type } = parseAddress(address);
|
|
14979
|
+
if (type === "association") {
|
|
14980
|
+
const pair = pairOf(address).sort(byCodeUnit);
|
|
14981
|
+
return unnamedOn(side, address) ? `${name} lists a type between ${pair.join(" and ")} that HubSpot's schema read does not name yet` : `${name} could not read the labels between ${pair.join(" and ")}`;
|
|
14982
|
+
}
|
|
14983
|
+
if (type === "pipeline" || type === "stage") return `${name} could not read the pipelines of ${objectOf(address)}`;
|
|
14984
|
+
return `${name} could not capture it: its group's name in the portal holds whitespace`;
|
|
14985
|
+
}
|
|
14986
|
+
function unnamedOn(side, address) {
|
|
14987
|
+
const [from, to] = pairOf(address);
|
|
14988
|
+
return statusOf(side, address) === "unreadable" && settlingOn(side, address) === void 0 && side.coverage !== void 0 && unnamedOf(side.coverage, from, to).length > 0;
|
|
14989
|
+
}
|
|
14618
14990
|
function excludedReason(side, address, status) {
|
|
14619
14991
|
if (status !== "excluded") return;
|
|
14620
14992
|
const name = describeShort(side.side);
|
|
@@ -14665,6 +15037,10 @@ function incomplete(comparison, a, b) {
|
|
|
14665
15037
|
const many = regroup.length > 1;
|
|
14666
15038
|
fixes.push(`rename the group${many ? "s" : ""} of ${regroup.join(", ")} in HubSpot to ${many ? "names" : "a name"} without spaces, then read the portal again`);
|
|
14667
15039
|
}
|
|
15040
|
+
fixes.push(...[a, b].flatMap((side) => waitFix(side, comparison)));
|
|
15041
|
+
const later = [...new Set(comparison.differences.map((d) => parseAddress(d.address).type).filter((type) => !handledType(type)))].sort(byCodeUnit);
|
|
15042
|
+
if (later.length > 0) fixes.push(`compare with a later version of ${bin}, which handles ${later.map((t) => `${t} resources`).join(", ")}`);
|
|
15043
|
+
else if (a.opaque || b.opaque) fixes.push(`compare with the later version of ${bin} that took the snapshot`);
|
|
14668
15044
|
const issue = {
|
|
14669
15045
|
code: "E_INCOMPLETE",
|
|
14670
15046
|
message: `compare is incomplete: ${items.join("; ")}. Nothing there was compared.`
|
|
@@ -14673,7 +15049,23 @@ function incomplete(comparison, a, b) {
|
|
|
14673
15049
|
return issue;
|
|
14674
15050
|
}
|
|
14675
15051
|
function uncaptured(side, address) {
|
|
14676
|
-
|
|
15052
|
+
const read = coverageOf(side, objectOf(address))?.status === "read";
|
|
15053
|
+
const property = parseAddress(address).type === "property";
|
|
15054
|
+
const unreadable = statusOf(side, address) === "unreadable" && !opaqueGap(side, address);
|
|
15055
|
+
return property && unreadable && read && settlingOn(side, address) === void 0;
|
|
15056
|
+
}
|
|
15057
|
+
function waitFix(side, comparison) {
|
|
15058
|
+
const unknown = comparison.differences.filter((d) => d.status === "unknown").map((d) => d.address);
|
|
15059
|
+
const until = unknown.flatMap((address) => settlingOn(side, address)?.until ?? []).sort(byCodeUnit).at(-1);
|
|
15060
|
+
let when;
|
|
15061
|
+
if (until !== void 0) when = `after ${until}`;
|
|
15062
|
+
else if (unknown.some((address) => parseAddress(address).type === "association" && unnamedOn(side, address))) when = "in a few minutes, once HubSpot names the new association";
|
|
15063
|
+
if (when === void 0) return [];
|
|
15064
|
+
const { side: which } = side;
|
|
15065
|
+
return [which.kind === "snapshot" ? `take a new snapshot of target ${sanitize(which.name)} ${when}` : `compare again ${when}`];
|
|
15066
|
+
}
|
|
15067
|
+
function settlingOn(side, address) {
|
|
15068
|
+
return side.coverage === void 0 ? void 0 : settlingAt(side.coverage, address);
|
|
14677
15069
|
}
|
|
14678
15070
|
function coverageOf(side, key) {
|
|
14679
15071
|
const objects = side.coverage?.objects;
|
|
@@ -14795,6 +15187,11 @@ function dictionary(ir) {
|
|
|
14795
15187
|
""
|
|
14796
15188
|
];
|
|
14797
15189
|
lines.push(...observation ? coverageLines(observation.coverage) : [CONFIG_COVERAGE]);
|
|
15190
|
+
const later = [...new Set(Object.keys(ir.resources).map((a) => parseAddress(a).type))].filter((t) => !handledType(t));
|
|
15191
|
+
if (later.length > 0) {
|
|
15192
|
+
const types = later.sort(byCodeUnit).map((t) => `${escapeMarkdown(t)} resources`);
|
|
15193
|
+
lines.push("", `Not described here: ${types.join(", ")}, which a later version of ${bin} handles.`);
|
|
15194
|
+
}
|
|
14798
15195
|
for (const [key, resources] of byObject(ir.resources)) lines.push("", ...objectLines(key, resources, observation === void 0));
|
|
14799
15196
|
if (!observation) lines.push(...overrideLines(ir.targets));
|
|
14800
15197
|
return `${lines.join("\n")}\n`;
|
|
@@ -14888,6 +15285,7 @@ function byObject(resources) {
|
|
|
14888
15285
|
const objects = /* @__PURE__ */ new Map();
|
|
14889
15286
|
for (const address of Object.keys(resources).sort(byCodeUnit)) {
|
|
14890
15287
|
const { type } = parseAddress(address);
|
|
15288
|
+
if (!handledType(type)) continue;
|
|
14891
15289
|
const key = objectOf(address);
|
|
14892
15290
|
const entry = objects.get(key) ?? {
|
|
14893
15291
|
associations: [],
|
|
@@ -15192,9 +15590,11 @@ function rebuild(given) {
|
|
|
15192
15590
|
}
|
|
15193
15591
|
}
|
|
15194
15592
|
excluded.sort((a, b) => byCodeUnit(a.address, b.address));
|
|
15593
|
+
const kept = Object.keys(input.state?.later?.resources ?? {}).sort(byCodeUnit);
|
|
15195
15594
|
const loses = input.state ? lossesOf(input.state, resources) : void 0;
|
|
15196
15595
|
return {
|
|
15197
15596
|
found,
|
|
15597
|
+
kept,
|
|
15198
15598
|
missing,
|
|
15199
15599
|
stale: staleOf(input),
|
|
15200
15600
|
excluded,
|
|
@@ -15234,15 +15634,19 @@ function adopt(address, resource, observed, id) {
|
|
|
15234
15634
|
}
|
|
15235
15635
|
};
|
|
15236
15636
|
}
|
|
15237
|
-
/** The new lineage's state: serial 1, as the first save of a new file has. */
|
|
15238
|
-
function rebuiltState(portalId, lineage, resources) {
|
|
15239
|
-
|
|
15637
|
+
/** The new lineage's state: serial 1, as the first save of a new file has, with what a later version wrote kept. */
|
|
15638
|
+
function rebuiltState(portalId, lineage, resources, previous = null) {
|
|
15639
|
+
const state = {
|
|
15240
15640
|
format: "kalup.state/1",
|
|
15241
15641
|
lineage,
|
|
15242
15642
|
serial: 1,
|
|
15243
15643
|
portalId,
|
|
15244
15644
|
resources
|
|
15245
15645
|
};
|
|
15646
|
+
return previous?.later ? {
|
|
15647
|
+
...state,
|
|
15648
|
+
later: previous.later
|
|
15649
|
+
} : state;
|
|
15246
15650
|
}
|
|
15247
15651
|
function staleOf(input) {
|
|
15248
15652
|
const { loaded, observation, state, target } = input;
|
|
@@ -17806,9 +18210,7 @@ function FileStateStore(dir, options = {}) {
|
|
|
17806
18210
|
file,
|
|
17807
18211
|
fix: `another ${bin} command wrote state for portal ${next.portalId}; run ${bin} plan again`
|
|
17808
18212
|
});
|
|
17809
|
-
const
|
|
17810
|
-
if (problems.length > 0) throw new Error(`refusing to save state that does not match kalup.state/1: ${problems[0]?.message}`);
|
|
17811
|
-
const bytes = `${stableStringify(next)}\n`;
|
|
18213
|
+
const bytes = stateText(next);
|
|
17812
18214
|
if (bytes === current) return false;
|
|
17813
18215
|
const temp = `${file}.tmp-${process.pid}`;
|
|
17814
18216
|
try {
|
|
@@ -18663,6 +19065,7 @@ async function pullTarget(ctx, pulling) {
|
|
|
18663
19065
|
const discovering = ctx.flags.discover === true;
|
|
18664
19066
|
const store = openStateStore(root);
|
|
18665
19067
|
const state = store.read(portalId, targetName);
|
|
19068
|
+
const now = /* @__PURE__ */ new Date();
|
|
18666
19069
|
const portal = await readPortal(http, loaded, target, warnings, {
|
|
18667
19070
|
schemas: discovering,
|
|
18668
19071
|
pipelines: discovering,
|
|
@@ -18681,7 +19084,15 @@ async function pullTarget(ctx, pulling) {
|
|
|
18681
19084
|
const { layout } = loaded;
|
|
18682
19085
|
const files = readProjectFiles(root, layout);
|
|
18683
19086
|
warnings.push(...unmovedState(root, portalId));
|
|
18684
|
-
const
|
|
19087
|
+
const settle = {
|
|
19088
|
+
state,
|
|
19089
|
+
now
|
|
19090
|
+
};
|
|
19091
|
+
const seen = observePortal(portal, loaded, targetName, [], settle);
|
|
19092
|
+
const settling = seen.observation.coverage?.settling ?? {};
|
|
19093
|
+
warnings.push(...seen.issues.filter((i) => i.code === "W_SETTLING"));
|
|
19094
|
+
const selected = addressMatcher(ctx.flags.only);
|
|
19095
|
+
const only = (address) => selected(address) && !Object.hasOwn(settling, address);
|
|
18685
19096
|
const { next, objects, overrides } = mergeFiles(files, portal, loaded, {
|
|
18686
19097
|
only,
|
|
18687
19098
|
overrides: target.overrides,
|
|
@@ -18705,7 +19116,7 @@ async function pullTarget(ctx, pulling) {
|
|
|
18705
19116
|
if (index !== void 0) next[layout.barrel] = index;
|
|
18706
19117
|
const changed = Object.keys(next).filter((file) => next[file] !== files[file]).sort();
|
|
18707
19118
|
const after = loadFiles(next, { layout });
|
|
18708
|
-
const { observation } = observePortal(portal, after, targetName, []);
|
|
19119
|
+
const { observation } = observePortal(portal, after, targetName, [], settle);
|
|
18709
19120
|
const data = {
|
|
18710
19121
|
target: targetName,
|
|
18711
19122
|
portalId,
|
|
@@ -18727,7 +19138,8 @@ async function pullTarget(ctx, pulling) {
|
|
|
18727
19138
|
created = state === null && data.state.serial !== null;
|
|
18728
19139
|
}
|
|
18729
19140
|
}
|
|
18730
|
-
|
|
19141
|
+
const unsettled = seen.issues.some((i) => i.code === "W_SETTLING");
|
|
19142
|
+
let exitCode = ctx.flags.check && ctx.flags.exitCode && (changed.length > 0 || differs(objects, warnings) || unsettled) ? exitCodes.differences : exitCodes.done;
|
|
18731
19143
|
if (incomplete) exitCode = exitCodes.error;
|
|
18732
19144
|
const newState = created && data.state !== void 0 ? shownPath(ctx.cwd, data.state.path) : void 0;
|
|
18733
19145
|
return {
|
|
@@ -19556,7 +19968,7 @@ function openJournal(stateDirectory, run, now = /* @__PURE__ */ new Date(), io =
|
|
|
19556
19968
|
outcome: entry.outcome,
|
|
19557
19969
|
ms: entry.ms
|
|
19558
19970
|
};
|
|
19559
|
-
if (Object.values(line).some((value) => typeof value === "string" &&
|
|
19971
|
+
if (Object.values(line).some((value) => typeof value === "string" && holdsKey(value, keys))) throw new Error("refusing to journal a line that holds a key");
|
|
19560
19972
|
const fd = io.openSync(path, "a");
|
|
19561
19973
|
try {
|
|
19562
19974
|
writeAll(io.writeSync, fd, `${JSON.stringify(line)}\n`);
|
|
@@ -19620,7 +20032,13 @@ async function plan(ctx) {
|
|
|
19620
20032
|
async function planTarget(http, input) {
|
|
19621
20033
|
const { loaded, portal, root, take, target } = input;
|
|
19622
20034
|
const state = openStateStore(root).read(portal.portalId, target);
|
|
19623
|
-
const { observation, issues } = await observeTarget(http, loaded, target, {
|
|
20035
|
+
const { observation, issues } = await observeTarget(http, loaded, target, {
|
|
20036
|
+
associationIds: associationIds(state),
|
|
20037
|
+
settle: {
|
|
20038
|
+
state,
|
|
20039
|
+
now: /* @__PURE__ */ new Date()
|
|
20040
|
+
}
|
|
20041
|
+
});
|
|
19624
20042
|
issues.push(...unfinished(state?.lastApply), ...unmovedState(root, portal.portalId));
|
|
19625
20043
|
const reads = planReads({
|
|
19626
20044
|
loaded,
|
|
@@ -20325,7 +20743,14 @@ async function compare(ctx) {
|
|
|
20325
20743
|
for (const side of opened) if ("http" in side) await guardPortal(side.http, side.guard);
|
|
20326
20744
|
const observations = [];
|
|
20327
20745
|
for (const side of opened) if ("http" in side) {
|
|
20328
|
-
const
|
|
20746
|
+
const state = openStateStore(project.root).read(side.guard.portalId, side.name);
|
|
20747
|
+
const read = await observeTarget(side.http, project?.loaded, side.name, {
|
|
20748
|
+
associationIds: associationIds(state),
|
|
20749
|
+
settle: {
|
|
20750
|
+
state,
|
|
20751
|
+
now: /* @__PURE__ */ new Date()
|
|
20752
|
+
}
|
|
20753
|
+
});
|
|
20329
20754
|
issues.push(...read.issues);
|
|
20330
20755
|
observations.push(read.observation);
|
|
20331
20756
|
} else observations.push(side.observation);
|
|
@@ -21186,7 +21611,14 @@ async function snapshot(ctx) {
|
|
|
21186
21611
|
const { name: target, via } = await resolveTarget(ctx, loaded.config);
|
|
21187
21612
|
const { http, guard } = connect(root, loaded, target, warnings);
|
|
21188
21613
|
await guardPortal(http, guard);
|
|
21189
|
-
const
|
|
21614
|
+
const state = openStateStore(root).read(guard.portalId, target);
|
|
21615
|
+
const { observation, issues: read } = await observeTarget(http, loaded, target, {
|
|
21616
|
+
associationIds: associationIds(state),
|
|
21617
|
+
settle: {
|
|
21618
|
+
state,
|
|
21619
|
+
now: /* @__PURE__ */ new Date()
|
|
21620
|
+
}
|
|
21621
|
+
});
|
|
21190
21622
|
const observedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21191
21623
|
const document = toSnapshot(observation, {
|
|
21192
21624
|
generator: loaded.ir.generator,
|
|
@@ -21264,8 +21696,14 @@ async function stateRebuild(ctx) {
|
|
|
21264
21696
|
});
|
|
21265
21697
|
const store = openStateStore(root);
|
|
21266
21698
|
const state = store.read(portalId, name);
|
|
21267
|
-
const { observation, issues: read } = await observeTarget(http, loaded, name, {
|
|
21268
|
-
|
|
21699
|
+
const { observation, issues: read } = await observeTarget(http, loaded, name, {
|
|
21700
|
+
associationIds: associationIds(state),
|
|
21701
|
+
settle: {
|
|
21702
|
+
state,
|
|
21703
|
+
now: /* @__PURE__ */ new Date()
|
|
21704
|
+
}
|
|
21705
|
+
});
|
|
21706
|
+
if (ctx.flags.write) requireComplete(observation, command, [...warnings, ...read], Object.keys(loaded.ir.resources));
|
|
21269
21707
|
const report = rebuild({
|
|
21270
21708
|
loaded,
|
|
21271
21709
|
observation,
|
|
@@ -21280,6 +21718,7 @@ async function stateRebuild(ctx) {
|
|
|
21280
21718
|
missing: report.missing,
|
|
21281
21719
|
stale: report.stale,
|
|
21282
21720
|
excluded: report.excluded,
|
|
21721
|
+
...report.kept.length > 0 ? { kept: report.kept } : {},
|
|
21283
21722
|
...report.loses ? { loses: report.loses } : {},
|
|
21284
21723
|
written: false
|
|
21285
21724
|
};
|
|
@@ -21299,7 +21738,7 @@ async function stateRebuild(ctx) {
|
|
|
21299
21738
|
const lock = await acquirePortalLock(portalId, { command: "state rebuild --write" });
|
|
21300
21739
|
try {
|
|
21301
21740
|
sameState(state, store.read(portalId, name), portalId, command);
|
|
21302
|
-
const written = replaceState(store, portalId, report.resources, command);
|
|
21741
|
+
const written = replaceState(store, portalId, report.resources, command, state);
|
|
21303
21742
|
Object.assign(data, written, { written: true });
|
|
21304
21743
|
} finally {
|
|
21305
21744
|
lock.release();
|
|
@@ -21315,9 +21754,9 @@ async function stateRebuild(ctx) {
|
|
|
21315
21754
|
* Archives the portal's state file, when it has one, and writes a new lineage holding `resources`. The caller holds the
|
|
21316
21755
|
* portal lock and has checked the file is the one it showed. `command` is the one to run again after a failed save.
|
|
21317
21756
|
*/
|
|
21318
|
-
function replaceState(store, portalId, resources, command) {
|
|
21757
|
+
function replaceState(store, portalId, resources, command, previous) {
|
|
21319
21758
|
const archived = store.archive(portalId, "state rebuild");
|
|
21320
|
-
const next = rebuiltState(portalId, store.newLineage(), resources);
|
|
21759
|
+
const next = rebuiltState(portalId, store.newLineage(), resources, previous);
|
|
21321
21760
|
try {
|
|
21322
21761
|
store.write(next, null);
|
|
21323
21762
|
} catch (error) {
|
|
@@ -21353,12 +21792,35 @@ function sameState(shown, now, portalId, command) {
|
|
|
21353
21792
|
});
|
|
21354
21793
|
}
|
|
21355
21794
|
/**
|
|
21356
|
-
* E_INCOMPLETE when the read left out a list
|
|
21357
|
-
*
|
|
21795
|
+
* E_INCOMPLETE when the read left out what config names: a list it needs, a property it could not capture, a resource
|
|
21796
|
+
* settling after an apply, or an association a type HubSpot does not name yet may be. A rebuild would drop every entry
|
|
21797
|
+
* there, created origins and bases included, though it could not check them. What settles or is unnamed where config
|
|
21798
|
+
* names nothing stops nothing. Nothing has been written. The report alone still runs.
|
|
21358
21799
|
*/
|
|
21359
|
-
function requireComplete(observation, command, issues) {
|
|
21800
|
+
function requireComplete(observation, command, issues, named) {
|
|
21360
21801
|
const { coverage } = observation;
|
|
21361
21802
|
if (coverage === void 0 || coverage.complete) return;
|
|
21803
|
+
const { unread, scopes } = unreadOf(coverage);
|
|
21804
|
+
const { settling, unnamed } = waitingOn(observation, named);
|
|
21805
|
+
const until = settling.map(([, s]) => s.until).sort()[settling.length - 1];
|
|
21806
|
+
const items = [
|
|
21807
|
+
...unread,
|
|
21808
|
+
...settling.map(([address, s]) => `${sanitize(address)} (settling after an apply until ${s.until})`),
|
|
21809
|
+
...unnamed.map((pair) => `the associations between ${sanitize(pair)} (a type HubSpot does not name yet)`)
|
|
21810
|
+
];
|
|
21811
|
+
if (items.length === 0) return;
|
|
21812
|
+
const fixes = [];
|
|
21813
|
+
if (scopes.length > 0) fixes.push(`add the scope${scopes.length > 1 ? "s" : ""} ${scopes.join(", ")} to the write key, then run ${command} again`);
|
|
21814
|
+
else if (unread.length > 0) fixes.push(`fix what the issues that follow name, then run ${command} again`);
|
|
21815
|
+
if (until !== void 0) fixes.push(`run ${command} again after ${until}`);
|
|
21816
|
+
else if (unnamed.length > 0) fixes.push(`run ${command} again in a few minutes, once HubSpot names the new association`);
|
|
21817
|
+
throw new KalupError([{
|
|
21818
|
+
code: "E_INCOMPLETE",
|
|
21819
|
+
message: `the read did not cover everything config names: ${items.join("; ")}. A rebuild would drop what it could not check. Nothing was written.`,
|
|
21820
|
+
fix: fixes.join("; ")
|
|
21821
|
+
}, ...issues]);
|
|
21822
|
+
}
|
|
21823
|
+
function unreadOf(coverage) {
|
|
21362
21824
|
const unread = [];
|
|
21363
21825
|
const scopes = /* @__PURE__ */ new Set();
|
|
21364
21826
|
for (const [object, c] of Object.entries(coverage.objects)) {
|
|
@@ -21367,13 +21829,13 @@ function requireComplete(observation, command, issues) {
|
|
|
21367
21829
|
if (c.missingScope !== void 0) scopes.add(c.missingScope);
|
|
21368
21830
|
}
|
|
21369
21831
|
if (c.unaddressable !== void 0) unread.push(`${c.unaddressable.map((n) => `property:${object}/${sanitize(n)}`).join(", ")} (a group name no address can hold)`);
|
|
21832
|
+
if (c.pipelines?.status === "unreadable") unread.push(`the pipelines of ${object}`);
|
|
21833
|
+
for (const [other, pair] of Object.entries(c.associations?.with ?? {})) if (pair.status === "unreadable") unread.push(`the labels from ${object} to ${other}`);
|
|
21370
21834
|
}
|
|
21371
|
-
|
|
21372
|
-
|
|
21373
|
-
|
|
21374
|
-
|
|
21375
|
-
fix: `${add}, then run ${command} again`
|
|
21376
|
-
}, ...issues]);
|
|
21835
|
+
return {
|
|
21836
|
+
unread,
|
|
21837
|
+
scopes: [...scopes]
|
|
21838
|
+
};
|
|
21377
21839
|
}
|
|
21378
21840
|
/** The report as lines a person reads. Every portal string is sanitized. */
|
|
21379
21841
|
function reportLines(report, total, state, path) {
|
|
@@ -21387,6 +21849,7 @@ function reportLines(report, total, state, path) {
|
|
|
21387
21849
|
};
|
|
21388
21850
|
for (const s of report.stale) lines.push(` stale entry: ${s.address} (${why[s.reason]}${s.id === null ? "" : `, records ${sanitize(s.id)}`})`);
|
|
21389
21851
|
for (const e of report.excluded) lines.push(` not adopted: ${e.address} (${e.reason})`);
|
|
21852
|
+
for (const address of report.kept) lines.push(` kept as it is: ${sanitize(address)} (a later version of ${bin} manages it)`);
|
|
21390
21853
|
return lines;
|
|
21391
21854
|
}
|
|
21392
21855
|
/** The config resources a rebuild can adopt. */
|
|
@@ -21774,8 +22237,11 @@ async function targetRebind(ctx) {
|
|
|
21774
22237
|
const store = openStateStore(root);
|
|
21775
22238
|
store.read(from, name);
|
|
21776
22239
|
const current = store.read(portalId, name);
|
|
21777
|
-
const { observation, issues: observed } = await observeTarget(http, loaded, name
|
|
21778
|
-
|
|
22240
|
+
const { observation, issues: observed } = await observeTarget(http, loaded, name, { settle: {
|
|
22241
|
+
state: current,
|
|
22242
|
+
now: /* @__PURE__ */ new Date()
|
|
22243
|
+
} });
|
|
22244
|
+
requireComplete(observation, command, [...warnings, ...observed], Object.keys(loaded.ir.resources));
|
|
21779
22245
|
const report = rebuild({
|
|
21780
22246
|
loaded,
|
|
21781
22247
|
observation,
|
|
@@ -21789,7 +22255,7 @@ async function targetRebind(ctx) {
|
|
|
21789
22255
|
`${CONFIG} gets portalId ${portalId}; the state file of portal ${from} is archived; plans saved for portal ${from} no longer apply.`
|
|
21790
22256
|
]);
|
|
21791
22257
|
await confirmTarget(ctx.prompt, name, "Type the target name to rebind it:");
|
|
21792
|
-
const written = replaceState(store, portalId, report.resources, command);
|
|
22258
|
+
const written = replaceState(store, portalId, report.resources, command, current);
|
|
21793
22259
|
writeStaged(root, { [CONFIG]: configText });
|
|
21794
22260
|
const archived = store.archive(from, "target rebind");
|
|
21795
22261
|
const data = {
|
|
@@ -21803,6 +22269,7 @@ async function targetRebind(ctx) {
|
|
|
21803
22269
|
missing: report.missing,
|
|
21804
22270
|
stale: report.stale,
|
|
21805
22271
|
excluded: report.excluded,
|
|
22272
|
+
...report.kept.length > 0 ? { kept: report.kept } : {},
|
|
21806
22273
|
...archived === null ? {} : { archived }
|
|
21807
22274
|
};
|
|
21808
22275
|
const lines = [
|