kalup 0.3.0 → 0.4.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/README.md +4 -4
- package/dist/{commands-T4j_GVBD.mjs → commands-CHucC_PV.mjs} +2866 -597
- package/dist/commands.d.mts +1 -1
- package/dist/commands.mjs +1 -1
- package/dist/{context-C1tH5K0x.d.mts → context-B7bI9BjL.d.mts} +42 -0
- package/dist/{host-CDDeLV5T.mjs → host-D1HtgrVd.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 +41 -1
- package/dist/schemas/plan-1.schema.json +18 -0
- package/docs/apply.md +5 -3
- package/docs/config.md +37 -8
- package/docs/dictionary.md +3 -3
- package/docs/errors/E_DUPLICATE_LABEL.md +24 -0
- package/docs/errors/E_MISSING_EXPORT.md +2 -2
- package/docs/errors/E_PIPELINE_FIELD.md +23 -0
- package/docs/errors/E_PIPELINE_ID.md +17 -0
- package/docs/errors/E_PIPELINE_STAGES.md +17 -0
- package/docs/errors/E_TOMBSTONE_ADDRESS.md +3 -3
- package/docs/errors/E_UNSUPPORTED_FILE.md +3 -3
- package/docs/errors/E_WRITE_IN_READ_MODE.md +1 -1
- package/docs/errors/E_WRITE_NOT_ALLOWED.md +1 -1
- package/docs/plan.md +20 -7
- package/docs/pull.md +10 -6
- package/docs/rm.md +6 -4
- package/docs/snapshot.md +2 -2
- package/docs/targets.md +2 -2
- package/package.json +2 -2
|
@@ -234,6 +234,20 @@ var ir_1_schema_default = {
|
|
|
234
234
|
"properties": { "type": { "const": "object" } }
|
|
235
235
|
},
|
|
236
236
|
"then": { "properties": { "definition": { "$ref": "#/$defs/objectDefinition" } } }
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"if": {
|
|
240
|
+
"required": ["type"],
|
|
241
|
+
"properties": { "type": { "const": "pipeline" } }
|
|
242
|
+
},
|
|
243
|
+
"then": { "properties": { "definition": { "$ref": "#/$defs/pipelineDefinition" } } }
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"if": {
|
|
247
|
+
"required": ["type"],
|
|
248
|
+
"properties": { "type": { "const": "stage" } }
|
|
249
|
+
},
|
|
250
|
+
"then": { "properties": { "definition": { "$ref": "#/$defs/stageDefinition" } } }
|
|
237
251
|
}
|
|
238
252
|
]
|
|
239
253
|
},
|
|
@@ -357,6 +371,43 @@ var ir_1_schema_default = {
|
|
|
357
371
|
},
|
|
358
372
|
"additionalProperties": false
|
|
359
373
|
},
|
|
374
|
+
"pipelineDefinition": {
|
|
375
|
+
"description": "A pipeline. Its stages are resources of their own; stages lists their IDs in display order.",
|
|
376
|
+
"type": "object",
|
|
377
|
+
"required": [
|
|
378
|
+
"label",
|
|
379
|
+
"displayOrder",
|
|
380
|
+
"stages"
|
|
381
|
+
],
|
|
382
|
+
"properties": {
|
|
383
|
+
"label": { "type": "string" },
|
|
384
|
+
"displayOrder": {
|
|
385
|
+
"type": "integer",
|
|
386
|
+
"minimum": 0
|
|
387
|
+
},
|
|
388
|
+
"stages": {
|
|
389
|
+
"type": "array",
|
|
390
|
+
"items": { "type": "string" }
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
"additionalProperties": false
|
|
394
|
+
},
|
|
395
|
+
"stageDefinition": {
|
|
396
|
+
"description": "A pipeline stage. Its metadata field depends on the object: probability on deals, ticketState on tickets, state on custom objects.",
|
|
397
|
+
"type": "object",
|
|
398
|
+
"required": ["label"],
|
|
399
|
+
"properties": {
|
|
400
|
+
"label": { "type": "string" },
|
|
401
|
+
"probability": {
|
|
402
|
+
"type": "number",
|
|
403
|
+
"minimum": 0,
|
|
404
|
+
"maximum": 1
|
|
405
|
+
},
|
|
406
|
+
"ticketState": { "enum": ["OPEN", "CLOSED"] },
|
|
407
|
+
"state": { "enum": ["OPEN", "CLOSED"] }
|
|
408
|
+
},
|
|
409
|
+
"additionalProperties": false
|
|
410
|
+
},
|
|
360
411
|
"binding": {
|
|
361
412
|
"description": "App terms, filled in by the loader. key and codec for properties, export for objects.",
|
|
362
413
|
"type": "object",
|
|
@@ -541,6 +592,14 @@ var ir_1_schema_default = {
|
|
|
541
592
|
"object": {
|
|
542
593
|
"type": "array",
|
|
543
594
|
"items": { "type": "string" }
|
|
595
|
+
},
|
|
596
|
+
"pipeline": {
|
|
597
|
+
"type": "array",
|
|
598
|
+
"items": { "type": "string" }
|
|
599
|
+
},
|
|
600
|
+
"stage": {
|
|
601
|
+
"type": "array",
|
|
602
|
+
"items": { "type": "string" }
|
|
544
603
|
}
|
|
545
604
|
},
|
|
546
605
|
"additionalProperties": false
|
|
@@ -628,6 +687,16 @@ var ir_1_schema_default = {
|
|
|
628
687
|
"type": "object",
|
|
629
688
|
"patternProperties": { "^[a-z]+:\\S+$": { "type": "string" } },
|
|
630
689
|
"additionalProperties": false
|
|
690
|
+
},
|
|
691
|
+
"pipelines": {
|
|
692
|
+
"description": "Whether the object's pipelines list was read. Absent when its pipelines are not in scope.",
|
|
693
|
+
"type": "object",
|
|
694
|
+
"required": ["status"],
|
|
695
|
+
"properties": {
|
|
696
|
+
"status": { "enum": ["read", "unreadable"] },
|
|
697
|
+
"missingScope": { "type": "string" }
|
|
698
|
+
},
|
|
699
|
+
"additionalProperties": false
|
|
631
700
|
}
|
|
632
701
|
},
|
|
633
702
|
"additionalProperties": false
|
|
@@ -1261,6 +1330,44 @@ const RESERVED_PREFIX = /^(hs_|a\d+_)/;
|
|
|
1261
1330
|
function reservedPrefix(name) {
|
|
1262
1331
|
return RESERVED_PREFIX.exec(name)?.[1];
|
|
1263
1332
|
}
|
|
1333
|
+
/** Every stage metadata field Kalup manages, in the order the writer and the plan list them. */
|
|
1334
|
+
const STAGE_FIELDS = [
|
|
1335
|
+
"probability",
|
|
1336
|
+
"ticketState",
|
|
1337
|
+
"state"
|
|
1338
|
+
];
|
|
1339
|
+
const FIELD_BY_OBJECT = {
|
|
1340
|
+
deals: "probability",
|
|
1341
|
+
tickets: "ticketState"
|
|
1342
|
+
};
|
|
1343
|
+
/**
|
|
1344
|
+
* The standard objects that have pipelines: the pipelines path answered for each on 2026-10-05 (leads with a 403 for a
|
|
1345
|
+
* missing scope). Every custom object may have them too.
|
|
1346
|
+
*/
|
|
1347
|
+
const PIPELINE_OBJECTS = /* @__PURE__ */ new Set([
|
|
1348
|
+
"appointments",
|
|
1349
|
+
"companies",
|
|
1350
|
+
"contacts",
|
|
1351
|
+
"courses",
|
|
1352
|
+
"deals",
|
|
1353
|
+
"leads",
|
|
1354
|
+
"listings",
|
|
1355
|
+
"orders",
|
|
1356
|
+
"services",
|
|
1357
|
+
"tickets"
|
|
1358
|
+
]);
|
|
1359
|
+
/** Whether an object can have pipelines: one of PIPELINE_OBJECTS, or a custom object. */
|
|
1360
|
+
function hasPipelines(object, custom) {
|
|
1361
|
+
return custom || PIPELINE_OBJECTS.has(object);
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* The metadata field of a stage of `object`, or undefined for an object whose pipelines Kalup reads and compares but
|
|
1365
|
+
* does not write: every standard object but deals and tickets. `custom` says whether the object is a custom object.
|
|
1366
|
+
*/
|
|
1367
|
+
function stageField(object, custom) {
|
|
1368
|
+
if (custom) return "state";
|
|
1369
|
+
return Object.hasOwn(FIELD_BY_OBJECT, object) ? FIELD_BY_OBJECT[object] : void 0;
|
|
1370
|
+
}
|
|
1264
1371
|
const ADDRESS = /^[a-z]+:\S+$/;
|
|
1265
1372
|
/** Whether `value` is an address: a lowercase type, a colon and a path without whitespace. */
|
|
1266
1373
|
function isAddress(value) {
|
|
@@ -1314,6 +1421,124 @@ const PROPERTY_FIELDS = [
|
|
|
1314
1421
|
"externalOptions",
|
|
1315
1422
|
"referencedObjectType"
|
|
1316
1423
|
];
|
|
1424
|
+
/**
|
|
1425
|
+
* The objects whose properties the properties API lists by name. A config key outside this set names a custom object
|
|
1426
|
+
* schema. The set is the 2026-09 `crm.schemas.<object>.read` scope list plus the objects that carry their own scopes
|
|
1427
|
+
* (products, leads, goals, marketing events, feedback submissions, users).
|
|
1428
|
+
*/
|
|
1429
|
+
const STANDARD_OBJECTS = /* @__PURE__ */ new Set([
|
|
1430
|
+
"appointments",
|
|
1431
|
+
"calls",
|
|
1432
|
+
"carts",
|
|
1433
|
+
"commerce_payments",
|
|
1434
|
+
"communications",
|
|
1435
|
+
"companies",
|
|
1436
|
+
"contacts",
|
|
1437
|
+
"courses",
|
|
1438
|
+
"deals",
|
|
1439
|
+
"emails",
|
|
1440
|
+
"feedback_submissions",
|
|
1441
|
+
"goals",
|
|
1442
|
+
"invoices",
|
|
1443
|
+
"leads",
|
|
1444
|
+
"line_items",
|
|
1445
|
+
"listings",
|
|
1446
|
+
"marketing_events",
|
|
1447
|
+
"meetings",
|
|
1448
|
+
"notes",
|
|
1449
|
+
"orders",
|
|
1450
|
+
"postal_mail",
|
|
1451
|
+
"products",
|
|
1452
|
+
"projects",
|
|
1453
|
+
"quotes",
|
|
1454
|
+
"services",
|
|
1455
|
+
"subscriptions",
|
|
1456
|
+
"tasks",
|
|
1457
|
+
"tickets",
|
|
1458
|
+
"users"
|
|
1459
|
+
]);
|
|
1460
|
+
/**
|
|
1461
|
+
* The object type ID of each standard object, by config key. Limits Tracking keys its per-object entries by these.
|
|
1462
|
+
* Source: "Object type ID values", retrieved 2026-09-24:
|
|
1463
|
+
* https://developers.hubspot.com/docs/api-reference/latest/crm/understanding-the-crm
|
|
1464
|
+
* A plain record: look a key up with `Object.hasOwn`, so `constructor` finds nothing.
|
|
1465
|
+
*/
|
|
1466
|
+
const STANDARD_OBJECT_TYPE_IDS = {
|
|
1467
|
+
appointments: "0-421",
|
|
1468
|
+
calls: "0-48",
|
|
1469
|
+
carts: "0-142",
|
|
1470
|
+
commerce_payments: "0-101",
|
|
1471
|
+
communications: "0-18",
|
|
1472
|
+
companies: "0-2",
|
|
1473
|
+
contacts: "0-1",
|
|
1474
|
+
courses: "0-410",
|
|
1475
|
+
deals: "0-3",
|
|
1476
|
+
emails: "0-49",
|
|
1477
|
+
feedback_submissions: "0-19",
|
|
1478
|
+
goals: "0-74",
|
|
1479
|
+
invoices: "0-53",
|
|
1480
|
+
leads: "0-136",
|
|
1481
|
+
line_items: "0-8",
|
|
1482
|
+
listings: "0-420",
|
|
1483
|
+
marketing_events: "0-54",
|
|
1484
|
+
meetings: "0-47",
|
|
1485
|
+
notes: "0-46",
|
|
1486
|
+
orders: "0-123",
|
|
1487
|
+
postal_mail: "0-116",
|
|
1488
|
+
products: "0-7",
|
|
1489
|
+
projects: "0-970",
|
|
1490
|
+
quotes: "0-14",
|
|
1491
|
+
services: "0-162",
|
|
1492
|
+
subscriptions: "0-69",
|
|
1493
|
+
tasks: "0-27",
|
|
1494
|
+
tickets: "0-5",
|
|
1495
|
+
users: "0-115"
|
|
1496
|
+
};
|
|
1497
|
+
/** One object's pull scope from its settings under `objects` and the property names its object file defines. */
|
|
1498
|
+
function scopeOf(scope = {}, defined = []) {
|
|
1499
|
+
return {
|
|
1500
|
+
custom: scope.custom ?? true,
|
|
1501
|
+
defined: new Set(defined),
|
|
1502
|
+
exclude: excluder(scope.exclude),
|
|
1503
|
+
include: new Set(scope.include ?? [])
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/** The names of the properties the object files define on `object`, references included. */
|
|
1507
|
+
function definedOn(ir, object) {
|
|
1508
|
+
const prefix = `property:${object}/`;
|
|
1509
|
+
return Object.keys(ir.resources).filter((address) => address.startsWith(prefix)).map((address) => address.slice(prefix.length));
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* A portal property is in scope when the object files define it or `include` names it, whatever `custom` and
|
|
1513
|
+
* `exclude` say, or when it is custom, `custom` is on and `exclude` does not name it. validate refuses a name both
|
|
1514
|
+
* `include` and `exclude` hold.
|
|
1515
|
+
*/
|
|
1516
|
+
function inScope(scope, property) {
|
|
1517
|
+
const { name } = property;
|
|
1518
|
+
return scope.defined.has(name) || scope.include.has(name) || scope.custom && !property.hubspotDefined && !scope.exclude(name);
|
|
1519
|
+
}
|
|
1520
|
+
/** The `exclude` patterns of one object as a predicate over internal names. */
|
|
1521
|
+
function excluder(patterns = []) {
|
|
1522
|
+
const matchers = patterns.map(addressMatcher);
|
|
1523
|
+
return (name) => matchers.some((matches) => matches(name));
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Whether an object's pipelines are in scope: `pipelines: true` under `objects`, or a pipeline or stage of it in the
|
|
1527
|
+
* files or in removed.ts. Unlike `custom`, which filters what pull writes while the properties are read anyway, a
|
|
1528
|
+
* pipelines list nobody asked for is never read: an upgrade must not widen what a project manages without a line in
|
|
1529
|
+
* config saying so.
|
|
1530
|
+
*/
|
|
1531
|
+
function pipelinesInScope(scope, ir, object) {
|
|
1532
|
+
const ours = (address) => address.startsWith(`pipeline:${object}/`) || address.startsWith(`stage:${object}/`);
|
|
1533
|
+
return scope?.pipelines === true || Object.keys(ir.resources).some(ours) || Object.keys(ir.tombstones).some(ours);
|
|
1534
|
+
}
|
|
1535
|
+
/** The --only glob as a predicate over addresses. `*` matches any run of characters, `/` included. */
|
|
1536
|
+
function addressMatcher(glob) {
|
|
1537
|
+
if (glob === void 0) return () => true;
|
|
1538
|
+
const literals = glob.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1539
|
+
const pattern = new RegExp(`^${literals.join(".*")}$`);
|
|
1540
|
+
return (address) => pattern.test(address);
|
|
1541
|
+
}
|
|
1317
1542
|
const punct = "{}[]()<>,:;.=&|?!+-*/%~^@#";
|
|
1318
1543
|
const escapes = {
|
|
1319
1544
|
n: "\n",
|
|
@@ -1542,8 +1767,8 @@ const CHAIN_FLAGS = [
|
|
|
1542
1767
|
const toolOwned = ["@kalup/core", "kalup"];
|
|
1543
1768
|
const typeLineFix = "write `export type <Name>Data = InferProperties<typeof <Name>.properties> & { id: string }`";
|
|
1544
1769
|
/**
|
|
1545
|
-
* Parses one object file, kalup.config.ts or removed.ts into plain data. Throws IssueError on anything
|
|
1546
|
-
* the grammar. `kind` is the kind the file must be, when its path decides it; otherwise the content does.
|
|
1770
|
+
* Parses one object file, pipeline file, kalup.config.ts or removed.ts into plain data. Throws IssueError on anything
|
|
1771
|
+
* outside the grammar. `kind` is the kind the file must be, when its path decides it; otherwise the content does.
|
|
1547
1772
|
*/
|
|
1548
1773
|
function read(text, file, kind) {
|
|
1549
1774
|
const src = (text.charCodeAt(0) === 65279 ? text.slice(1) : text).replace(/\r\n?/g, "\n");
|
|
@@ -1574,6 +1799,14 @@ function read(text, file, kind) {
|
|
|
1574
1799
|
},
|
|
1575
1800
|
lines: s.lines
|
|
1576
1801
|
};
|
|
1802
|
+
if (found === "pipeline") return {
|
|
1803
|
+
kind: "pipeline",
|
|
1804
|
+
data: {
|
|
1805
|
+
...top,
|
|
1806
|
+
...parsePipelineFile(s, imports)
|
|
1807
|
+
},
|
|
1808
|
+
lines: s.lines
|
|
1809
|
+
};
|
|
1577
1810
|
return {
|
|
1578
1811
|
kind: "object",
|
|
1579
1812
|
data: {
|
|
@@ -1586,7 +1819,7 @@ function read(text, file, kind) {
|
|
|
1586
1819
|
function kindAt(s) {
|
|
1587
1820
|
let j = s.i;
|
|
1588
1821
|
while (at(s, j).kind === "comment") j += 1;
|
|
1589
|
-
if (!(is(at(s, j), "ident", "export") && is(at(s, j + 1), "ident", "default"))) return "object";
|
|
1822
|
+
if (!(is(at(s, j), "ident", "export") && is(at(s, j + 1), "ident", "default"))) return is(at(s, j + 1), "ident", "const") && is(at(s, j + 4), "ident", "definePipeline") ? "pipeline" : "object";
|
|
1590
1823
|
return is(at(s, j + 2), "ident", "defineRemoved") ? "removed" : "config";
|
|
1591
1824
|
}
|
|
1592
1825
|
function at(s, i) {
|
|
@@ -1795,6 +2028,10 @@ const SETTINGS = {
|
|
|
1795
2028
|
levels: ["object"],
|
|
1796
2029
|
example: "'Firm'"
|
|
1797
2030
|
},
|
|
2031
|
+
pipelines: {
|
|
2032
|
+
levels: ["object"],
|
|
2033
|
+
example: "true"
|
|
2034
|
+
},
|
|
1798
2035
|
protected: {
|
|
1799
2036
|
levels: ["target"],
|
|
1800
2037
|
example: "true"
|
|
@@ -1915,10 +2152,19 @@ const definitionFields = {
|
|
|
1915
2152
|
lifecycle: lifecycle$1
|
|
1916
2153
|
};
|
|
1917
2154
|
const definition = shape(definitionFields, [], misplaced("a property definition"));
|
|
1918
|
-
const
|
|
2155
|
+
const stageState = oneOf("OPEN", "CLOSED");
|
|
2156
|
+
const stageFields$1 = {
|
|
2157
|
+
probability: num,
|
|
2158
|
+
ticketState: stageState,
|
|
2159
|
+
state: stageState
|
|
2160
|
+
};
|
|
2161
|
+
const overrideDefinition = shape({
|
|
2162
|
+
...definitionFields,
|
|
2163
|
+
...stageFields$1
|
|
2164
|
+
}, [], (key) => ({
|
|
1919
2165
|
code: "E_OVERRIDE_DEFINITION",
|
|
1920
2166
|
message: key === "type" ? "'type' comes from the builder, so it cannot differ per target" : `unknown field '${key}' in a definition override`,
|
|
1921
|
-
fix: "override only label, description, group, fieldType, formField, options, hidden, displayOrder, the display hints, calculationFormula or lifecycle"
|
|
2167
|
+
fix: "override only label, description, group, fieldType, formField, options, hidden, displayOrder, the display hints, calculationFormula or lifecycle, or a stage metadata field"
|
|
1922
2168
|
}));
|
|
1923
2169
|
const customFields = {
|
|
1924
2170
|
labels: shape({
|
|
@@ -1956,7 +2202,8 @@ const config = shape({
|
|
|
1956
2202
|
include: list$1(str),
|
|
1957
2203
|
exclude: list$1(str),
|
|
1958
2204
|
custom: bool,
|
|
1959
|
-
as: str
|
|
2205
|
+
as: str,
|
|
2206
|
+
pipelines: bool
|
|
1960
2207
|
}, [], misplaced())),
|
|
1961
2208
|
targets: map(shape({
|
|
1962
2209
|
portalId: num,
|
|
@@ -2127,6 +2374,75 @@ function parseTypeLine(s, exports, typed) {
|
|
|
2127
2374
|
if (typed.has(ref.value)) fail$1(s, "E_NOT_DATA", nameTok, `'${nameTok.value}' is exported twice`, "remove the duplicate type export");
|
|
2128
2375
|
typed.add(ref.value);
|
|
2129
2376
|
}
|
|
2377
|
+
const PIPELINE_FIX = "only imports and `export const <Name> = definePipeline('<object>', {...})` are allowed here";
|
|
2378
|
+
const stage = shape({
|
|
2379
|
+
id: str,
|
|
2380
|
+
label: str,
|
|
2381
|
+
...stageFields$1
|
|
2382
|
+
}, ["id", "label"]);
|
|
2383
|
+
function parsePipelineFile(s, imports) {
|
|
2384
|
+
const exports = [];
|
|
2385
|
+
for (;;) {
|
|
2386
|
+
const cs = takeComments(s);
|
|
2387
|
+
if (at(s, s.i).kind === "eof") {
|
|
2388
|
+
if (!exports.length) fail$1(s, "E_MISSING_EXPORT", at(s, 0), "no definePipeline export in this file", "add `export const <Name> = definePipeline('<object>', {...})`");
|
|
2389
|
+
if (cs[0]) failComment(s, cs[0]);
|
|
2390
|
+
return {
|
|
2391
|
+
imports,
|
|
2392
|
+
exports
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
expect(s, "ident", "export", PIPELINE_FIX);
|
|
2396
|
+
expect(s, "ident", "const", PIPELINE_FIX);
|
|
2397
|
+
exports.push(parsePipeline(s, cs, exports));
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function parsePipeline(s, cs, exports) {
|
|
2401
|
+
const nameTok = next(s);
|
|
2402
|
+
if (nameTok.kind !== "ident") fail$1(s, "E_NOT_DATA", nameTok, `expected an export name but found ${show$3(nameTok)}`, "write export const <Name> = definePipeline(...)");
|
|
2403
|
+
const name = nameTok.value;
|
|
2404
|
+
if (exports.some((x) => x.name === name)) fail$1(s, "E_DUPLICATE_KEY", nameTok, `duplicate export '${name}'`, "rename one of the two exports", name);
|
|
2405
|
+
expect(s, "punct", "=", "write export const <Name> = definePipeline(...)", name);
|
|
2406
|
+
const b = next(s);
|
|
2407
|
+
if (!is(b, "ident", "definePipeline")) fail$1(s, "E_NOT_DATA", b, `'${b.value}' is not definePipeline`, "write definePipeline('<object>', {...})", name);
|
|
2408
|
+
expect(s, "punct", "(", void 0, name);
|
|
2409
|
+
const object = str(s, name);
|
|
2410
|
+
expect(s, "punct", ",", "add the pipeline literal as the second argument", name);
|
|
2411
|
+
s.lines[name] = nameTok.line;
|
|
2412
|
+
const fields = {};
|
|
2413
|
+
const open = peek(s);
|
|
2414
|
+
entries(s, name, false, (key, tok) => {
|
|
2415
|
+
const path = join$2(name, key);
|
|
2416
|
+
if (key === "id" || key === "label") fields[key] = str(s, path);
|
|
2417
|
+
else if (key === "displayOrder") fields.displayOrder = num(s, path);
|
|
2418
|
+
else if (key === "stages") {
|
|
2419
|
+
const stages = [];
|
|
2420
|
+
entries(s, path, true, (k, _t, scs) => {
|
|
2421
|
+
stages.push({
|
|
2422
|
+
key: k,
|
|
2423
|
+
...stage(s, join$2(path, k)),
|
|
2424
|
+
comments: texts(scs)
|
|
2425
|
+
});
|
|
2426
|
+
});
|
|
2427
|
+
fields.stages = stages;
|
|
2428
|
+
} else fail$1(s, "E_NOT_DATA", tok, `unknown field '${key}'`, "use one of id, label, displayOrder, stages", name);
|
|
2429
|
+
});
|
|
2430
|
+
for (const k of [
|
|
2431
|
+
"id",
|
|
2432
|
+
"label",
|
|
2433
|
+
"displayOrder",
|
|
2434
|
+
"stages"
|
|
2435
|
+
]) if (fields[k] === void 0) fail$1(s, "E_NOT_DATA", open, `missing field '${k}'`, `add ${k}`, name);
|
|
2436
|
+
skip(s, ",");
|
|
2437
|
+
expect(s, "punct", ")", void 0, name);
|
|
2438
|
+
skip(s, ";");
|
|
2439
|
+
return {
|
|
2440
|
+
...fields,
|
|
2441
|
+
name,
|
|
2442
|
+
object,
|
|
2443
|
+
comments: texts(cs)
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2130
2446
|
function parseConfig(s, imports) {
|
|
2131
2447
|
const fix = "write export default defineConfig({...})";
|
|
2132
2448
|
expect(s, "ident", "export", fix);
|
|
@@ -2209,6 +2525,10 @@ function inDir(at, file) {
|
|
|
2209
2525
|
function objectPath(at, object) {
|
|
2210
2526
|
return `${at.dir}/objects/${object}.ts`;
|
|
2211
2527
|
}
|
|
2528
|
+
/** The file pull writes an object's new pipelines into: `<dir>/pipelines/<object>.ts`. */
|
|
2529
|
+
function pipelinePath(at, object) {
|
|
2530
|
+
return `${at.dir}/pipelines/${object}.ts`;
|
|
2531
|
+
}
|
|
2212
2532
|
/** An object file's path from the barrel, without the extension, such as `./objects/companies`. */
|
|
2213
2533
|
function barrelPath(at, file) {
|
|
2214
2534
|
return `./${file.slice(at.dir.length + 1, -3)}`;
|
|
@@ -2218,8 +2538,9 @@ const TRAILING_SEPARATORS = /[\\/]+$/;
|
|
|
2218
2538
|
const SEPARATOR = /[\\/]/;
|
|
2219
2539
|
/**
|
|
2220
2540
|
* Builds the IR from a map of relative path to text. Reads kalup.config.ts, then in the folder of object files
|
|
2221
|
-
* (<dir>/) removed.ts, every <dir>/** /*.ts except index.ts
|
|
2222
|
-
* the resources the lock lists. Throws an IssueError, with every
|
|
2541
|
+
* (<dir>/) removed.ts, every <dir>/** /*.ts except index.ts (those under <dir>/pipelines/ as pipeline files), and
|
|
2542
|
+
* blueprints.lock.json, whose provenance it merges into the resources the lock lists. Throws an IssueError, with every
|
|
2543
|
+
* issue found, when the files cannot yield one IR.
|
|
2223
2544
|
*/
|
|
2224
2545
|
function loadFiles(files, options = {}) {
|
|
2225
2546
|
const issues = [];
|
|
@@ -2342,19 +2663,22 @@ function readObjectFiles(files, at, issues) {
|
|
|
2342
2663
|
const out = [];
|
|
2343
2664
|
for (const file of Object.keys(files).sort()) {
|
|
2344
2665
|
if (!inDir(at, file) || file === at.barrel || file === at.removed) continue;
|
|
2345
|
-
const
|
|
2346
|
-
if (later) {
|
|
2347
|
-
issues.push(unsupported$1(at, file, `this version does not read ${later} yet`));
|
|
2348
|
-
continue;
|
|
2349
|
-
}
|
|
2666
|
+
const pipelines = inPipelines(at, file);
|
|
2350
2667
|
try {
|
|
2351
|
-
const result = read(files[file] ?? "", file);
|
|
2352
|
-
if (result.kind === "object")
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2668
|
+
const result = read(files[file] ?? "", file, pipelines ? "pipeline" : void 0);
|
|
2669
|
+
if (result.kind === "object" || result.kind === "pipeline") {
|
|
2670
|
+
if (result.kind === "pipeline" && !pipelines) {
|
|
2671
|
+
const fix = `move it to ${at.dir}/pipelines/`;
|
|
2672
|
+
issues.push(unsupported$1(at, file, `a definePipeline file belongs under ${at.dir}/pipelines/`, fix));
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
out.push({
|
|
2676
|
+
file,
|
|
2677
|
+
kind: result.kind,
|
|
2678
|
+
data: result.data,
|
|
2679
|
+
lines: result.lines
|
|
2680
|
+
});
|
|
2681
|
+
} else if (result.kind === "config") issues.push(unsupported$1(at, file, `a defineConfig file under ${at.dir}/ is not an object file`));
|
|
2358
2682
|
else {
|
|
2359
2683
|
const fix = `move its entries to ${at.removed}`;
|
|
2360
2684
|
issues.push(unsupported$1(at, file, `a defineRemoved file belongs at ${at.removed}`, fix));
|
|
@@ -2366,9 +2690,9 @@ function readObjectFiles(files, at, issues) {
|
|
|
2366
2690
|
}
|
|
2367
2691
|
return out;
|
|
2368
2692
|
}
|
|
2369
|
-
/**
|
|
2370
|
-
function
|
|
2371
|
-
return file.startsWith(`${at.dir}/pipelines/`)
|
|
2693
|
+
/** Whether a file lies in the pipelines folder, `<dir>/pipelines/`, which holds definePipeline files only. */
|
|
2694
|
+
function inPipelines(at, file) {
|
|
2695
|
+
return file.startsWith(`${at.dir}/pipelines/`);
|
|
2372
2696
|
}
|
|
2373
2697
|
function unsupported$1(at, file, message, fix = `move ${file} out of ${at.dir}/ until a release reads it`) {
|
|
2374
2698
|
return {
|
|
@@ -2397,26 +2721,36 @@ function flatten(objectFiles, issues) {
|
|
|
2397
2721
|
resources[address] = resource;
|
|
2398
2722
|
sources[address] = source;
|
|
2399
2723
|
};
|
|
2400
|
-
for (const { file, data, lines } of objectFiles)
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
if (resource) add(`object:${e.object}`, resource, at(e.name));
|
|
2724
|
+
for (const { file, kind, data, lines } of objectFiles) {
|
|
2725
|
+
if (kind === "pipeline") {
|
|
2726
|
+
for (const e of data.exports) flattenPipeline(e, (configPath) => ({
|
|
2727
|
+
file,
|
|
2728
|
+
line: lines[configPath] ?? 1,
|
|
2729
|
+
configPath
|
|
2730
|
+
}), add, issues);
|
|
2731
|
+
continue;
|
|
2409
2732
|
}
|
|
2410
|
-
for (const
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
};
|
|
2416
|
-
|
|
2733
|
+
for (const e of data.exports) {
|
|
2734
|
+
const at = (configPath) => ({
|
|
2735
|
+
file,
|
|
2736
|
+
line: lines[configPath] ?? 1,
|
|
2737
|
+
configPath
|
|
2738
|
+
});
|
|
2739
|
+
if (e.builder === "defineCustomObject") {
|
|
2740
|
+
const resource = objectResource$1(e, at(e.name), issues);
|
|
2741
|
+
if (resource) add(`object:${e.object}`, resource, at(e.name));
|
|
2742
|
+
}
|
|
2743
|
+
for (const group of e.groups) {
|
|
2744
|
+
const resource = {
|
|
2745
|
+
type: "group",
|
|
2746
|
+
managed: true,
|
|
2747
|
+
definition: { label: group.label }
|
|
2748
|
+
};
|
|
2749
|
+
add(`group:${e.object}/${group.name}`, resource, at(`${e.name}.groups.${group.name}`));
|
|
2750
|
+
}
|
|
2751
|
+
flattenProperties(e, at, add, issues);
|
|
2752
|
+
optionsStated.push(...statedOptions(e, (address) => sources[address]?.file === file));
|
|
2417
2753
|
}
|
|
2418
|
-
flattenProperties(e, at, add, issues);
|
|
2419
|
-
optionsStated.push(...statedOptions(e, (address) => sources[address]?.file === file));
|
|
2420
2754
|
}
|
|
2421
2755
|
return {
|
|
2422
2756
|
resources,
|
|
@@ -2424,6 +2758,51 @@ function flatten(objectFiles, issues) {
|
|
|
2424
2758
|
optionsStated: [...new Set(optionsStated)]
|
|
2425
2759
|
};
|
|
2426
2760
|
}
|
|
2761
|
+
/** The address of a pipeline, and of one of its stages. */
|
|
2762
|
+
function pipelineAddress(object, id) {
|
|
2763
|
+
return `pipeline:${object}/${id}`;
|
|
2764
|
+
}
|
|
2765
|
+
function stageAddress(object, pipeline, stage) {
|
|
2766
|
+
return `stage:${object}/${pipeline}/${stage}`;
|
|
2767
|
+
}
|
|
2768
|
+
function flattenPipeline(e, at, add, issues) {
|
|
2769
|
+
const unaddressable = (id, source) => {
|
|
2770
|
+
const held = isAddress(pipelineAddress(e.object, id)) && !id.includes("/") && !e.object.includes("/");
|
|
2771
|
+
if (!held) issues.push({
|
|
2772
|
+
code: "E_PIPELINE_ID",
|
|
2773
|
+
message: `the ID '${id}' holds whitespace or a slash, so no address can hold it`,
|
|
2774
|
+
...source,
|
|
2775
|
+
fix: "use an ID without whitespace or slashes"
|
|
2776
|
+
});
|
|
2777
|
+
return !held;
|
|
2778
|
+
};
|
|
2779
|
+
if (unaddressable(e.id, at(`${e.name}.id`))) return;
|
|
2780
|
+
add(pipelineAddress(e.object, e.id), {
|
|
2781
|
+
type: "pipeline",
|
|
2782
|
+
managed: true,
|
|
2783
|
+
definition: {
|
|
2784
|
+
label: e.label,
|
|
2785
|
+
displayOrder: e.displayOrder,
|
|
2786
|
+
stages: e.stages.map((s) => s.id)
|
|
2787
|
+
},
|
|
2788
|
+
binding: { export: e.name }
|
|
2789
|
+
}, at(e.name));
|
|
2790
|
+
for (const st of e.stages) {
|
|
2791
|
+
const stage = stageAddress(e.object, e.id, st.id);
|
|
2792
|
+
if (unaddressable(st.id, at(`${e.name}.stages.${st.key}.id`))) continue;
|
|
2793
|
+
add(stage, {
|
|
2794
|
+
type: "stage",
|
|
2795
|
+
managed: true,
|
|
2796
|
+
definition: compact$5({
|
|
2797
|
+
label: st.label,
|
|
2798
|
+
probability: st.probability,
|
|
2799
|
+
ticketState: st.ticketState,
|
|
2800
|
+
state: st.state
|
|
2801
|
+
}),
|
|
2802
|
+
binding: { key: st.key }
|
|
2803
|
+
}, at(`${e.name}.stages.${st.key}`));
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2427
2806
|
function statedOptions(e, mine) {
|
|
2428
2807
|
return e.properties.filter((p) => p.definition?.lifecycle?.options !== void 0).map((p) => `property:${e.object}/${p.name}`).filter(mine);
|
|
2429
2808
|
}
|
|
@@ -2599,7 +2978,10 @@ function byCodeUnit(a, b) {
|
|
|
2599
2978
|
function basename$1(root) {
|
|
2600
2979
|
return root.replace(TRAILING_SEPARATORS, "").split(SEPARATOR).pop() ?? "";
|
|
2601
2980
|
}
|
|
2602
|
-
/**
|
|
2981
|
+
/**
|
|
2982
|
+
* The definition fields a target may override, by resource type. A stage takes the metadata field of its pipeline's
|
|
2983
|
+
* object only, which validate checks.
|
|
2984
|
+
*/
|
|
2603
2985
|
const OVERRIDABLE = {
|
|
2604
2986
|
property: [
|
|
2605
2987
|
"label",
|
|
@@ -2616,7 +2998,14 @@ const OVERRIDABLE = {
|
|
|
2616
2998
|
"textDisplayHint",
|
|
2617
2999
|
"calculationFormula"
|
|
2618
3000
|
],
|
|
2619
|
-
group: ["label"]
|
|
3001
|
+
group: ["label"],
|
|
3002
|
+
pipeline: ["label", "displayOrder"],
|
|
3003
|
+
stage: [
|
|
3004
|
+
"label",
|
|
3005
|
+
"probability",
|
|
3006
|
+
"ticketState",
|
|
3007
|
+
"state"
|
|
3008
|
+
]
|
|
2620
3009
|
};
|
|
2621
3010
|
/** The lifecycle fields a target may override on a property, each on its own. */
|
|
2622
3011
|
const OVERRIDABLE_LIFECYCLE = [
|
|
@@ -2646,10 +3035,18 @@ function effectiveResources(ir, target) {
|
|
|
2646
3035
|
*/
|
|
2647
3036
|
function withDefinition(address, resource, override) {
|
|
2648
3037
|
const { type, path } = parseAddress(address);
|
|
2649
|
-
if (!(resource.managed && (
|
|
2650
|
-
const
|
|
2651
|
-
const
|
|
2652
|
-
|
|
3038
|
+
if (!(resource.managed && Object.hasOwn(OVERRIDABLE, type))) return resource;
|
|
3039
|
+
const fields = OVERRIDABLE[type];
|
|
3040
|
+
const stated = pick$3(override, fields);
|
|
3041
|
+
if (type === "pipeline" || type === "stage") return {
|
|
3042
|
+
...resource,
|
|
3043
|
+
definition: {
|
|
3044
|
+
...resource.definition,
|
|
3045
|
+
...stated
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
const object = path.slice(0, path.indexOf("/"));
|
|
3049
|
+
const merged = {
|
|
2653
3050
|
...resource.definition,
|
|
2654
3051
|
...definitionToIR(object, resource.binding?.codec ?? "string", stated)
|
|
2655
3052
|
};
|
|
@@ -2673,10 +3070,26 @@ function own$5(record, key) {
|
|
|
2673
3070
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
2674
3071
|
}
|
|
2675
3072
|
const CONFIG$2$1 = "kalup.config.ts";
|
|
2676
|
-
/** The resource types a tombstone may name in this version. */
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3073
|
+
/** The resource types a tombstone may name in this version, and the shape of each one's path. */
|
|
3074
|
+
/** The address types a tombstone may name, each with its form and the shape of its path. */
|
|
3075
|
+
const REMOVABLE = {
|
|
3076
|
+
property: {
|
|
3077
|
+
form: "property:<object>/<name>",
|
|
3078
|
+
path: /^[^\s/]+\/[^\s/]+$/
|
|
3079
|
+
},
|
|
3080
|
+
group: {
|
|
3081
|
+
form: "group:<object>/<name>",
|
|
3082
|
+
path: /^[^\s/]+\/[^\s/]+$/
|
|
3083
|
+
},
|
|
3084
|
+
pipeline: {
|
|
3085
|
+
form: "pipeline:<object>/<id>",
|
|
3086
|
+
path: /^[^\s/]+\/[^\s/]+$/
|
|
3087
|
+
},
|
|
3088
|
+
stage: {
|
|
3089
|
+
form: "stage:<object>/<pipeline>/<stage>",
|
|
3090
|
+
path: /^[^\s/]+\/[^\s/]+\/[^\s/]+$/
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
2680
3093
|
/** The definition fields a property can own, the names `lifecycle.ignoreChanges` may use. */
|
|
2681
3094
|
const DEFINITION_FIELDS = PROPERTY_FIELDS.filter((field) => field !== "type" && field !== "externalOptions" && field !== "referencedObjectType");
|
|
2682
3095
|
function validate$2(loaded, options = {}) {
|
|
@@ -2714,12 +3127,163 @@ function validate$2(loaded, options = {}) {
|
|
|
2714
3127
|
warnings
|
|
2715
3128
|
});
|
|
2716
3129
|
checkTombstones(loaded, issues);
|
|
3130
|
+
checkPipelines(loaded, issues);
|
|
2717
3131
|
return {
|
|
2718
3132
|
issues,
|
|
2719
3133
|
warnings
|
|
2720
3134
|
};
|
|
2721
3135
|
}
|
|
2722
3136
|
/**
|
|
3137
|
+
* The rules HubSpot keeps for pipelines and stages, observed 2026-10-01 and 2026-10-05: IDs of a length it stores and
|
|
3138
|
+
* not held elsewhere (a pipeline ID across the portal, a stage ID across one object's pipelines), at least one stage
|
|
3139
|
+
* and on tickets one closed one, labels unique ignoring case (a stage's also ignoring spaces around it), and each
|
|
3140
|
+
* stage's metadata field the one its object takes.
|
|
3141
|
+
*/
|
|
3142
|
+
function checkPipelines(loaded, issues) {
|
|
3143
|
+
const { ir, sources } = loaded;
|
|
3144
|
+
const at = (address, suffix = "") => {
|
|
3145
|
+
const source = sources[address] ?? {
|
|
3146
|
+
file: "",
|
|
3147
|
+
line: 0,
|
|
3148
|
+
configPath: address
|
|
3149
|
+
};
|
|
3150
|
+
return {
|
|
3151
|
+
file: source.file,
|
|
3152
|
+
line: source.line,
|
|
3153
|
+
configPath: source.configPath + suffix
|
|
3154
|
+
};
|
|
3155
|
+
};
|
|
3156
|
+
const pipelineIds = /* @__PURE__ */ new Map();
|
|
3157
|
+
const pipelineLabels = /* @__PURE__ */ new Map();
|
|
3158
|
+
const stageIds = /* @__PURE__ */ new Map();
|
|
3159
|
+
for (const [address, resource] of Object.entries(ir.resources)) {
|
|
3160
|
+
if (resource.type !== "pipeline") continue;
|
|
3161
|
+
const [object = "", id = ""] = parseAddress(address).path.split("/");
|
|
3162
|
+
const d = resource.definition ?? {};
|
|
3163
|
+
if (!hasPipelines(object, !STANDARD_OBJECTS.has(object))) issues.push({
|
|
3164
|
+
code: "E_PIPELINE_FIELD",
|
|
3165
|
+
message: `${address} is a pipeline of ${object}, which has no pipelines`,
|
|
3166
|
+
...at(address),
|
|
3167
|
+
fix: "name an object that has pipelines, such as deals, tickets or a custom object"
|
|
3168
|
+
});
|
|
3169
|
+
const first = pipelineIds.get(id);
|
|
3170
|
+
if (first === void 0) pipelineIds.set(id, address);
|
|
3171
|
+
else issues.push({
|
|
3172
|
+
code: "E_PIPELINE_ID",
|
|
3173
|
+
message: `${address} has the ID of ${first}, and HubSpot keeps pipeline IDs unique across objects`,
|
|
3174
|
+
...at(address, ".id"),
|
|
3175
|
+
fix: "give one of the two another ID"
|
|
3176
|
+
});
|
|
3177
|
+
if (id.length > 36) issues.push(tooLong(address, 36, at(address, ".id")));
|
|
3178
|
+
const label = String(d.label).toLowerCase();
|
|
3179
|
+
const same = pipelineLabels.get(`${object}/${label}`);
|
|
3180
|
+
if (same === void 0) pipelineLabels.set(`${object}/${label}`, address);
|
|
3181
|
+
else issues.push({
|
|
3182
|
+
code: "E_DUPLICATE_LABEL",
|
|
3183
|
+
message: `${same} and ${address} share the label '${d.label}', ignoring case`,
|
|
3184
|
+
...at(address, ".label"),
|
|
3185
|
+
fix: "give one of the two another label"
|
|
3186
|
+
});
|
|
3187
|
+
const order = d.displayOrder;
|
|
3188
|
+
if (!(Number.isInteger(order) && order >= 0)) issues.push({
|
|
3189
|
+
code: "E_PIPELINE_FIELD",
|
|
3190
|
+
message: `displayOrder ${order} of ${address} is not an integer from 0 up`,
|
|
3191
|
+
...at(address, ".displayOrder"),
|
|
3192
|
+
fix: "use 0 or more: HubSpot lists pipelines lowest first"
|
|
3193
|
+
});
|
|
3194
|
+
checkStages(ir, address, object, stageIds, at, issues);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
function checkStages(ir, pipeline, object, stageIds, at, issues) {
|
|
3198
|
+
const ids = (ir.resources[pipeline]?.definition?.stages ?? []).filter((id) => Object.hasOwn(ir.resources, `stage:${pipeline.slice(9)}/${id}`));
|
|
3199
|
+
if (ids.length === 0) issues.push({
|
|
3200
|
+
code: "E_PIPELINE_STAGES",
|
|
3201
|
+
message: `${pipeline} has no stage, and HubSpot needs one`,
|
|
3202
|
+
...at(pipeline, ".stages"),
|
|
3203
|
+
fix: "add a stage, or run kalup rm on the pipeline"
|
|
3204
|
+
});
|
|
3205
|
+
const field = stageField(object, !STANDARD_OBJECTS.has(object));
|
|
3206
|
+
const labels = /* @__PURE__ */ new Map();
|
|
3207
|
+
let closed = false;
|
|
3208
|
+
for (const id of ids) {
|
|
3209
|
+
const address = `stage:${pipeline.slice(9)}/${id}`;
|
|
3210
|
+
const d = ir.resources[address]?.definition ?? {};
|
|
3211
|
+
const first = stageIds.get(`${object}/${id}`);
|
|
3212
|
+
if (first === void 0) stageIds.set(`${object}/${id}`, address);
|
|
3213
|
+
else issues.push({
|
|
3214
|
+
code: "E_PIPELINE_ID",
|
|
3215
|
+
message: `${address} has the ID of ${first}, and HubSpot keeps stage IDs unique across an object's pipelines`,
|
|
3216
|
+
...at(address, ".id"),
|
|
3217
|
+
fix: "give one of the two another ID, such as the pipeline ID followed by the stage"
|
|
3218
|
+
});
|
|
3219
|
+
if (id.length > 100) issues.push(tooLong(address, 100, at(address, ".id")));
|
|
3220
|
+
const label = String(d.label).trim().toLowerCase();
|
|
3221
|
+
const same = labels.get(label);
|
|
3222
|
+
if (same === void 0) labels.set(label, address);
|
|
3223
|
+
else issues.push({
|
|
3224
|
+
code: "E_DUPLICATE_LABEL",
|
|
3225
|
+
message: `stages ${same} and ${address} share the label '${label}', ignoring case and spaces around it`,
|
|
3226
|
+
...at(address, ".label"),
|
|
3227
|
+
fix: "give one of the two another label"
|
|
3228
|
+
});
|
|
3229
|
+
closed ||= d.ticketState === "CLOSED";
|
|
3230
|
+
for (const rule of stageRules(object, field, d)) issues.push({
|
|
3231
|
+
code: "E_PIPELINE_FIELD",
|
|
3232
|
+
message: rule.message,
|
|
3233
|
+
...at(address, rule.field),
|
|
3234
|
+
fix: rule.fix
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
if (field === "ticketState" && ids.length > 0 && !closed) issues.push({
|
|
3238
|
+
code: "E_PIPELINE_STAGES",
|
|
3239
|
+
message: `${pipeline} has no stage with ticketState 'CLOSED', and HubSpot needs one`,
|
|
3240
|
+
...at(pipeline, ".stages"),
|
|
3241
|
+
fix: "mark the stage tickets end in ticketState: 'CLOSED'"
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
const FIELD_OBJECTS = {
|
|
3245
|
+
probability: "deal stages",
|
|
3246
|
+
ticketState: "ticket stages",
|
|
3247
|
+
state: "custom object stages"
|
|
3248
|
+
};
|
|
3249
|
+
/**
|
|
3250
|
+
* What is wrong with one stage's metadata, as config or an override states it: a field its object does not take, a deal
|
|
3251
|
+
* stage without a probability (unless `override`, which states only what differs), a probability outside 0..1. Each
|
|
3252
|
+
* entry's `field` is the suffix it points at.
|
|
3253
|
+
*/
|
|
3254
|
+
function stageRules(object, field, d, override = false) {
|
|
3255
|
+
const out = [];
|
|
3256
|
+
for (const name of STAGE_FIELDS) {
|
|
3257
|
+
if (d[name] === void 0 || name === field) continue;
|
|
3258
|
+
const takes = field === void 0 ? `a stage of ${object} takes none: Kalup reads and compares its pipelines and does not write them` : `a stage of ${object} takes ${field}`;
|
|
3259
|
+
out.push({
|
|
3260
|
+
field: `.${name}`,
|
|
3261
|
+
message: `${name} is for ${FIELD_OBJECTS[name]}; ${takes}`,
|
|
3262
|
+
fix: field === void 0 ? `remove ${name}` : `replace ${name} with ${field}`
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
const p = d.probability;
|
|
3266
|
+
if (field === "probability" && p === void 0 && !override) out.push({
|
|
3267
|
+
field: "",
|
|
3268
|
+
message: "a deal stage needs a probability, and HubSpot refuses one without",
|
|
3269
|
+
fix: "add probability, from 0 to 1: 0 and 1 make the stage closed"
|
|
3270
|
+
});
|
|
3271
|
+
else if (field === "probability" && p !== void 0 && !(typeof p === "number" && p >= 0 && p <= 1)) out.push({
|
|
3272
|
+
field: ".probability",
|
|
3273
|
+
message: `probability ${p} is not from 0 to 1`,
|
|
3274
|
+
fix: "use a number from 0 to 1"
|
|
3275
|
+
});
|
|
3276
|
+
return out;
|
|
3277
|
+
}
|
|
3278
|
+
function tooLong(address, max, at) {
|
|
3279
|
+
return {
|
|
3280
|
+
code: "E_PIPELINE_ID",
|
|
3281
|
+
message: `the ID of ${address} is longer than ${max} characters, which HubSpot answers with an error and does not store`,
|
|
3282
|
+
...at,
|
|
3283
|
+
fix: `use an ID of at most ${max} characters`
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
2723
3287
|
* The settings under objects and targets.<t>.objects: a name include and exclude both list, a target object objects
|
|
2724
3288
|
* does not declare, and a target mode that overrides an object's mode the target says nothing more about.
|
|
2725
3289
|
*/
|
|
@@ -2753,6 +3317,12 @@ function checkScopes(loaded, { issues, warnings }) {
|
|
|
2753
3317
|
}
|
|
2754
3318
|
function checkExcludes(config, at, issues) {
|
|
2755
3319
|
for (const [object, scope] of Object.entries(config.objects)) {
|
|
3320
|
+
if (scope.pipelines === true && !hasPipelines(object, !STANDARD_OBJECTS.has(object))) issues.push({
|
|
3321
|
+
code: "E_SETTING_VALUE",
|
|
3322
|
+
message: `objects.${object}.pipelines is true, and ${object} has no pipelines`,
|
|
3323
|
+
...at(`objects.${object}.pipelines`),
|
|
3324
|
+
fix: `remove pipelines from objects.${object}`
|
|
3325
|
+
});
|
|
2756
3326
|
const both = (scope.include ?? []).filter((name) => (scope.exclude ?? []).includes(name));
|
|
2757
3327
|
if (both.length > 0) issues.push({
|
|
2758
3328
|
code: "E_SETTING_VALUE",
|
|
@@ -2884,6 +3454,7 @@ function checkTargets(loaded, requested, { issues, warnings }) {
|
|
|
2884
3454
|
issues,
|
|
2885
3455
|
warnings
|
|
2886
3456
|
});
|
|
3457
|
+
checkTargetPipelines(loaded, name, overrides, at, issues);
|
|
2887
3458
|
}
|
|
2888
3459
|
const declared = Object.keys(config.targets);
|
|
2889
3460
|
if (config.defaultTarget !== void 0 && !Object.hasOwn(config.targets, config.defaultTarget)) issues.push({
|
|
@@ -2900,7 +3471,7 @@ function checkTargets(loaded, requested, { issues, warnings }) {
|
|
|
2900
3471
|
});
|
|
2901
3472
|
}
|
|
2902
3473
|
/**
|
|
2903
|
-
* Every key of removed.ts is a property
|
|
3474
|
+
* Every key of removed.ts is a property, group, pipeline or stage address, and none is also a resource in config:
|
|
2904
3475
|
* removing a resource takes it out of config.
|
|
2905
3476
|
*/
|
|
2906
3477
|
function checkTombstones(loaded, issues) {
|
|
@@ -2911,7 +3482,7 @@ function checkTombstones(loaded, issues) {
|
|
|
2911
3482
|
...removedLines[key] === void 0 ? {} : { line: removedLines[key] },
|
|
2912
3483
|
configPath: key
|
|
2913
3484
|
};
|
|
2914
|
-
const fix = "write the address of a property or
|
|
3485
|
+
const fix = "write the address of a property, group, pipeline or stage, such as 'property:companies/legacy_score'";
|
|
2915
3486
|
if (!isAddress(key)) {
|
|
2916
3487
|
issues.push({
|
|
2917
3488
|
code: "E_TOMBSTONE_ADDRESS",
|
|
@@ -2922,14 +3493,15 @@ function checkTombstones(loaded, issues) {
|
|
|
2922
3493
|
continue;
|
|
2923
3494
|
}
|
|
2924
3495
|
const { type, path } = parseAddress(key);
|
|
2925
|
-
|
|
3496
|
+
const shape = Object.hasOwn(REMOVABLE, type) ? REMOVABLE[type] : void 0;
|
|
3497
|
+
if (!shape) issues.push({
|
|
2926
3498
|
code: "E_TOMBSTONE_ADDRESS",
|
|
2927
|
-
message: `cannot remove ${key}: this version removes properties and
|
|
3499
|
+
message: `cannot remove ${key}: this version removes properties, groups, pipelines and stages only`,
|
|
2928
3500
|
...at,
|
|
2929
3501
|
fix: `remove ${key} from ${removed}`
|
|
2930
3502
|
});
|
|
2931
|
-
else if (!
|
|
2932
|
-
const message = `'${key}' is not of the form ${
|
|
3503
|
+
else if (!shape.path.test(path)) {
|
|
3504
|
+
const message = `'${key}' is not of the form ${shape.form}`;
|
|
2933
3505
|
issues.push({
|
|
2934
3506
|
code: "E_TOMBSTONE_ADDRESS",
|
|
2935
3507
|
message,
|
|
@@ -3005,8 +3577,15 @@ function checkDefinitions(ir, target, overrides, at, { issues, warnings }) {
|
|
|
3005
3577
|
report("", refused, `remove the definition override for ${address} under targets.${target}.overrides`);
|
|
3006
3578
|
continue;
|
|
3007
3579
|
}
|
|
3008
|
-
const type = parseAddress(address)
|
|
3580
|
+
const { type, path } = parseAddress(address);
|
|
3009
3581
|
checkFields(type, d, report);
|
|
3582
|
+
const order = d.displayOrder;
|
|
3583
|
+
if (type === "pipeline" && order !== void 0 && !(Number.isInteger(order) && order >= 0)) report(".displayOrder", `displayOrder ${order} is not an integer from 0 up`, "use 0 or more");
|
|
3584
|
+
if (type === "stage") {
|
|
3585
|
+
const object = path.slice(0, path.indexOf("/"));
|
|
3586
|
+
const field = stageField(object, !STANDARD_OBJECTS.has(object));
|
|
3587
|
+
for (const rule of stageRules(object, field, d, true)) report(rule.field, rule.message, rule.fix);
|
|
3588
|
+
}
|
|
3010
3589
|
if (type === "property") {
|
|
3011
3590
|
checkEffective(ir, address, resource, d, report);
|
|
3012
3591
|
checkEffectiveOptions(address, resource, d, report);
|
|
@@ -3014,6 +3593,41 @@ function checkDefinitions(ir, target, overrides, at, { issues, warnings }) {
|
|
|
3014
3593
|
}
|
|
3015
3594
|
}
|
|
3016
3595
|
}
|
|
3596
|
+
/**
|
|
3597
|
+
* The pipeline rules on the resources one target's definition overrides leave: a label two pipelines or two stages
|
|
3598
|
+
* would share, a ticket pipeline left with no closed stage. Only what the shared files do not
|
|
3599
|
+
* break already is reported, as E_OVERRIDE_DEFINITION at the first override the message names, else at an override of
|
|
3600
|
+
* a stage of the pipeline it names.
|
|
3601
|
+
*/
|
|
3602
|
+
function checkTargetPipelines(loaded, target, overrides, at, issues) {
|
|
3603
|
+
const touched = Object.keys(overrides).filter((address) => (address.startsWith("pipeline:") || address.startsWith("stage:")) && Object.hasOwn(loaded.ir.resources, address) && overrides[address]?.definition !== void 0);
|
|
3604
|
+
if (touched.length === 0) return;
|
|
3605
|
+
const shared = [];
|
|
3606
|
+
checkPipelines(loaded, shared);
|
|
3607
|
+
const known = new Set(shared.map((issue) => issue.message));
|
|
3608
|
+
const found = [];
|
|
3609
|
+
checkPipelines({
|
|
3610
|
+
...loaded,
|
|
3611
|
+
ir: {
|
|
3612
|
+
...loaded.ir,
|
|
3613
|
+
resources: effectiveResources(loaded.ir, target)
|
|
3614
|
+
}
|
|
3615
|
+
}, found);
|
|
3616
|
+
const spanning = found.filter((i) => SPANNING.has(i.code) && !known.has(i.message));
|
|
3617
|
+
for (const issue of spanning) {
|
|
3618
|
+
const words = issue.message.split(WORD_BREAK);
|
|
3619
|
+
const pipelineOf = (a) => a.startsWith("stage:") ? `pipeline:${a.slice(6, a.lastIndexOf("/"))}` : a;
|
|
3620
|
+
const address = words.find((word) => touched.includes(word)) ?? touched.find((a) => words.includes(pipelineOf(a))) ?? touched[0];
|
|
3621
|
+
issues.push({
|
|
3622
|
+
code: "E_OVERRIDE_DEFINITION",
|
|
3623
|
+
message: `on target ${target}, ${issue.message}`,
|
|
3624
|
+
...located$1(at, `${address}.definition`)(),
|
|
3625
|
+
...issue.fix === void 0 ? {} : { fix: issue.fix }
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
const SPANNING = /* @__PURE__ */ new Set(["E_DUPLICATE_LABEL", "E_PIPELINE_STAGES"]);
|
|
3630
|
+
const WORD_BREAK = /[\s,']+/;
|
|
3017
3631
|
function located$1(at, path) {
|
|
3018
3632
|
return (suffix = "") => {
|
|
3019
3633
|
const full = `${path}${suffix}`;
|
|
@@ -3038,14 +3652,16 @@ function checkFields(type, d, report) {
|
|
|
3038
3652
|
function refusal$1(address, resource) {
|
|
3039
3653
|
const { type } = parseAddress(address);
|
|
3040
3654
|
if (type === "object") return "a custom object schema cannot take a definition override in this release";
|
|
3041
|
-
if (
|
|
3655
|
+
if (!Object.hasOwn(OVERRIDABLE, type)) return "only a property, a group, a pipeline or a stage can take a definition override";
|
|
3042
3656
|
if (resource.managed) return;
|
|
3043
3657
|
const d = resource.definition ?? {};
|
|
3044
3658
|
return d.label !== void 0 && d.group !== void 0 && d.fieldType !== void 0 ? "it is .managed(false), so no target owns its definition" : "it is a reference (its shared definition has no label, group and fieldType), so nothing on it can differ per target";
|
|
3045
3659
|
}
|
|
3046
3660
|
function notOverridable(type, field) {
|
|
3047
3661
|
if (field === "hasUniqueValue" || field === "dataSensitivity") return `${field} is fixed when HubSpot creates the property, so it cannot differ per target`;
|
|
3048
|
-
|
|
3662
|
+
if (type === "group") return `a group override may set label only, not ${field}`;
|
|
3663
|
+
if (type === "pipeline") return `a pipeline override may set label and displayOrder only, not ${field}`;
|
|
3664
|
+
return type === "stage" ? `a stage override may set label and its metadata only, not ${field}` : `${field} cannot differ per target`;
|
|
3049
3665
|
}
|
|
3050
3666
|
function checkEffective(ir, address, resource, d, report) {
|
|
3051
3667
|
const codec = resource.binding?.codec;
|
|
@@ -3221,7 +3837,7 @@ function walkUnresolved(value, visit) {
|
|
|
3221
3837
|
}
|
|
3222
3838
|
const SCHEMA = blueprint_1_schema_default;
|
|
3223
3839
|
/** `<group|property>:<object>/<name>`. */
|
|
3224
|
-
const ON_OBJECT
|
|
3840
|
+
const ON_OBJECT = /^(group|property):([^\s/]+)\/([^\s/]+)$/;
|
|
3225
3841
|
/** Object keys, group names and property names: lowercase, digits and underscores, a letter first. */
|
|
3226
3842
|
const NAME = /^[a-z][a-z0-9_]*$/;
|
|
3227
3843
|
/** The codec each HubSpot type implies when a blueprint's binding names none. */
|
|
@@ -3262,7 +3878,7 @@ function rules(blueprint) {
|
|
|
3262
3878
|
const report = (configPath, message) => issues.push(issue(configPath, message));
|
|
3263
3879
|
for (const [address, resource] of Object.entries(blueprint.resources)) {
|
|
3264
3880
|
const at = `resources.${address}`;
|
|
3265
|
-
const parts = ON_OBJECT
|
|
3881
|
+
const parts = ON_OBJECT.exec(address);
|
|
3266
3882
|
if (!parts) {
|
|
3267
3883
|
report(at, `'${address}' is not of the form group:<object>/<name> or property:<object>/<name>`);
|
|
3268
3884
|
continue;
|
|
@@ -3287,7 +3903,7 @@ function checkName(report, at, type, name) {
|
|
|
3287
3903
|
function checkProperty(report, at, object, resource) {
|
|
3288
3904
|
const d = resource.definition;
|
|
3289
3905
|
const group = d.group.$ref;
|
|
3290
|
-
const parts = ON_OBJECT
|
|
3906
|
+
const parts = ON_OBJECT.exec(group);
|
|
3291
3907
|
if (!(parts && parts[1] === "group" && parts[2] === object)) report(`${at}.definition.group`, `the group '${group}' is not a group of ${object}, group:${object}/<name>`);
|
|
3292
3908
|
else if (!NAME.test(parts[3])) report(`${at}.definition.group`, `group name '${parts[3]}' is not lowercase letters, digits and underscores starting with a letter`);
|
|
3293
3909
|
const options = d.options ?? [];
|
|
@@ -3460,6 +4076,79 @@ const registry = {
|
|
|
3460
4076
|
}
|
|
3461
4077
|
}
|
|
3462
4078
|
},
|
|
4079
|
+
pipeline: {
|
|
4080
|
+
family: "crm.pipelines",
|
|
4081
|
+
version: "2026-09",
|
|
4082
|
+
status: "ga",
|
|
4083
|
+
expires: "2028-03",
|
|
4084
|
+
identity: "natural",
|
|
4085
|
+
auth: "account",
|
|
4086
|
+
delete: "permanent",
|
|
4087
|
+
scopes: {
|
|
4088
|
+
read: ["crm.schemas.{object}.read"],
|
|
4089
|
+
write: ["crm.schemas.{object}.write"]
|
|
4090
|
+
},
|
|
4091
|
+
tier: "any",
|
|
4092
|
+
limitKey: "pipelines",
|
|
4093
|
+
paths: {
|
|
4094
|
+
list: {
|
|
4095
|
+
method: "GET",
|
|
4096
|
+
path: "/crm/pipelines/2026-09/{objectType}",
|
|
4097
|
+
tag: "read"
|
|
4098
|
+
},
|
|
4099
|
+
read: {
|
|
4100
|
+
method: "GET",
|
|
4101
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}",
|
|
4102
|
+
tag: "read"
|
|
4103
|
+
},
|
|
4104
|
+
create: {
|
|
4105
|
+
method: "POST",
|
|
4106
|
+
path: "/crm/pipelines/2026-09/{objectType}",
|
|
4107
|
+
tag: "write"
|
|
4108
|
+
},
|
|
4109
|
+
update: {
|
|
4110
|
+
method: "PATCH",
|
|
4111
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}",
|
|
4112
|
+
tag: "write"
|
|
4113
|
+
},
|
|
4114
|
+
delete: {
|
|
4115
|
+
method: "DELETE",
|
|
4116
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}",
|
|
4117
|
+
tag: "write"
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
},
|
|
4121
|
+
stage: {
|
|
4122
|
+
family: "crm.pipelines",
|
|
4123
|
+
version: "2026-09",
|
|
4124
|
+
status: "ga",
|
|
4125
|
+
expires: "2028-03",
|
|
4126
|
+
identity: "natural",
|
|
4127
|
+
auth: "account",
|
|
4128
|
+
delete: "permanent",
|
|
4129
|
+
scopes: {
|
|
4130
|
+
read: ["crm.schemas.{object}.read"],
|
|
4131
|
+
write: ["crm.schemas.{object}.write"]
|
|
4132
|
+
},
|
|
4133
|
+
tier: "any",
|
|
4134
|
+
paths: {
|
|
4135
|
+
create: {
|
|
4136
|
+
method: "POST",
|
|
4137
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}/stages",
|
|
4138
|
+
tag: "write"
|
|
4139
|
+
},
|
|
4140
|
+
update: {
|
|
4141
|
+
method: "PATCH",
|
|
4142
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}/stages/{stageId}",
|
|
4143
|
+
tag: "write"
|
|
4144
|
+
},
|
|
4145
|
+
delete: {
|
|
4146
|
+
method: "DELETE",
|
|
4147
|
+
path: "/crm/pipelines/2026-09/{objectType}/{pipelineId}/stages/{stageId}",
|
|
4148
|
+
tag: "write"
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
},
|
|
3463
4152
|
accountInfo: {
|
|
3464
4153
|
family: "account-info",
|
|
3465
4154
|
version: "2026-09",
|
|
@@ -3496,6 +4185,11 @@ const registry = {
|
|
|
3496
4185
|
method: "GET",
|
|
3497
4186
|
path: "/crm/limits/2026-09/custom-object-types",
|
|
3498
4187
|
tag: "read"
|
|
4188
|
+
},
|
|
4189
|
+
pipelines: {
|
|
4190
|
+
method: "GET",
|
|
4191
|
+
path: "/crm/limits/2026-09/pipelines",
|
|
4192
|
+
tag: "read"
|
|
3499
4193
|
}
|
|
3500
4194
|
}
|
|
3501
4195
|
}
|
|
@@ -3507,7 +4201,9 @@ const registry = {
|
|
|
3507
4201
|
const NORM_VERSIONS = {
|
|
3508
4202
|
property: 1,
|
|
3509
4203
|
group: 1,
|
|
3510
|
-
object: 1
|
|
4204
|
+
object: 1,
|
|
4205
|
+
pipeline: 1,
|
|
4206
|
+
stage: 1
|
|
3511
4207
|
};
|
|
3512
4208
|
/**
|
|
3513
4209
|
* Standard objects whose properties and groups read under a scope other than `crm.schemas.<object>.read`, checked
|
|
@@ -3607,7 +4303,10 @@ const fallback = {
|
|
|
3607
4303
|
intervalMs: 1e3
|
|
3608
4304
|
};
|
|
3609
4305
|
const fallbackWarning = "HubSpot sent no rate-limit headers. Sending at most 8 requests per second.";
|
|
3610
|
-
/**
|
|
4306
|
+
/**
|
|
4307
|
+
* The writes apply sends: properties, property groups, pipelines and stages. No custom object schema write, and no
|
|
4308
|
+
* pipeline or stage PUT, which the registry does not name.
|
|
4309
|
+
*/
|
|
3611
4310
|
const MILESTONE_3_WRITES = [
|
|
3612
4311
|
{
|
|
3613
4312
|
type: "property",
|
|
@@ -3632,6 +4331,30 @@ const MILESTONE_3_WRITES = [
|
|
|
3632
4331
|
{
|
|
3633
4332
|
type: "group",
|
|
3634
4333
|
path: "delete"
|
|
4334
|
+
},
|
|
4335
|
+
{
|
|
4336
|
+
type: "pipeline",
|
|
4337
|
+
path: "create"
|
|
4338
|
+
},
|
|
4339
|
+
{
|
|
4340
|
+
type: "pipeline",
|
|
4341
|
+
path: "update"
|
|
4342
|
+
},
|
|
4343
|
+
{
|
|
4344
|
+
type: "pipeline",
|
|
4345
|
+
path: "delete"
|
|
4346
|
+
},
|
|
4347
|
+
{
|
|
4348
|
+
type: "stage",
|
|
4349
|
+
path: "create"
|
|
4350
|
+
},
|
|
4351
|
+
{
|
|
4352
|
+
type: "stage",
|
|
4353
|
+
path: "update"
|
|
4354
|
+
},
|
|
4355
|
+
{
|
|
4356
|
+
type: "stage",
|
|
4357
|
+
path: "delete"
|
|
3635
4358
|
}
|
|
3636
4359
|
];
|
|
3637
4360
|
/** How many `errors` entries an outcome keeps: a property used in hundreds of places must not balloon it. */
|
|
@@ -3833,7 +4556,7 @@ async function exchange(fetch, url, init, timeoutMs, key) {
|
|
|
3833
4556
|
kind: "answered",
|
|
3834
4557
|
status: res.status,
|
|
3835
4558
|
headers: res.headers,
|
|
3836
|
-
text: scrub$
|
|
4559
|
+
text: scrub$2(await res.text(), key)
|
|
3837
4560
|
};
|
|
3838
4561
|
} catch (error) {
|
|
3839
4562
|
return {
|
|
@@ -3848,16 +4571,16 @@ async function exchange(fetch, url, init, timeoutMs, key) {
|
|
|
3848
4571
|
clearTimeout(timer);
|
|
3849
4572
|
}
|
|
3850
4573
|
}
|
|
3851
|
-
function scrub$
|
|
4574
|
+
function scrub$2(text, key) {
|
|
3852
4575
|
return key === "" ? text : text.split(key).join("[key]");
|
|
3853
4576
|
}
|
|
3854
4577
|
function quote(text, key, max = 120) {
|
|
3855
|
-
return sanitize(scrub$
|
|
4578
|
+
return sanitize(scrub$2(scrub$2(sanitize(scrub$2(text, key), Number.POSITIVE_INFINITY), key), sanitize(key, Number.POSITIVE_INFINITY)), max);
|
|
3856
4579
|
}
|
|
3857
4580
|
function scrubJson(value, key) {
|
|
3858
|
-
if (typeof value === "string") return scrub$
|
|
4581
|
+
if (typeof value === "string") return scrub$2(value, key);
|
|
3859
4582
|
if (Array.isArray(value)) return value.map((item) => scrubJson(item, key));
|
|
3860
|
-
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([name, item]) => [scrub$
|
|
4583
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([name, item]) => [scrub$2(name, key), scrubJson(item, key)]));
|
|
3861
4584
|
return value;
|
|
3862
4585
|
}
|
|
3863
4586
|
function readStep(answer, attempt, context) {
|
|
@@ -4066,119 +4789,241 @@ function portalMidnight(now, timeZone) {
|
|
|
4066
4789
|
return new Date(Date.UTC(part.year ?? 0, (part.month ?? 1) - 1, (part.day ?? 1) + 1) - offset).toISOString();
|
|
4067
4790
|
}
|
|
4068
4791
|
/**
|
|
4069
|
-
*
|
|
4070
|
-
*
|
|
4071
|
-
*
|
|
4072
|
-
*/
|
|
4073
|
-
const STANDARD_OBJECTS = /* @__PURE__ */ new Set([
|
|
4074
|
-
"appointments",
|
|
4075
|
-
"calls",
|
|
4076
|
-
"carts",
|
|
4077
|
-
"commerce_payments",
|
|
4078
|
-
"communications",
|
|
4079
|
-
"companies",
|
|
4080
|
-
"contacts",
|
|
4081
|
-
"courses",
|
|
4082
|
-
"deals",
|
|
4083
|
-
"emails",
|
|
4084
|
-
"feedback_submissions",
|
|
4085
|
-
"goals",
|
|
4086
|
-
"invoices",
|
|
4087
|
-
"leads",
|
|
4088
|
-
"line_items",
|
|
4089
|
-
"listings",
|
|
4090
|
-
"marketing_events",
|
|
4091
|
-
"meetings",
|
|
4092
|
-
"notes",
|
|
4093
|
-
"orders",
|
|
4094
|
-
"postal_mail",
|
|
4095
|
-
"products",
|
|
4096
|
-
"projects",
|
|
4097
|
-
"quotes",
|
|
4098
|
-
"services",
|
|
4099
|
-
"subscriptions",
|
|
4100
|
-
"tasks",
|
|
4101
|
-
"tickets",
|
|
4102
|
-
"users"
|
|
4103
|
-
]);
|
|
4104
|
-
/**
|
|
4105
|
-
* The object type ID of each standard object, by config key. Limits Tracking keys its per-object entries by these.
|
|
4106
|
-
* Source: "Object type ID values", retrieved 2026-09-24:
|
|
4107
|
-
* https://developers.hubspot.com/docs/api-reference/latest/crm/understanding-the-crm
|
|
4108
|
-
* A plain record: look a key up with `Object.hasOwn`, so `constructor` finds nothing.
|
|
4792
|
+
* A portal resource a name override shadows belongs to no address. Where another resource refers to it (a property's
|
|
4793
|
+
* group, a name in a custom object schema), the read records `shadowed:<name>`, which no HubSpot internal name and so
|
|
4794
|
+
* no config name equals.
|
|
4109
4795
|
*/
|
|
4110
|
-
const
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
courses: "0-410",
|
|
4119
|
-
deals: "0-3",
|
|
4120
|
-
emails: "0-49",
|
|
4121
|
-
feedback_submissions: "0-19",
|
|
4122
|
-
goals: "0-74",
|
|
4123
|
-
invoices: "0-53",
|
|
4124
|
-
leads: "0-136",
|
|
4125
|
-
line_items: "0-8",
|
|
4126
|
-
listings: "0-420",
|
|
4127
|
-
marketing_events: "0-54",
|
|
4128
|
-
meetings: "0-47",
|
|
4129
|
-
notes: "0-46",
|
|
4130
|
-
orders: "0-123",
|
|
4131
|
-
postal_mail: "0-116",
|
|
4132
|
-
products: "0-7",
|
|
4133
|
-
projects: "0-970",
|
|
4134
|
-
quotes: "0-14",
|
|
4135
|
-
services: "0-162",
|
|
4136
|
-
subscriptions: "0-69",
|
|
4137
|
-
tasks: "0-27",
|
|
4138
|
-
tickets: "0-5",
|
|
4139
|
-
users: "0-115"
|
|
4796
|
+
const SHADOWED = "shadowed:";
|
|
4797
|
+
const KINDS$1 = {
|
|
4798
|
+
string: "string",
|
|
4799
|
+
number: "number",
|
|
4800
|
+
bool: "boolean",
|
|
4801
|
+
date: "date",
|
|
4802
|
+
datetime: "datetime",
|
|
4803
|
+
phone_number: "phoneNumber"
|
|
4140
4804
|
};
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
return
|
|
4144
|
-
custom: scope.custom ?? true,
|
|
4145
|
-
defined: new Set(defined),
|
|
4146
|
-
exclude: excluder(scope.exclude),
|
|
4147
|
-
include: new Set(scope.include ?? [])
|
|
4148
|
-
};
|
|
4805
|
+
function kindOf$4(type, fieldType) {
|
|
4806
|
+
if (type === "enumeration") return fieldType === "checkbox" ? "multiEnum" : "enum";
|
|
4807
|
+
return Object.hasOwn(KINDS$1, type) ? KINDS$1[type] : void 0;
|
|
4149
4808
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
return Object.keys(ir.resources).filter((address) => address.startsWith(prefix)).map((address) => address.slice(prefix.length));
|
|
4809
|
+
function builderOf(p, external) {
|
|
4810
|
+
if (isOwner(p)) return "owner";
|
|
4811
|
+
return external ? "string" : kindOf$4(p.type, p.fieldType);
|
|
4154
4812
|
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
* `exclude` say, or when it is custom, `custom` is on and `exclude` does not name it. validate refuses a name both
|
|
4158
|
-
* `include` and `exclude` hold.
|
|
4159
|
-
*/
|
|
4160
|
-
function inScope(scope, property) {
|
|
4161
|
-
const { name } = property;
|
|
4162
|
-
return scope.defined.has(name) || scope.include.has(name) || scope.custom && !property.hubspotDefined && !scope.exclude(name);
|
|
4813
|
+
function writable(p, kind, external) {
|
|
4814
|
+
return (kind === "owner" || !external) && FIELD_TYPES[kind].includes(p.fieldType);
|
|
4163
4815
|
}
|
|
4164
|
-
/**
|
|
4165
|
-
function
|
|
4166
|
-
|
|
4167
|
-
return (name) => matchers.some((matches) => matches(name));
|
|
4816
|
+
/** A HubSpot user property p.owner writes: an enumeration HubSpot fills with owners, as a select or radio. */
|
|
4817
|
+
function isOwner(p) {
|
|
4818
|
+
return p.referencedObjectType === "OWNER" && p.externalOptions === true && p.type === "enumeration" && FIELD_TYPES.owner.includes(p.fieldType);
|
|
4168
4819
|
}
|
|
4169
|
-
/**
|
|
4170
|
-
function
|
|
4171
|
-
if (
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4820
|
+
/** Why Kalup does not write a property: its options come from HubSpot, or its type and fieldType fit no builder. */
|
|
4821
|
+
function unsupportedReason(p) {
|
|
4822
|
+
if (p.referencedObjectType === "OWNER") return `is a HubSpot user property with fieldType ${sanitize(p.fieldType)}`;
|
|
4823
|
+
if (p.externalOptions) return "takes its options from HubSpot (externalOptions)";
|
|
4824
|
+
return `has type ${sanitize(p.type)} and fieldType ${sanitize(p.fieldType)}`;
|
|
4825
|
+
}
|
|
4826
|
+
/**
|
|
4827
|
+
* Live properties in portal order. Archived properties are skipped. An owner property (a select or radio HubSpot fills
|
|
4828
|
+
* with users) is `p.owner`. HubSpot fills the options of any other externalOptions property, so it reads as a string: a
|
|
4829
|
+
* HubSpot-defined or HubSpot-calculated one is a `p.string` reference, a custom one is unsupported. So is a property
|
|
4830
|
+
* whose type no builder carries (object_coordinates, json, anything unknown), or a custom one whose fieldType no builder
|
|
4831
|
+
* accepts (a calculation_rollup), each with one warning when `warn` says the project cares about it: a property outside
|
|
4832
|
+
* the pull scope and the files is noise. A custom calculation_equation property is managed: HubSpot marks it
|
|
4833
|
+
* `calculated`, but its formula is Kalup's to write.
|
|
4834
|
+
*/
|
|
4835
|
+
function normalizeProperties(object, raw, issues, warn = () => true) {
|
|
4836
|
+
const properties = [];
|
|
4837
|
+
const unsupported = [];
|
|
4838
|
+
for (const p of raw) {
|
|
4839
|
+
if (p.archived) continue;
|
|
4840
|
+
const hubspotDefined = Boolean(p.hubspotDefined);
|
|
4841
|
+
const reference = Boolean(p.hubspotDefined || p.calculated && p.fieldType !== "calculation_equation" || RESERVED_PREFIX.test(p.name));
|
|
4842
|
+
const external = p.externalOptions === true || p.referencedObjectType === "OWNER";
|
|
4843
|
+
const kind = builderOf(p, external);
|
|
4844
|
+
if (kind === void 0 || !(reference || writable(p, kind, external))) {
|
|
4845
|
+
const u = unsupportedOf(p);
|
|
4846
|
+
warnUnsupported(issues, object, u, warn(p));
|
|
4847
|
+
unsupported.push(u);
|
|
4848
|
+
continue;
|
|
4849
|
+
}
|
|
4850
|
+
const options = kind === "enum" || kind === "multiEnum" ? normalizeOptions(p.options ?? []) : void 0;
|
|
4851
|
+
properties.push({
|
|
4852
|
+
name: p.name,
|
|
4853
|
+
...external ? { external: true } : {},
|
|
4854
|
+
hubspotDefined,
|
|
4855
|
+
type: p.type,
|
|
4856
|
+
fieldType: p.fieldType,
|
|
4857
|
+
kind,
|
|
4858
|
+
reference,
|
|
4859
|
+
calculated: Boolean(p.calculated),
|
|
4860
|
+
definition: definitionOf$1(p, reference, options)
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
return {
|
|
4864
|
+
properties,
|
|
4865
|
+
unsupported
|
|
4866
|
+
};
|
|
4867
|
+
}
|
|
4868
|
+
function warnUnsupported(issues, object, u, wanted) {
|
|
4869
|
+
if (wanted) issues.push({
|
|
4870
|
+
code: "W_UNSUPPORTED_TYPE",
|
|
4871
|
+
message: `property:${object}/${sanitize(u.name)} ${unsupportedReason(u)}, which Kalup does not write; read as a p.string reference`
|
|
4872
|
+
});
|
|
4873
|
+
}
|
|
4874
|
+
function unsupportedOf(p) {
|
|
4875
|
+
return compact$4({
|
|
4876
|
+
name: p.name,
|
|
4877
|
+
label: p.label,
|
|
4878
|
+
group: p.groupName,
|
|
4879
|
+
description: p.description || void 0,
|
|
4880
|
+
type: p.type,
|
|
4881
|
+
fieldType: p.fieldType,
|
|
4882
|
+
options: p.options?.length ? normalizeOptions(p.options) : void 0,
|
|
4883
|
+
hubspotDefined: Boolean(p.hubspotDefined),
|
|
4884
|
+
externalOptions: p.externalOptions === true || void 0,
|
|
4885
|
+
referencedObjectType: p.referencedObjectType
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4888
|
+
/** The portal names of the unarchived properties in each portal group, sorted. */
|
|
4889
|
+
function groupMembers(raw) {
|
|
4890
|
+
const members = /* @__PURE__ */ new Map();
|
|
4891
|
+
for (const p of raw.filter((property) => !property.archived)) members.set(p.groupName, [...members.get(p.groupName) ?? [], p.name]);
|
|
4892
|
+
return new Map([...members].map(([group, names]) => [group, names.sort(byCodeUnit)]));
|
|
4893
|
+
}
|
|
4894
|
+
/** The meta of each unarchived property, by name. */
|
|
4895
|
+
function propertyMeta(raw) {
|
|
4896
|
+
return new Map(raw.filter((p) => !p.archived).map((p) => [p.name, metaOf(p)]));
|
|
4897
|
+
}
|
|
4898
|
+
function metaOf(p) {
|
|
4899
|
+
const text = (value) => typeof value === "string" ? value : void 0;
|
|
4900
|
+
const flag = (value) => typeof value === "boolean" ? value : void 0;
|
|
4901
|
+
const flags = p.modificationMetadata;
|
|
4902
|
+
const modificationMetadata = flags === void 0 ? void 0 : compact$4({
|
|
4903
|
+
archivable: flag(flags.archivable),
|
|
4904
|
+
readOnlyDefinition: flag(flags.readOnlyDefinition),
|
|
4905
|
+
readOnlyOptions: flag(flags.readOnlyOptions),
|
|
4906
|
+
readOnlyValue: flag(flags.readOnlyValue)
|
|
4907
|
+
});
|
|
4908
|
+
const options = p.options?.map((o) => compact$4({
|
|
4909
|
+
value: o.value,
|
|
4910
|
+
displayOrder: typeof o.displayOrder === "number" ? o.displayOrder : void 0
|
|
4911
|
+
}));
|
|
4912
|
+
return compact$4({
|
|
4913
|
+
sensitivity: p.sensitivity,
|
|
4914
|
+
hubspotDefined: flag(p.hubspotDefined),
|
|
4915
|
+
modificationMetadata,
|
|
4916
|
+
createdAt: text(p.createdAt),
|
|
4917
|
+
updatedAt: text(p.updatedAt),
|
|
4918
|
+
options: options?.length ? options : void 0,
|
|
4919
|
+
currencyPropertyName: text(p.currencyPropertyName)
|
|
4920
|
+
});
|
|
4921
|
+
}
|
|
4922
|
+
function definitionOf$1(p, reference, options) {
|
|
4923
|
+
if (!reference) return compact$4({
|
|
4924
|
+
label: p.label,
|
|
4925
|
+
group: p.groupName,
|
|
4926
|
+
fieldType: p.fieldType,
|
|
4927
|
+
description: p.description || void 0,
|
|
4928
|
+
options: options?.length ? options : void 0,
|
|
4929
|
+
hasUniqueValue: p.hasUniqueValue || void 0,
|
|
4930
|
+
formField: p.formField || void 0,
|
|
4931
|
+
hidden: p.hidden || void 0,
|
|
4932
|
+
displayOrder: typeof p.displayOrder === "number" && p.displayOrder !== -1 ? p.displayOrder : void 0,
|
|
4933
|
+
...displayOf(p),
|
|
4934
|
+
calculationFormula: p.fieldType === "calculation_equation" ? filled(p.calculationFormula) : void 0,
|
|
4935
|
+
dataSensitivity: unlessDefault("dataSensitivity", p.dataSensitivity)
|
|
4936
|
+
});
|
|
4937
|
+
return options?.length ? { options } : void 0;
|
|
4938
|
+
}
|
|
4939
|
+
function displayOf(p) {
|
|
4940
|
+
const shown = (field) => TYPE_FIELDS[field]?.includes(p.type) === true;
|
|
4941
|
+
return {
|
|
4942
|
+
numberDisplayHint: shown("numberDisplayHint") ? unlessDefault("numberDisplayHint", p.numberDisplayHint) : void 0,
|
|
4943
|
+
showCurrencySymbol: shown("showCurrencySymbol") ? p.showCurrencySymbol || void 0 : void 0,
|
|
4944
|
+
currencyPropertyName: shown("currencyPropertyName") ? filled(p.currencyPropertyName) : void 0,
|
|
4945
|
+
textDisplayHint: shown("textDisplayHint") ? filled(p.textDisplayHint) : void 0
|
|
4946
|
+
};
|
|
4947
|
+
}
|
|
4948
|
+
function filled(value) {
|
|
4949
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
4950
|
+
}
|
|
4951
|
+
function unlessDefault(field, value) {
|
|
4952
|
+
return typeof value === "string" && value !== "" && value !== DEFAULTS.definition[field] ? value : void 0;
|
|
4953
|
+
}
|
|
4954
|
+
function normalizeOptions(raw) {
|
|
4955
|
+
const order = (o) => o.displayOrder === void 0 || o.displayOrder < 0 ? Number.MAX_SAFE_INTEGER : o.displayOrder;
|
|
4956
|
+
return [...raw].sort((a, b) => order(a) - order(b)).map((o) => compact$4({
|
|
4957
|
+
value: o.value,
|
|
4958
|
+
label: o.label,
|
|
4959
|
+
hidden: o.hidden || void 0,
|
|
4960
|
+
description: o.description || void 0
|
|
4961
|
+
}));
|
|
4962
|
+
}
|
|
4963
|
+
const STATES = /* @__PURE__ */ new Set(["OPEN", "CLOSED"]);
|
|
4964
|
+
/**
|
|
4965
|
+
* The unarchived pipelines of `object`, by displayOrder then ID, since pipelines may share a number and the list is not
|
|
4966
|
+
* sorted; each one's stages by displayOrder, which HubSpot never stores tied (observed 2026-10-05). A stage keeps the
|
|
4967
|
+
* one metadata field its object takes: a deal's probability as a number, a ticket's ticketState, a custom object's
|
|
4968
|
+
* state. `isClosed` is HubSpot's own, derived from that field, and is dropped; so is the metadata of a stage on an
|
|
4969
|
+
* object whose pipelines Kalup does not write.
|
|
4970
|
+
*/
|
|
4971
|
+
function normalizePipelines(object, raw, custom) {
|
|
4972
|
+
const field = stageField(object, custom);
|
|
4973
|
+
const ordered = (list) => [...list].sort((a, b) => a.displayOrder - b.displayOrder || byCodeUnit(a.id, b.id));
|
|
4974
|
+
return ordered(raw.filter((p) => !p.archived)).map((p) => ({
|
|
4975
|
+
id: p.id,
|
|
4976
|
+
label: p.label,
|
|
4977
|
+
displayOrder: p.displayOrder,
|
|
4978
|
+
stages: ordered(p.stages.filter((st) => !st.archived)).map((st) => ({
|
|
4979
|
+
id: st.id,
|
|
4980
|
+
label: st.label,
|
|
4981
|
+
...stageMetadata$1(field, st.metadata ?? {})
|
|
4982
|
+
}))
|
|
4983
|
+
}));
|
|
4984
|
+
}
|
|
4985
|
+
function stageMetadata$1(field, metadata) {
|
|
4986
|
+
const value = field === void 0 ? void 0 : metadata[field];
|
|
4987
|
+
if (field === "probability") {
|
|
4988
|
+
const probability = Number(value);
|
|
4989
|
+
return value !== void 0 && Number.isFinite(probability) ? { probability } : {};
|
|
4990
|
+
}
|
|
4991
|
+
return field !== void 0 && value !== void 0 && STATES.has(value) ? { [field]: value } : {};
|
|
4992
|
+
}
|
|
4993
|
+
function normalizeGroups(raw) {
|
|
4994
|
+
return { groups: new Map(raw.filter((g) => !g.archived).map((g) => [g.name, g.label])) };
|
|
4995
|
+
}
|
|
4996
|
+
/** The schema fields the object file carries, the three lists as HubSpot returned them. */
|
|
4997
|
+
function normalizeSchema(schema) {
|
|
4998
|
+
return compact$4({
|
|
4999
|
+
labels: compact$4({
|
|
5000
|
+
singular: schema.labels?.singular,
|
|
5001
|
+
plural: schema.labels?.plural
|
|
5002
|
+
}),
|
|
5003
|
+
primaryDisplayProperty: schema.primaryDisplayProperty,
|
|
5004
|
+
requiredProperties: schema.requiredProperties,
|
|
5005
|
+
searchableProperties: schema.searchableProperties,
|
|
5006
|
+
secondaryDisplayProperties: schema.secondaryDisplayProperties
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
function compact$4(value) {
|
|
5010
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
4175
5011
|
}
|
|
4176
5012
|
const SETS$1 = /* @__PURE__ */ new Set(["requiredProperties", "searchableProperties"]);
|
|
5013
|
+
/**
|
|
5014
|
+
* Fields that are an order of members: a pipeline's stage IDs. Like `options.order`, one compares only the members
|
|
5015
|
+
* both sides hold, since a stage is a resource of its own and one only one side holds is that stage's step.
|
|
5016
|
+
*/
|
|
5017
|
+
const ORDERS = /* @__PURE__ */ new Set(["stages"]);
|
|
4177
5018
|
/** Every unit desired owns that observed holds, sorted by unit. */
|
|
4178
5019
|
function classify(base, desired, observed, rules) {
|
|
4179
5020
|
const ignored = new Set(rules.ignoreChanges);
|
|
4180
5021
|
const units = [];
|
|
4181
|
-
for (const [field, value] of Object.entries(desired.fields))
|
|
5022
|
+
for (const [field, value] of Object.entries(desired.fields)) {
|
|
5023
|
+
if (ignored.has(field) || !Object.hasOwn(observed.fields, field)) continue;
|
|
5024
|
+
if (ORDERS.has(field)) units.push(orderUnit(field, value, observed.fields[field] ?? [], base));
|
|
5025
|
+
else units.push(compare$1(field, value, observed.fields[field], valueIn(base, field), SETS$1.has(field)));
|
|
5026
|
+
}
|
|
4182
5027
|
if (desired.options && observed.options && !ignored.has("options")) units.push(...options(desired.options, observed.options, base, rules));
|
|
4183
5028
|
return units.sort((a, b) => byCodeUnit(a.unit, b.unit));
|
|
4184
5029
|
}
|
|
@@ -4195,15 +5040,24 @@ function advanceBase(previous, approved, live, units) {
|
|
|
4195
5040
|
const named = (unit) => units === void 0 || units.some((u) => unit === u || unit.startsWith(`${u}.`) || unit.startsWith(`${u}[`));
|
|
4196
5041
|
const next = new Map(Object.entries(previous ?? {}));
|
|
4197
5042
|
let agreed = false;
|
|
4198
|
-
for (const [field, value] of Object.entries(approved.fields))
|
|
4199
|
-
|
|
4200
|
-
|
|
5043
|
+
for (const [field, value] of Object.entries(approved.fields)) {
|
|
5044
|
+
if (!(named(field) && Object.hasOwn(live.fields, field))) continue;
|
|
5045
|
+
const both = agreedField(field, value, live.fields[field]);
|
|
5046
|
+
if (both !== void 0) {
|
|
5047
|
+
next.set(field, both);
|
|
5048
|
+
agreed = true;
|
|
5049
|
+
}
|
|
4201
5050
|
}
|
|
4202
5051
|
if (approved.options && live.options) agreed = advanceOptions(next, approved.options, live.options, named) || agreed;
|
|
4203
5052
|
if (!agreed && previous === void 0) return;
|
|
4204
5053
|
for (const [field, omitted] of Object.entries(DEFAULTS.definition)) if (field !== "options" && !Object.hasOwn(approved.fields, field) && !next.has(field) && Object.hasOwn(live.fields, field) && same$3(live.fields[field], omitted)) next.set(field, omitted);
|
|
4205
5054
|
return sortedRecord(next);
|
|
4206
5055
|
}
|
|
5056
|
+
function agreedField(field, approved, live) {
|
|
5057
|
+
if (!ORDERS.has(field)) return same$3(approved, live, SETS$1.has(field)) ? approved : void 0;
|
|
5058
|
+
const [mine, theirs] = commonOrders(approved, live ?? []);
|
|
5059
|
+
return mine.length > 0 && same$3(mine, theirs) ? mine : void 0;
|
|
5060
|
+
}
|
|
4207
5061
|
function advanceOptions(next, approved, live, named) {
|
|
4208
5062
|
const wanted = new Map(approved.map((o) => [o.value, o]));
|
|
4209
5063
|
const held = new Map(live.map((o) => [o.value, o]));
|
|
@@ -4277,6 +5131,16 @@ function options(desired, observed, base, rules) {
|
|
|
4277
5131
|
units.push(compare$1("options.order", order, common(observed, wanted), orderIn(base, order)));
|
|
4278
5132
|
return units;
|
|
4279
5133
|
}
|
|
5134
|
+
function orderUnit(field, desired, observed, base) {
|
|
5135
|
+
const [mine, theirs] = commonOrders(desired, observed);
|
|
5136
|
+
const stored = valueIn(base, field);
|
|
5137
|
+
const order = (stored?.value ?? []).filter((member) => mine.includes(member));
|
|
5138
|
+
return compare$1(field, mine, theirs, stored && order.length === mine.length ? { value: order } : void 0);
|
|
5139
|
+
}
|
|
5140
|
+
/** The members two orders share, each in its own order. */
|
|
5141
|
+
function commonOrders(a, b) {
|
|
5142
|
+
return [a.filter((member) => b.includes(member)), b.filter((member) => a.includes(member))];
|
|
5143
|
+
}
|
|
4280
5144
|
function orderIn(base, members) {
|
|
4281
5145
|
const stored = valueIn(base, "optionsOrder");
|
|
4282
5146
|
if (!stored) return;
|
|
@@ -4337,6 +5201,10 @@ function equal(a, b) {
|
|
|
4337
5201
|
function sortedRecord(entries) {
|
|
4338
5202
|
return Object.fromEntries([...entries].sort(([a], [b]) => byCodeUnit(a, b)));
|
|
4339
5203
|
}
|
|
5204
|
+
/** A count and its noun: `1 write`, `2 writes`. The plural is the noun plus `s` unless `many` gives another. */
|
|
5205
|
+
function plural(count, one, many = `${one}s`) {
|
|
5206
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
5207
|
+
}
|
|
4340
5208
|
const PLAN_SCHEMA = {
|
|
4341
5209
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
4342
5210
|
$id: "https://kalup.dev/schemas/plan-1.schema.json",
|
|
@@ -4739,6 +5607,24 @@ const PLAN_SCHEMA = {
|
|
|
4739
5607
|
"description": "Logical form, $ref included, resolved at apply. A create holds the full definition.",
|
|
4740
5608
|
"type": "object"
|
|
4741
5609
|
},
|
|
5610
|
+
"stages": {
|
|
5611
|
+
"description": "A pipeline create only: the stages it carries, in display order, each with its full definition. HubSpot refuses a pipeline without a stage.",
|
|
5612
|
+
"type": "array",
|
|
5613
|
+
"items": {
|
|
5614
|
+
"type": "object",
|
|
5615
|
+
"required": ["address", "desired"],
|
|
5616
|
+
"properties": {
|
|
5617
|
+
"address": { "$ref": "#/$defs/address" },
|
|
5618
|
+
"desired": { "type": "object" }
|
|
5619
|
+
},
|
|
5620
|
+
"additionalProperties": false
|
|
5621
|
+
}
|
|
5622
|
+
},
|
|
5623
|
+
"stageLabels": {
|
|
5624
|
+
"description": "Display only, never approved: the label of each stage a pipeline step's stage order names, config's else the portal's, so the plan text shows labels and not IDs.",
|
|
5625
|
+
"type": "object",
|
|
5626
|
+
"additionalProperties": { "type": "string" }
|
|
5627
|
+
},
|
|
4742
5628
|
"ignoreChanges": {
|
|
4743
5629
|
"description": "Create only: set on create, released after.",
|
|
4744
5630
|
"type": "array",
|
|
@@ -5044,197 +5930,6 @@ function validatePlan(document) {
|
|
|
5044
5930
|
...path ? { configPath: path } : {}
|
|
5045
5931
|
}));
|
|
5046
5932
|
}
|
|
5047
|
-
/**
|
|
5048
|
-
* A portal resource a name override shadows belongs to no address. Where another resource refers to it (a property's
|
|
5049
|
-
* group, a name in a custom object schema), the read records `shadowed:<name>`, which no HubSpot internal name and so
|
|
5050
|
-
* no config name equals.
|
|
5051
|
-
*/
|
|
5052
|
-
const SHADOWED = "shadowed:";
|
|
5053
|
-
const KINDS$1 = {
|
|
5054
|
-
string: "string",
|
|
5055
|
-
number: "number",
|
|
5056
|
-
bool: "boolean",
|
|
5057
|
-
date: "date",
|
|
5058
|
-
datetime: "datetime",
|
|
5059
|
-
phone_number: "phoneNumber"
|
|
5060
|
-
};
|
|
5061
|
-
function kindOf$4(type, fieldType) {
|
|
5062
|
-
if (type === "enumeration") return fieldType === "checkbox" ? "multiEnum" : "enum";
|
|
5063
|
-
return Object.hasOwn(KINDS$1, type) ? KINDS$1[type] : void 0;
|
|
5064
|
-
}
|
|
5065
|
-
function builderOf(p, external) {
|
|
5066
|
-
if (isOwner(p)) return "owner";
|
|
5067
|
-
return external ? "string" : kindOf$4(p.type, p.fieldType);
|
|
5068
|
-
}
|
|
5069
|
-
function writable(p, kind, external) {
|
|
5070
|
-
return (kind === "owner" || !external) && FIELD_TYPES[kind].includes(p.fieldType);
|
|
5071
|
-
}
|
|
5072
|
-
/** A HubSpot user property p.owner writes: an enumeration HubSpot fills with owners, as a select or radio. */
|
|
5073
|
-
function isOwner(p) {
|
|
5074
|
-
return p.referencedObjectType === "OWNER" && p.externalOptions === true && p.type === "enumeration" && FIELD_TYPES.owner.includes(p.fieldType);
|
|
5075
|
-
}
|
|
5076
|
-
/** Why Kalup does not write a property: its options come from HubSpot, or its type and fieldType fit no builder. */
|
|
5077
|
-
function unsupportedReason(p) {
|
|
5078
|
-
if (p.referencedObjectType === "OWNER") return `is a HubSpot user property with fieldType ${sanitize(p.fieldType)}`;
|
|
5079
|
-
if (p.externalOptions) return "takes its options from HubSpot (externalOptions)";
|
|
5080
|
-
return `has type ${sanitize(p.type)} and fieldType ${sanitize(p.fieldType)}`;
|
|
5081
|
-
}
|
|
5082
|
-
/**
|
|
5083
|
-
* Live properties in portal order. Archived properties are skipped. An owner property (a select or radio HubSpot fills
|
|
5084
|
-
* with users) is `p.owner`. HubSpot fills the options of any other externalOptions property, so it reads as a string: a
|
|
5085
|
-
* HubSpot-defined or HubSpot-calculated one is a `p.string` reference, a custom one is unsupported. So is a property
|
|
5086
|
-
* whose type no builder carries (object_coordinates, json, anything unknown), or a custom one whose fieldType no builder
|
|
5087
|
-
* accepts (a calculation_rollup), each with one warning when `warn` says the project cares about it: a property outside
|
|
5088
|
-
* the pull scope and the files is noise. A custom calculation_equation property is managed: HubSpot marks it
|
|
5089
|
-
* `calculated`, but its formula is Kalup's to write.
|
|
5090
|
-
*/
|
|
5091
|
-
function normalizeProperties(object, raw, issues, warn = () => true) {
|
|
5092
|
-
const properties = [];
|
|
5093
|
-
const unsupported = [];
|
|
5094
|
-
for (const p of raw) {
|
|
5095
|
-
if (p.archived) continue;
|
|
5096
|
-
const hubspotDefined = Boolean(p.hubspotDefined);
|
|
5097
|
-
const reference = Boolean(p.hubspotDefined || p.calculated && p.fieldType !== "calculation_equation" || RESERVED_PREFIX.test(p.name));
|
|
5098
|
-
const external = p.externalOptions === true || p.referencedObjectType === "OWNER";
|
|
5099
|
-
const kind = builderOf(p, external);
|
|
5100
|
-
if (kind === void 0 || !(reference || writable(p, kind, external))) {
|
|
5101
|
-
const u = unsupportedOf(p);
|
|
5102
|
-
warnUnsupported(issues, object, u, warn(p));
|
|
5103
|
-
unsupported.push(u);
|
|
5104
|
-
continue;
|
|
5105
|
-
}
|
|
5106
|
-
const options = kind === "enum" || kind === "multiEnum" ? normalizeOptions(p.options ?? []) : void 0;
|
|
5107
|
-
properties.push({
|
|
5108
|
-
name: p.name,
|
|
5109
|
-
...external ? { external: true } : {},
|
|
5110
|
-
hubspotDefined,
|
|
5111
|
-
type: p.type,
|
|
5112
|
-
fieldType: p.fieldType,
|
|
5113
|
-
kind,
|
|
5114
|
-
reference,
|
|
5115
|
-
calculated: Boolean(p.calculated),
|
|
5116
|
-
definition: definitionOf$1(p, reference, options)
|
|
5117
|
-
});
|
|
5118
|
-
}
|
|
5119
|
-
return {
|
|
5120
|
-
properties,
|
|
5121
|
-
unsupported
|
|
5122
|
-
};
|
|
5123
|
-
}
|
|
5124
|
-
function warnUnsupported(issues, object, u, wanted) {
|
|
5125
|
-
if (wanted) issues.push({
|
|
5126
|
-
code: "W_UNSUPPORTED_TYPE",
|
|
5127
|
-
message: `property:${object}/${sanitize(u.name)} ${unsupportedReason(u)}, which Kalup does not write; read as a p.string reference`
|
|
5128
|
-
});
|
|
5129
|
-
}
|
|
5130
|
-
function unsupportedOf(p) {
|
|
5131
|
-
return compact$4({
|
|
5132
|
-
name: p.name,
|
|
5133
|
-
label: p.label,
|
|
5134
|
-
group: p.groupName,
|
|
5135
|
-
description: p.description || void 0,
|
|
5136
|
-
type: p.type,
|
|
5137
|
-
fieldType: p.fieldType,
|
|
5138
|
-
options: p.options?.length ? normalizeOptions(p.options) : void 0,
|
|
5139
|
-
hubspotDefined: Boolean(p.hubspotDefined),
|
|
5140
|
-
externalOptions: p.externalOptions === true || void 0,
|
|
5141
|
-
referencedObjectType: p.referencedObjectType
|
|
5142
|
-
});
|
|
5143
|
-
}
|
|
5144
|
-
/** The portal names of the unarchived properties in each portal group, sorted. */
|
|
5145
|
-
function groupMembers(raw) {
|
|
5146
|
-
const members = /* @__PURE__ */ new Map();
|
|
5147
|
-
for (const p of raw.filter((property) => !property.archived)) members.set(p.groupName, [...members.get(p.groupName) ?? [], p.name]);
|
|
5148
|
-
return new Map([...members].map(([group, names]) => [group, names.sort(byCodeUnit)]));
|
|
5149
|
-
}
|
|
5150
|
-
/** The meta of each unarchived property, by name. */
|
|
5151
|
-
function propertyMeta(raw) {
|
|
5152
|
-
return new Map(raw.filter((p) => !p.archived).map((p) => [p.name, metaOf(p)]));
|
|
5153
|
-
}
|
|
5154
|
-
function metaOf(p) {
|
|
5155
|
-
const text = (value) => typeof value === "string" ? value : void 0;
|
|
5156
|
-
const flag = (value) => typeof value === "boolean" ? value : void 0;
|
|
5157
|
-
const flags = p.modificationMetadata;
|
|
5158
|
-
const modificationMetadata = flags === void 0 ? void 0 : compact$4({
|
|
5159
|
-
archivable: flag(flags.archivable),
|
|
5160
|
-
readOnlyDefinition: flag(flags.readOnlyDefinition),
|
|
5161
|
-
readOnlyOptions: flag(flags.readOnlyOptions),
|
|
5162
|
-
readOnlyValue: flag(flags.readOnlyValue)
|
|
5163
|
-
});
|
|
5164
|
-
const options = p.options?.map((o) => compact$4({
|
|
5165
|
-
value: o.value,
|
|
5166
|
-
displayOrder: typeof o.displayOrder === "number" ? o.displayOrder : void 0
|
|
5167
|
-
}));
|
|
5168
|
-
return compact$4({
|
|
5169
|
-
sensitivity: p.sensitivity,
|
|
5170
|
-
hubspotDefined: flag(p.hubspotDefined),
|
|
5171
|
-
modificationMetadata,
|
|
5172
|
-
createdAt: text(p.createdAt),
|
|
5173
|
-
updatedAt: text(p.updatedAt),
|
|
5174
|
-
options: options?.length ? options : void 0,
|
|
5175
|
-
currencyPropertyName: text(p.currencyPropertyName)
|
|
5176
|
-
});
|
|
5177
|
-
}
|
|
5178
|
-
function definitionOf$1(p, reference, options) {
|
|
5179
|
-
if (!reference) return compact$4({
|
|
5180
|
-
label: p.label,
|
|
5181
|
-
group: p.groupName,
|
|
5182
|
-
fieldType: p.fieldType,
|
|
5183
|
-
description: p.description || void 0,
|
|
5184
|
-
options: options?.length ? options : void 0,
|
|
5185
|
-
hasUniqueValue: p.hasUniqueValue || void 0,
|
|
5186
|
-
formField: p.formField || void 0,
|
|
5187
|
-
hidden: p.hidden || void 0,
|
|
5188
|
-
displayOrder: typeof p.displayOrder === "number" && p.displayOrder !== -1 ? p.displayOrder : void 0,
|
|
5189
|
-
...displayOf(p),
|
|
5190
|
-
calculationFormula: p.fieldType === "calculation_equation" ? filled(p.calculationFormula) : void 0,
|
|
5191
|
-
dataSensitivity: unlessDefault("dataSensitivity", p.dataSensitivity)
|
|
5192
|
-
});
|
|
5193
|
-
return options?.length ? { options } : void 0;
|
|
5194
|
-
}
|
|
5195
|
-
function displayOf(p) {
|
|
5196
|
-
const shown = (field) => TYPE_FIELDS[field]?.includes(p.type) === true;
|
|
5197
|
-
return {
|
|
5198
|
-
numberDisplayHint: shown("numberDisplayHint") ? unlessDefault("numberDisplayHint", p.numberDisplayHint) : void 0,
|
|
5199
|
-
showCurrencySymbol: shown("showCurrencySymbol") ? p.showCurrencySymbol || void 0 : void 0,
|
|
5200
|
-
currencyPropertyName: shown("currencyPropertyName") ? filled(p.currencyPropertyName) : void 0,
|
|
5201
|
-
textDisplayHint: shown("textDisplayHint") ? filled(p.textDisplayHint) : void 0
|
|
5202
|
-
};
|
|
5203
|
-
}
|
|
5204
|
-
function filled(value) {
|
|
5205
|
-
return typeof value === "string" && value !== "" ? value : void 0;
|
|
5206
|
-
}
|
|
5207
|
-
function unlessDefault(field, value) {
|
|
5208
|
-
return typeof value === "string" && value !== "" && value !== DEFAULTS.definition[field] ? value : void 0;
|
|
5209
|
-
}
|
|
5210
|
-
function normalizeOptions(raw) {
|
|
5211
|
-
const order = (o) => o.displayOrder === void 0 || o.displayOrder < 0 ? Number.MAX_SAFE_INTEGER : o.displayOrder;
|
|
5212
|
-
return [...raw].sort((a, b) => order(a) - order(b)).map((o) => compact$4({
|
|
5213
|
-
value: o.value,
|
|
5214
|
-
label: o.label,
|
|
5215
|
-
hidden: o.hidden || void 0,
|
|
5216
|
-
description: o.description || void 0
|
|
5217
|
-
}));
|
|
5218
|
-
}
|
|
5219
|
-
function normalizeGroups(raw) {
|
|
5220
|
-
return { groups: new Map(raw.filter((g) => !g.archived).map((g) => [g.name, g.label])) };
|
|
5221
|
-
}
|
|
5222
|
-
/** The schema fields the object file carries, the three lists as HubSpot returned them. */
|
|
5223
|
-
function normalizeSchema(schema) {
|
|
5224
|
-
return compact$4({
|
|
5225
|
-
labels: compact$4({
|
|
5226
|
-
singular: schema.labels?.singular,
|
|
5227
|
-
plural: schema.labels?.plural
|
|
5228
|
-
}),
|
|
5229
|
-
primaryDisplayProperty: schema.primaryDisplayProperty,
|
|
5230
|
-
requiredProperties: schema.requiredProperties,
|
|
5231
|
-
searchableProperties: schema.searchableProperties,
|
|
5232
|
-
secondaryDisplayProperties: schema.secondaryDisplayProperties
|
|
5233
|
-
});
|
|
5234
|
-
}
|
|
5235
|
-
function compact$4(value) {
|
|
5236
|
-
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
5237
|
-
}
|
|
5238
5933
|
const CONFIG$1$1 = "kalup.config.ts";
|
|
5239
5934
|
/** HubSpot lists only non-sensitive properties unless asked, and takes one sensitivity per request. */
|
|
5240
5935
|
const SENSITIVITIES$1 = [
|
|
@@ -5301,9 +5996,23 @@ async function readPortal(http, loaded, target, issues, options = {}) {
|
|
|
5301
5996
|
name: p.name,
|
|
5302
5997
|
hubspotDefined: Boolean(p.hubspotDefined)
|
|
5303
5998
|
});
|
|
5999
|
+
const pipelines = await objectPipelines(http, {
|
|
6000
|
+
key,
|
|
6001
|
+
schema,
|
|
6002
|
+
loaded,
|
|
6003
|
+
options
|
|
6004
|
+
}, {
|
|
6005
|
+
renames,
|
|
6006
|
+
excluded,
|
|
6007
|
+
shadowed
|
|
6008
|
+
}, {
|
|
6009
|
+
issues,
|
|
6010
|
+
gaps
|
|
6011
|
+
});
|
|
5304
6012
|
objects.push({
|
|
5305
6013
|
object: key,
|
|
5306
6014
|
objectTypeId: schema?.objectTypeId,
|
|
6015
|
+
...pipelines ? { pipelines } : {},
|
|
5307
6016
|
...normalizeGroups(localize(lists.groups, groupNames).filter(kept("group"))),
|
|
5308
6017
|
...normalizeProperties(key, properties, issues, wanted),
|
|
5309
6018
|
meta: propertyMeta(properties),
|
|
@@ -5323,6 +6032,46 @@ async function readPortal(http, loaded, target, issues, options = {}) {
|
|
|
5323
6032
|
unknownIncludes
|
|
5324
6033
|
};
|
|
5325
6034
|
}
|
|
6035
|
+
async function objectPipelines(http, { key, schema, loaded, options }, { renames, excluded, shadowed }, { issues, gaps }) {
|
|
6036
|
+
const custom = schema !== void 0;
|
|
6037
|
+
if (!(options.pipelines === true && hasPipelines(key, custom) || pipelinesInScope(loaded.config.objects[key], loaded.ir, key))) return;
|
|
6038
|
+
return await readPipelines(http, key, schema ? schema.objectTypeId : key, issues, gaps, (listed) => localPipelines$1(key, normalizePipelines(key, listed, custom), renames, excluded, shadowed));
|
|
6039
|
+
}
|
|
6040
|
+
async function readPipelines(http, object, objectType, issues, gaps, local) {
|
|
6041
|
+
const listed = await gap(() => http.request({
|
|
6042
|
+
type: "pipeline",
|
|
6043
|
+
path: "list",
|
|
6044
|
+
params: { objectType }
|
|
6045
|
+
}), issues, gaps, {
|
|
6046
|
+
list: "pipelines",
|
|
6047
|
+
object,
|
|
6048
|
+
scope: readScope(registry.pipeline, objectType)
|
|
6049
|
+
});
|
|
6050
|
+
return listed && local(listed.results);
|
|
6051
|
+
}
|
|
6052
|
+
function localPipelines$1(object, live, renames, excluded, shadowed) {
|
|
6053
|
+
const where = ` on ${object}`;
|
|
6054
|
+
const pipelines = localNames(renames, `pipeline:${object}/`, live.map((p) => p.id), where);
|
|
6055
|
+
shadowed.push(...pipelines.shadowed);
|
|
6056
|
+
const out = [];
|
|
6057
|
+
for (const p of live) {
|
|
6058
|
+
const id = pipelines.local(p.id);
|
|
6059
|
+
if (pipelines.shadows(p.id) || excluded.has(`pipeline:${object}/${id}`)) continue;
|
|
6060
|
+
const prefix = `stage:${object}/${id}/`;
|
|
6061
|
+
const stages = localNames(renames, prefix, p.stages.map((st) => st.id), where);
|
|
6062
|
+
shadowed.push(...stages.shadowed);
|
|
6063
|
+
const kept = p.stages.filter((st) => !(stages.shadows(st.id) || excluded.has(`${prefix}${stages.local(st.id)}`)));
|
|
6064
|
+
out.push({
|
|
6065
|
+
...p,
|
|
6066
|
+
id,
|
|
6067
|
+
stages: kept.map((st) => ({
|
|
6068
|
+
...st,
|
|
6069
|
+
id: stages.local(st.id)
|
|
6070
|
+
}))
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
return out;
|
|
6074
|
+
}
|
|
5326
6075
|
/**
|
|
5327
6076
|
* One object's archived properties, sorted by name, from the three archived lists merged by name (the first list
|
|
5328
6077
|
* wins). Portal names: no override applies. Any error propagates.
|
|
@@ -5415,6 +6164,10 @@ async function gap(read, issues, gaps, missed) {
|
|
|
5415
6164
|
}
|
|
5416
6165
|
function excludedAddresses(ir, overrides) {
|
|
5417
6166
|
const out = new Set(Object.keys(overrides).filter((address) => overrides[address]?.skip === true));
|
|
6167
|
+
for (const address of Object.keys(ir.resources)) {
|
|
6168
|
+
const pipeline = address.startsWith("stage:") ? `pipeline:${address.slice(6, address.lastIndexOf("/"))}` : void 0;
|
|
6169
|
+
if (pipeline !== void 0 && out.has(pipeline)) out.add(address);
|
|
6170
|
+
}
|
|
5418
6171
|
for (const [address, resource] of Object.entries(ir.resources)) {
|
|
5419
6172
|
const override = Object.hasOwn(overrides, address) ? overrides[address] : void 0;
|
|
5420
6173
|
const moved = resource.managed ? override?.definition?.group : void 0;
|
|
@@ -5526,7 +6279,7 @@ function sha256$1(text) {
|
|
|
5526
6279
|
return `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
|
5527
6280
|
}
|
|
5528
6281
|
function approvalStep(step) {
|
|
5529
|
-
const { address, action, transport, api, labels, baseUnits, desired, ignoreChanges, changes, expect } = step;
|
|
6282
|
+
const { address, action, transport, api, labels, baseUnits, desired, ignoreChanges, changes, expect, stages } = step;
|
|
5530
6283
|
return {
|
|
5531
6284
|
address,
|
|
5532
6285
|
action,
|
|
@@ -5535,6 +6288,7 @@ function approvalStep(step) {
|
|
|
5535
6288
|
labels,
|
|
5536
6289
|
baseUnits,
|
|
5537
6290
|
desired,
|
|
6291
|
+
...stages ? { stages } : {},
|
|
5538
6292
|
ignoreChanges,
|
|
5539
6293
|
changes: changes?.map(({ unit, op, after }) => ({
|
|
5540
6294
|
unit,
|
|
@@ -5544,28 +6298,70 @@ function approvalStep(step) {
|
|
|
5544
6298
|
expect
|
|
5545
6299
|
};
|
|
5546
6300
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
}
|
|
5564
|
-
|
|
6301
|
+
/**
|
|
6302
|
+
* The exact create body for a managed property (POST /crm/properties/{version}/{objectType}) or group. Enum aliases
|
|
6303
|
+
* live in the binding, so they never reach HubSpot. An unmanaged resource is never created, so it throws. Object
|
|
6304
|
+
* schemas wait for milestone 4.
|
|
6305
|
+
*/
|
|
6306
|
+
function toCreatePayload(address, resource) {
|
|
6307
|
+
const { type, path } = parseAddress(address);
|
|
6308
|
+
const name = lastSegment(path);
|
|
6309
|
+
if (!resource.managed) throw new Error(`${address} is not managed and is never created`);
|
|
6310
|
+
const { definition } = resource;
|
|
6311
|
+
if (!definition) throw new Error(`${address} has no definition to create from`);
|
|
6312
|
+
if (type === "group") return {
|
|
6313
|
+
name,
|
|
6314
|
+
label: definition.label
|
|
6315
|
+
};
|
|
6316
|
+
if (type === "object") throw new Error(`${address}: the create payload for an object waits for milestone 4`);
|
|
6317
|
+
if (type !== "property") throw new Error(`${address}: no create payload for type "${type}"`);
|
|
6318
|
+
const group = definition.group;
|
|
6319
|
+
if (!group?.$ref) throw new Error(`${address}: definition.group is not a $ref`);
|
|
6320
|
+
const payload = {
|
|
6321
|
+
name,
|
|
6322
|
+
label: definition.label,
|
|
6323
|
+
type: definition.type,
|
|
6324
|
+
fieldType: definition.fieldType,
|
|
6325
|
+
groupName: lastSegment(parseAddress(group.$ref).path)
|
|
6326
|
+
};
|
|
6327
|
+
if ("description" in definition) payload.description = definition.description;
|
|
6328
|
+
if (Array.isArray(definition.options)) payload.options = definition.options.map((option, index) => ({
|
|
6329
|
+
...option,
|
|
6330
|
+
displayOrder: index
|
|
6331
|
+
}));
|
|
6332
|
+
else if (definition.type === "bool" && definition.fieldType === "booleancheckbox") payload.options = BOOLEAN_OPTIONS.map((option) => ({ ...option }));
|
|
6333
|
+
for (const field of CREATE_FIELDS) if (field in definition) payload[field] = definition[field];
|
|
6334
|
+
return payload;
|
|
5565
6335
|
}
|
|
5566
|
-
/**
|
|
5567
|
-
|
|
5568
|
-
|
|
6336
|
+
/** The fields a create sends as the definition states them, after name, label, type, fieldType, group and options. */
|
|
6337
|
+
const CREATE_FIELDS = [
|
|
6338
|
+
"hasUniqueValue",
|
|
6339
|
+
"formField",
|
|
6340
|
+
"hidden",
|
|
6341
|
+
"displayOrder",
|
|
6342
|
+
"numberDisplayHint",
|
|
6343
|
+
"showCurrencySymbol",
|
|
6344
|
+
"currencyPropertyName",
|
|
6345
|
+
"textDisplayHint",
|
|
6346
|
+
"calculationFormula",
|
|
6347
|
+
"dataSensitivity",
|
|
6348
|
+
"externalOptions",
|
|
6349
|
+
"referencedObjectType"
|
|
6350
|
+
];
|
|
6351
|
+
/** The two options HubSpot requires on a `bool` property, labelled as HubSpot's UI labels them. */
|
|
6352
|
+
const BOOLEAN_OPTIONS = [{
|
|
6353
|
+
label: "Yes",
|
|
6354
|
+
value: "true",
|
|
6355
|
+
displayOrder: 0,
|
|
6356
|
+
hidden: false
|
|
6357
|
+
}, {
|
|
6358
|
+
label: "No",
|
|
6359
|
+
value: "false",
|
|
6360
|
+
displayOrder: 1,
|
|
6361
|
+
hidden: false
|
|
6362
|
+
}];
|
|
6363
|
+
function lastSegment(path) {
|
|
6364
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
5569
6365
|
}
|
|
5570
6366
|
/** The classes a person settles: config and HubSpot moved apart, or never agreed. */
|
|
5571
6367
|
const REVERTING = /* @__PURE__ */ new Set([
|
|
@@ -5584,12 +6380,24 @@ const FIELD_END = /[.[]/;
|
|
|
5584
6380
|
/**
|
|
5585
6381
|
* What an update may write, by the unit's field, from HubSpot's documented update schema as live runs confirmed it
|
|
5586
6382
|
* (docs/hubspot.md): a property its label, description, group (sent as groupName), formField, fieldType, options,
|
|
5587
|
-
* hidden, displayOrder, number and text display fields and calculation formula; a group its label
|
|
5588
|
-
*
|
|
6383
|
+
* hidden, displayOrder, number and text display fields and calculation formula; a group its label; a pipeline its
|
|
6384
|
+
* label, displayOrder and the order of its stages; a stage its label and its metadata field. A custom object schema is
|
|
6385
|
+
* compared and never written in this release.
|
|
5589
6386
|
*/
|
|
5590
6387
|
const WRITABLE = {
|
|
5591
6388
|
object: /* @__PURE__ */ new Set(),
|
|
5592
6389
|
group: /* @__PURE__ */ new Set(["label"]),
|
|
6390
|
+
pipeline: /* @__PURE__ */ new Set([
|
|
6391
|
+
"label",
|
|
6392
|
+
"displayOrder",
|
|
6393
|
+
"stages"
|
|
6394
|
+
]),
|
|
6395
|
+
stage: /* @__PURE__ */ new Set([
|
|
6396
|
+
"label",
|
|
6397
|
+
"probability",
|
|
6398
|
+
"ticketState",
|
|
6399
|
+
"state"
|
|
6400
|
+
]),
|
|
5593
6401
|
property: /* @__PURE__ */ new Set([
|
|
5594
6402
|
"label",
|
|
5595
6403
|
"description",
|
|
@@ -5606,8 +6414,24 @@ const WRITABLE = {
|
|
|
5606
6414
|
"calculationFormula"
|
|
5607
6415
|
])
|
|
5608
6416
|
};
|
|
5609
|
-
/**
|
|
5610
|
-
|
|
6417
|
+
/**
|
|
6418
|
+
* Units whose change rewrites values HubSpot holds on records, which plan does not check, or changes how existing
|
|
6419
|
+
* records count (a stage's probability or closed state moves forecasts and open and closed reports): a step that sets
|
|
6420
|
+
* one is risky.
|
|
6421
|
+
*/
|
|
6422
|
+
const REVALUES = /* @__PURE__ */ new Set([
|
|
6423
|
+
"fieldType",
|
|
6424
|
+
"calculationFormula",
|
|
6425
|
+
"probability",
|
|
6426
|
+
"ticketState",
|
|
6427
|
+
"state"
|
|
6428
|
+
]);
|
|
6429
|
+
/** An ID made of digits alone: one HubSpot assigned, as to a pipeline or stage made in the HubSpot UI. */
|
|
6430
|
+
const ASSIGNED = /^\d+$/;
|
|
6431
|
+
/** Whether Kalup writes the pipelines of an object: deals, tickets and custom objects. */
|
|
6432
|
+
function writesPipelines(object) {
|
|
6433
|
+
return stageField(object, !STANDARD_OBJECTS.has(object)) !== void 0;
|
|
6434
|
+
}
|
|
5611
6435
|
/**
|
|
5612
6436
|
* What a classified unit becomes. `converged` agrees; `config-change`, `add` and `remove` are written; `keep` is kept
|
|
5613
6437
|
* with a note. `drift` and `conflict` are held unless `--take config` selected the unit (`taken`) or the target
|
|
@@ -5638,7 +6462,7 @@ function deriveChange(unit, policy, taken) {
|
|
|
5638
6462
|
function stepRisk(step, context) {
|
|
5639
6463
|
if (step.risk === "blocked" || step.action === "unknown") return "blocked";
|
|
5640
6464
|
switch (step.action) {
|
|
5641
|
-
case "create": return context.owner ? "risky" : "safe";
|
|
6465
|
+
case "create": return context.owner || assignedId(step) ? "risky" : "safe";
|
|
5642
6466
|
case "delete": return "destructive";
|
|
5643
6467
|
case "manual": return "manual";
|
|
5644
6468
|
case "release": return "safe";
|
|
@@ -5663,6 +6487,15 @@ function stepLabels(step, context) {
|
|
|
5663
6487
|
if ((step.changes ?? []).some((c) => c.op === "remove" && context.takeoverUnits?.has(c.unit))) labels.push("takeover");
|
|
5664
6488
|
return labels;
|
|
5665
6489
|
}
|
|
6490
|
+
/**
|
|
6491
|
+
* Whether a pipeline or stage create sends an ID HubSpot assigned in another portal, all digits, for itself or a stage
|
|
6492
|
+
* it carries: on a portal that holds the same pipeline under other IDs it makes a copy, so it is risky.
|
|
6493
|
+
*/
|
|
6494
|
+
function assignedId(step) {
|
|
6495
|
+
const type = step.address.slice(0, step.address.indexOf(":"));
|
|
6496
|
+
const ids = [step.address, ...(step.stages ?? []).map((st) => st.address)].map((a) => a.slice(a.lastIndexOf("/") + 1));
|
|
6497
|
+
return step.action === "create" && (type === "pipeline" || type === "stage") && ids.some((id) => ASSIGNED.test(id));
|
|
6498
|
+
}
|
|
5666
6499
|
/** The field a unit belongs to: `options` for an option member, a member's field and `options.order`. */
|
|
5667
6500
|
function fieldOf(unit) {
|
|
5668
6501
|
const at = unit.search(FIELD_END);
|
|
@@ -5731,77 +6564,79 @@ function deleteBlock(meta, members) {
|
|
|
5731
6564
|
fix: "move them to another group or delete them first; HubSpot archives a group only once every property in it is archived"
|
|
5732
6565
|
};
|
|
5733
6566
|
}
|
|
5734
|
-
|
|
5735
|
-
|
|
6567
|
+
/**
|
|
6568
|
+
* Whether a stage's values leave it closed: a ticket or custom object stage set or created CLOSED. Apply runs such a
|
|
6569
|
+
* stage step before the pipeline's other stage steps, so a ticket pipeline always keeps a closed stage.
|
|
6570
|
+
*/
|
|
6571
|
+
function closesStage(values) {
|
|
6572
|
+
return values?.ticketState === "CLOSED" || values?.state === "CLOSED";
|
|
5736
6573
|
}
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
6574
|
+
/**
|
|
6575
|
+
* `resources` as the steps `before` a stage delete leave them: each stage those steps create added to its pipeline's
|
|
6576
|
+
* order, and each stage field they write set. A delete runs only once every step before it is done, so the stage delete
|
|
6577
|
+
* rule counts what they leave.
|
|
6578
|
+
*/
|
|
6579
|
+
function afterSteps(resources, before) {
|
|
6580
|
+
const out = { ...resources };
|
|
6581
|
+
const at = (address) => Object.hasOwn(out, address) ? out[address] : void 0;
|
|
6582
|
+
for (const step of before) {
|
|
6583
|
+
if (!((step.action === "create" || step.action === "update" || step.action === "adopt") && step.address.startsWith("stage:"))) continue;
|
|
6584
|
+
const pipeline = `pipeline:${step.address.slice(6, step.address.lastIndexOf("/"))}`;
|
|
6585
|
+
const id = step.address.slice(step.address.lastIndexOf("/") + 1);
|
|
6586
|
+
const held = at(pipeline);
|
|
6587
|
+
const stages = held?.definition?.stages ?? [];
|
|
6588
|
+
if (held && !stages.includes(id)) out[pipeline] = {
|
|
6589
|
+
...held,
|
|
6590
|
+
definition: {
|
|
6591
|
+
...held.definition,
|
|
6592
|
+
stages: [...stages, id]
|
|
6593
|
+
}
|
|
6594
|
+
};
|
|
6595
|
+
const written = step.action === "create" ? step.desired ?? {} : Object.fromEntries((step.changes ?? []).map((c) => [c.unit, c.after]));
|
|
6596
|
+
const current = at(step.address);
|
|
6597
|
+
out[step.address] = {
|
|
6598
|
+
type: "stage",
|
|
6599
|
+
managed: true,
|
|
6600
|
+
...current,
|
|
6601
|
+
definition: {
|
|
6602
|
+
...current?.definition,
|
|
6603
|
+
...written
|
|
6604
|
+
}
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
return out;
|
|
5740
6608
|
}
|
|
5741
6609
|
/**
|
|
5742
|
-
*
|
|
5743
|
-
*
|
|
5744
|
-
*
|
|
6610
|
+
* Why a stage cannot be deleted, or undefined when it can: HubSpot refuses to leave a pipeline with no stage, or a
|
|
6611
|
+
* ticket pipeline with no closed stage (observed 2026-10-05). `resources` holds the pipeline and its stages as a read
|
|
6612
|
+
* found them and the steps before the delete leave them (afterSteps); `gone` the IDs of the stages of that pipeline
|
|
6613
|
+
* deleted before this one. `cli` names the command in the fix.
|
|
5745
6614
|
*/
|
|
5746
|
-
function
|
|
5747
|
-
const
|
|
5748
|
-
const
|
|
5749
|
-
|
|
5750
|
-
const
|
|
5751
|
-
|
|
5752
|
-
if (
|
|
5753
|
-
|
|
5754
|
-
|
|
6615
|
+
function stageDeleteRule(resources, stage, gone, cli) {
|
|
6616
|
+
const pipeline = `pipeline:${stage.slice(6, stage.lastIndexOf("/"))}`;
|
|
6617
|
+
const id = stage.slice(stage.lastIndexOf("/") + 1);
|
|
6618
|
+
const at = (address) => Object.hasOwn(resources, address) ? resources[address] : void 0;
|
|
6619
|
+
const ids = (at(pipeline)?.definition?.stages ?? []).filter((other) => other !== id && !gone.has(other));
|
|
6620
|
+
const fix = `run ${cli} rm on ${pipeline} to delete the whole pipeline, or keep the stage: set its tombstone's action to release`;
|
|
6621
|
+
if (ids.length === 0) return {
|
|
6622
|
+
short: "last stage",
|
|
6623
|
+
detail: "it is the last stage of its pipeline, and HubSpot keeps one",
|
|
6624
|
+
fix
|
|
5755
6625
|
};
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
label: definition.label,
|
|
5763
|
-
type: definition.type,
|
|
5764
|
-
fieldType: definition.fieldType,
|
|
5765
|
-
groupName: lastSegment(parseAddress(group.$ref).path)
|
|
6626
|
+
const prefix = `${pipeline.replace("pipeline:", "stage:")}/`;
|
|
6627
|
+
const closed = (other) => at(`${prefix}${other}`)?.definition?.ticketState === "CLOSED";
|
|
6628
|
+
if (at(stage)?.definition?.ticketState === "CLOSED" && !ids.some(closed)) return {
|
|
6629
|
+
short: "last closed stage",
|
|
6630
|
+
detail: "it is the last closed stage of its ticket pipeline, and HubSpot keeps one",
|
|
6631
|
+
fix: "mark another stage ticketState: 'CLOSED' first"
|
|
5766
6632
|
};
|
|
5767
|
-
if ("description" in definition) payload.description = definition.description;
|
|
5768
|
-
if (Array.isArray(definition.options)) payload.options = definition.options.map((option, index) => ({
|
|
5769
|
-
...option,
|
|
5770
|
-
displayOrder: index
|
|
5771
|
-
}));
|
|
5772
|
-
else if (definition.type === "bool" && definition.fieldType === "booleancheckbox") payload.options = BOOLEAN_OPTIONS.map((option) => ({ ...option }));
|
|
5773
|
-
for (const field of CREATE_FIELDS) if (field in definition) payload[field] = definition[field];
|
|
5774
|
-
return payload;
|
|
5775
6633
|
}
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
"
|
|
5782
|
-
"numberDisplayHint",
|
|
5783
|
-
"showCurrencySymbol",
|
|
5784
|
-
"currencyPropertyName",
|
|
5785
|
-
"textDisplayHint",
|
|
5786
|
-
"calculationFormula",
|
|
5787
|
-
"dataSensitivity",
|
|
5788
|
-
"externalOptions",
|
|
5789
|
-
"referencedObjectType"
|
|
5790
|
-
];
|
|
5791
|
-
/** The two options HubSpot requires on a `bool` property, labelled as HubSpot's UI labels them. */
|
|
5792
|
-
const BOOLEAN_OPTIONS = [{
|
|
5793
|
-
label: "Yes",
|
|
5794
|
-
value: "true",
|
|
5795
|
-
displayOrder: 0,
|
|
5796
|
-
hidden: false
|
|
5797
|
-
}, {
|
|
5798
|
-
label: "No",
|
|
5799
|
-
value: "false",
|
|
5800
|
-
displayOrder: 1,
|
|
5801
|
-
hidden: false
|
|
5802
|
-
}];
|
|
5803
|
-
function lastSegment(path) {
|
|
5804
|
-
return path.slice(path.lastIndexOf("/") + 1);
|
|
6634
|
+
function classOf(change, context) {
|
|
6635
|
+
return context.classes?.[change.unit] ?? change.class;
|
|
6636
|
+
}
|
|
6637
|
+
function revertsByTake(change, context) {
|
|
6638
|
+
const cls = classOf(change, context);
|
|
6639
|
+
return REVERTING.has(cls) && !(context.drift === "overwrite" && cls !== "diverged");
|
|
5805
6640
|
}
|
|
5806
6641
|
const MEMBER$1 = /^options\[(.*)\](?:\.(label|hidden|description))?$/s;
|
|
5807
6642
|
/**
|
|
@@ -5889,6 +6724,67 @@ function optionsPatch(changes, live, desired = []) {
|
|
|
5889
6724
|
displayOrder
|
|
5890
6725
|
}));
|
|
5891
6726
|
}
|
|
6727
|
+
/** A stage's metadata as HubSpot takes it: the field config states, as a string. */
|
|
6728
|
+
function stageMetadata(fields) {
|
|
6729
|
+
const out = {};
|
|
6730
|
+
for (const field of STAGE_FIELDS) if (fields[field] !== void 0) out[field] = String(fields[field]);
|
|
6731
|
+
return out;
|
|
6732
|
+
}
|
|
6733
|
+
/**
|
|
6734
|
+
* The create body of a pipeline step: its ID, label and displayOrder, and every stage it carries, in display order,
|
|
6735
|
+
* each with its ID, its index as displayOrder and its metadata. HubSpot refuses a pipeline without a stage.
|
|
6736
|
+
*/
|
|
6737
|
+
function pipelineCreateBody(step, pipelineId) {
|
|
6738
|
+
const desired = step.desired ?? {};
|
|
6739
|
+
return {
|
|
6740
|
+
pipelineId,
|
|
6741
|
+
label: desired.label,
|
|
6742
|
+
displayOrder: desired.displayOrder,
|
|
6743
|
+
stages: (step.stages ?? []).map((st, displayOrder) => ({
|
|
6744
|
+
stageId: st.address.slice(st.address.lastIndexOf("/") + 1),
|
|
6745
|
+
label: st.desired.label,
|
|
6746
|
+
displayOrder,
|
|
6747
|
+
metadata: stageMetadata(st.desired)
|
|
6748
|
+
}))
|
|
6749
|
+
};
|
|
6750
|
+
}
|
|
6751
|
+
/** The create body of a stage step: its ID, label and metadata, at `displayOrder`, a free slot after every stage. */
|
|
6752
|
+
function stageCreateBody(desired, stageId, displayOrder) {
|
|
6753
|
+
return {
|
|
6754
|
+
stageId,
|
|
6755
|
+
label: desired.label,
|
|
6756
|
+
displayOrder,
|
|
6757
|
+
metadata: stageMetadata(desired)
|
|
6758
|
+
};
|
|
6759
|
+
}
|
|
6760
|
+
/** The PATCH body of a pipeline step: its approved label and displayOrder. Stage order is written stage by stage. */
|
|
6761
|
+
function pipelinePatch(changes) {
|
|
6762
|
+
const body = {};
|
|
6763
|
+
for (const change of changes) if (change.unit === "label" || change.unit === "displayOrder") body[change.unit] = change.after;
|
|
6764
|
+
return body;
|
|
6765
|
+
}
|
|
6766
|
+
/** The PATCH body of a stage step: its approved label, and its metadata field, which HubSpot merges into the rest. */
|
|
6767
|
+
function stagePatch(changes) {
|
|
6768
|
+
const body = {};
|
|
6769
|
+
const fields = {};
|
|
6770
|
+
for (const change of changes) if (change.unit === "label") body.label = change.after;
|
|
6771
|
+
else fields[change.unit] = change.after;
|
|
6772
|
+
const metadata = stageMetadata(fields);
|
|
6773
|
+
return Object.keys(metadata).length > 0 ? {
|
|
6774
|
+
...body,
|
|
6775
|
+
metadata
|
|
6776
|
+
} : body;
|
|
6777
|
+
}
|
|
6778
|
+
/**
|
|
6779
|
+
* The stage order a write leaves: every live stage, the approved order's stages in that order, each in a slot one of
|
|
6780
|
+
* them holds, so a stage the order leaves out keeps its place among its neighbours. Stages the approved order names and
|
|
6781
|
+
* HubSpot does not hold are left out.
|
|
6782
|
+
*/
|
|
6783
|
+
function stageOrder(live, approved) {
|
|
6784
|
+
const queue = approved.filter((id) => live.includes(id));
|
|
6785
|
+
const placed = new Set(queue);
|
|
6786
|
+
return live.map((id) => placed.has(id) ? queue.shift() : id);
|
|
6787
|
+
}
|
|
5892
6788
|
/** The option values a step's approved changes remove. */
|
|
5893
6789
|
function removedValues(changes = []) {
|
|
5894
6790
|
return changes.filter((c) => c.op === "remove").flatMap((c) => {
|
|
@@ -5951,6 +6847,17 @@ const CAPTURED = {
|
|
|
5951
6847
|
"secondaryDisplayProperties"
|
|
5952
6848
|
],
|
|
5953
6849
|
group: ["label"],
|
|
6850
|
+
pipeline: [
|
|
6851
|
+
"label",
|
|
6852
|
+
"displayOrder",
|
|
6853
|
+
"stages"
|
|
6854
|
+
],
|
|
6855
|
+
stage: [
|
|
6856
|
+
"label",
|
|
6857
|
+
"probability",
|
|
6858
|
+
"ticketState",
|
|
6859
|
+
"state"
|
|
6860
|
+
],
|
|
5954
6861
|
property: PROPERTY_FIELDS.filter((field) => field !== "options"),
|
|
5955
6862
|
unsupported: [
|
|
5956
6863
|
"label",
|
|
@@ -5999,10 +6906,36 @@ function objectOf(address) {
|
|
|
5999
6906
|
const { type, path } = parseAddress(address);
|
|
6000
6907
|
return type === "object" ? path : path.slice(0, path.indexOf("/"));
|
|
6001
6908
|
}
|
|
6909
|
+
/** The path after the object: a stage's is `<pipeline>/<stage>`; see stageIdOf for its own ID. */
|
|
6002
6910
|
function nameOf(address) {
|
|
6003
6911
|
const { type, path } = parseAddress(address);
|
|
6004
6912
|
return type === "object" ? path : path.slice(path.indexOf("/") + 1);
|
|
6005
6913
|
}
|
|
6914
|
+
/** The pipeline a stage address is under, `pipeline:<k>/<p>`. */
|
|
6915
|
+
function pipelineOf(stage) {
|
|
6916
|
+
return `pipeline:${stage.slice(6, stage.lastIndexOf("/"))}`;
|
|
6917
|
+
}
|
|
6918
|
+
/** A pipeline's or stage's own ID: the last segment of its address. */
|
|
6919
|
+
function ownId(address) {
|
|
6920
|
+
return address.slice(address.lastIndexOf("/") + 1);
|
|
6921
|
+
}
|
|
6922
|
+
/** The name a title gives a resource: a stage's own ID, otherwise the name in its address. */
|
|
6923
|
+
function shownName(address) {
|
|
6924
|
+
return parseAddress(address).type === "stage" ? ownId(address) : nameOf(address);
|
|
6925
|
+
}
|
|
6926
|
+
/** Where a title places a resource after its name: its pipeline and object for a stage, its object for the rest. */
|
|
6927
|
+
function placeOf(address) {
|
|
6928
|
+
const { type } = parseAddress(address);
|
|
6929
|
+
if (type === "object") return "";
|
|
6930
|
+
return `${type === "stage" ? ` of pipeline ${ownId(pipelineOf(address))}` : ""} on ${objectOf(address)}`;
|
|
6931
|
+
}
|
|
6932
|
+
/** How a title words a field a step sets: a field whose change rewrites or recounts record values says so. */
|
|
6933
|
+
function fieldWords(unit) {
|
|
6934
|
+
if (unit === "stages") return "the stage order";
|
|
6935
|
+
return REVALUES.has(unit) ? `${unit} (the effect on existing values is not checked)` : unit;
|
|
6936
|
+
}
|
|
6937
|
+
/** What a title adds to the delete of a pipeline or stage: HubSpot purges both (observed 2026-10-01). */
|
|
6938
|
+
const PURGED = "; it cannot be restored";
|
|
6006
6939
|
/**
|
|
6007
6940
|
* What an update or adopt step writes, in words, as the end of its title: `, set label, relabel option "north", add
|
|
6008
6941
|
* option "Paused"`. `option` gives an added or removed option's words, `field` a set field's. The units stay in the
|
|
@@ -6129,9 +7062,21 @@ const NOT_CAPTURED = {
|
|
|
6129
7062
|
"modificationMetadata",
|
|
6130
7063
|
"sensitiveDataCategories",
|
|
6131
7064
|
"updatedAt",
|
|
6132
|
-
"updatedUserId"
|
|
7065
|
+
"updatedUserId"
|
|
7066
|
+
],
|
|
7067
|
+
group: ["displayOrder"],
|
|
7068
|
+
pipeline: [
|
|
7069
|
+
"archived",
|
|
7070
|
+
"createdAt",
|
|
7071
|
+
"updatedAt"
|
|
7072
|
+
],
|
|
7073
|
+
stage: [
|
|
7074
|
+
"archived",
|
|
7075
|
+
"createdAt",
|
|
7076
|
+
"isClosed",
|
|
7077
|
+
"updatedAt",
|
|
7078
|
+
"writePermissions"
|
|
6133
7079
|
],
|
|
6134
|
-
group: ["displayOrder"],
|
|
6135
7080
|
object: [
|
|
6136
7081
|
"allowsSensitiveProperties",
|
|
6137
7082
|
"associations",
|
|
@@ -6148,8 +7093,12 @@ const NOT_CAPTURED = {
|
|
|
6148
7093
|
const OBSERVED_TYPES = /* @__PURE__ */ new Set([
|
|
6149
7094
|
"object",
|
|
6150
7095
|
"group",
|
|
6151
|
-
"property"
|
|
7096
|
+
"property",
|
|
7097
|
+
"pipeline",
|
|
7098
|
+
"stage"
|
|
6152
7099
|
]);
|
|
7100
|
+
/** The resource types read from an object's pipelines list. */
|
|
7101
|
+
const PIPELINE_TYPES = /* @__PURE__ */ new Set(["pipeline", "stage"]);
|
|
6153
7102
|
const NAME_FIX = "rename it in HubSpot to a name without spaces";
|
|
6154
7103
|
/**
|
|
6155
7104
|
* The config IR as the side of a comparison, as target `target` sees it when one is given: its definition overrides
|
|
@@ -6189,13 +7138,14 @@ function observePortal(portal, loaded, targetName, issues) {
|
|
|
6189
7138
|
resources,
|
|
6190
7139
|
meta
|
|
6191
7140
|
}, issues) : unread$2(key, portal),
|
|
7141
|
+
pipelines: live ? pipelineCoverage(live, portal, resources, issues) : void 0,
|
|
6192
7142
|
shadowed: nonEmpty([...new Set(portal.shadowed.filter(under).map(nameOf))].sort(byCodeUnit)),
|
|
6193
7143
|
excluded: nonEmpty(portal.excluded.filter(under)),
|
|
6194
7144
|
renamed: renamed(target.overrides ?? {}, under)
|
|
6195
7145
|
});
|
|
6196
7146
|
}
|
|
6197
7147
|
const coverage = {
|
|
6198
|
-
complete: Object.values(objects).every((o) => o.status !== "unreadable" && o.unaddressable === void 0),
|
|
7148
|
+
complete: Object.values(objects).every((o) => o.status !== "unreadable" && o.unaddressable === void 0 && o.pipelines?.status !== "unreadable"),
|
|
6199
7149
|
objects,
|
|
6200
7150
|
otherObjects: portal.customObjects === void 0 ? "unknown" : [...portal.otherObjects].sort(byCodeUnit),
|
|
6201
7151
|
notCaptured: NOT_CAPTURED
|
|
@@ -6230,18 +7180,27 @@ function statusOf(observation, address) {
|
|
|
6230
7180
|
const object = Object.hasOwn(coverage.objects, key) ? coverage.objects[key] : void 0;
|
|
6231
7181
|
if (!(object && OBSERVED_TYPES.has(type))) return "not-observed";
|
|
6232
7182
|
if (object.status !== "read") return object.status;
|
|
7183
|
+
if (PIPELINE_TYPES.has(type)) return pipelineStatus(object, address, held);
|
|
6233
7184
|
if (type === "object") {
|
|
6234
7185
|
if (object.unsupportedSchema) return "unsupported";
|
|
6235
7186
|
return held ? "present" : "absent";
|
|
6236
7187
|
}
|
|
6237
7188
|
if (object.excluded?.includes(address)) return "excluded";
|
|
6238
7189
|
if (held) return "present";
|
|
7190
|
+
return type === "property" ? propertyStatus(object, address) : "absent";
|
|
7191
|
+
}
|
|
7192
|
+
function propertyStatus(object, address) {
|
|
6239
7193
|
const name = nameOf(address);
|
|
6240
|
-
if (type !== "property") return "absent";
|
|
6241
7194
|
if (object.unsupported?.some((u) => u.name === name)) return "unsupported";
|
|
6242
7195
|
if (object.unaddressable?.includes(name)) return "unreadable";
|
|
6243
7196
|
return !(object.renamed && Object.hasOwn(object.renamed, address)) && object.outOfScope?.includes(name) ? "excluded" : "absent";
|
|
6244
7197
|
}
|
|
7198
|
+
function pipelineStatus(object, address, held) {
|
|
7199
|
+
if (object.pipelines === void 0) return "not-observed";
|
|
7200
|
+
if (object.pipelines.status !== "read") return "unreadable";
|
|
7201
|
+
if (object.excluded?.includes(address)) return "excluded";
|
|
7202
|
+
return held ? "present" : "absent";
|
|
7203
|
+
}
|
|
6245
7204
|
/** The type ID of each custom object the observation saw exist, by config key. */
|
|
6246
7205
|
function objectTypeIds(observation) {
|
|
6247
7206
|
const out = {};
|
|
@@ -6323,6 +7282,51 @@ function unsupportedProperty(object, u) {
|
|
|
6323
7282
|
group: { $ref: `group:${object}/${u.group}` }
|
|
6324
7283
|
};
|
|
6325
7284
|
}
|
|
7285
|
+
function pipelineCoverage(live, portal, resources, issues) {
|
|
7286
|
+
const { object } = live;
|
|
7287
|
+
if (live.pipelines === void 0) {
|
|
7288
|
+
const gap = portal.gaps.find((g) => g.object === object && g.list === "pipelines");
|
|
7289
|
+
return gap ? {
|
|
7290
|
+
status: "unreadable",
|
|
7291
|
+
missingScope: gap.scope
|
|
7292
|
+
} : void 0;
|
|
7293
|
+
}
|
|
7294
|
+
const held = (id) => isAddress(`pipeline:${object}/${id}`) && !id.includes("/");
|
|
7295
|
+
const leave = (what, id) => issues.push({
|
|
7296
|
+
code: "W_UNADDRESSABLE_NAME",
|
|
7297
|
+
message: `${what} '${sanitize(id)}' on ${object} has an ID no address can hold, so it is not captured`,
|
|
7298
|
+
fix: "leave it out of config; HubSpot never changes a pipeline or stage ID"
|
|
7299
|
+
});
|
|
7300
|
+
for (const p of live.pipelines) {
|
|
7301
|
+
if (!held(p.id)) {
|
|
7302
|
+
leave("pipeline", p.id);
|
|
7303
|
+
continue;
|
|
7304
|
+
}
|
|
7305
|
+
const stages = p.stages.filter((st) => {
|
|
7306
|
+
if (!held(st.id)) leave("stage", st.id);
|
|
7307
|
+
return held(st.id);
|
|
7308
|
+
});
|
|
7309
|
+
const definition = {
|
|
7310
|
+
label: p.label,
|
|
7311
|
+
displayOrder: p.displayOrder,
|
|
7312
|
+
stages: stages.map((st) => st.id)
|
|
7313
|
+
};
|
|
7314
|
+
resources.push([`pipeline:${object}/${p.id}`, {
|
|
7315
|
+
type: "pipeline",
|
|
7316
|
+
managed: true,
|
|
7317
|
+
definition
|
|
7318
|
+
}]);
|
|
7319
|
+
for (const st of stages) {
|
|
7320
|
+
const { id, ...fields } = st;
|
|
7321
|
+
resources.push([`stage:${object}/${p.id}/${id}`, {
|
|
7322
|
+
type: "stage",
|
|
7323
|
+
managed: true,
|
|
7324
|
+
definition: fields
|
|
7325
|
+
}]);
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
return { status: "read" };
|
|
7329
|
+
}
|
|
6326
7330
|
function unread$2(key, portal) {
|
|
6327
7331
|
if (portal.excluded.includes(`object:${key}`)) return { status: "excluded" };
|
|
6328
7332
|
if (portal.absent.includes(key)) return { status: "absent" };
|
|
@@ -6342,6 +7346,25 @@ function nonEmpty(list) {
|
|
|
6342
7346
|
function compact$3(value) {
|
|
6343
7347
|
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
6344
7348
|
}
|
|
7349
|
+
const pinWarningDays = 90;
|
|
7350
|
+
/** One warning per API family whose pin expires within 90 days. `expires` is a month; its first day counts. */
|
|
7351
|
+
function pinWarnings(rows, now = Date.now()) {
|
|
7352
|
+
const out = [];
|
|
7353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7354
|
+
for (const row of rows) {
|
|
7355
|
+
if (row.expires === void 0) continue;
|
|
7356
|
+
const pin = `${row.family} ${row.version}`;
|
|
7357
|
+
const expires = /* @__PURE__ */ new Date(`${row.expires}-01T00:00:00Z`);
|
|
7358
|
+
if (seen.has(pin) || expires.getTime() - now > pinWarningDays * 864e5) continue;
|
|
7359
|
+
seen.add(pin);
|
|
7360
|
+
out.push({
|
|
7361
|
+
code: "W_PIN_EXPIRES",
|
|
7362
|
+
message: `the ${row.family} API pin ${row.version} expires ${row.expires}`,
|
|
7363
|
+
fix: `upgrade ${bin} to a release that pins a newer version`
|
|
7364
|
+
});
|
|
7365
|
+
}
|
|
7366
|
+
return out;
|
|
7367
|
+
}
|
|
6345
7368
|
/** The account types a target is not protected on unless config says so: test portals, sandboxes, app developers. */
|
|
6346
7369
|
const UNPROTECTED = /* @__PURE__ */ new Set([
|
|
6347
7370
|
"DEVELOPER_TEST",
|
|
@@ -6399,6 +7422,30 @@ async function preflight(http, request) {
|
|
|
6399
7422
|
} : overall);
|
|
6400
7423
|
}));
|
|
6401
7424
|
}
|
|
7425
|
+
if (request.pipelines) {
|
|
7426
|
+
const observed = new Set(Object.values(request.objectTypeIds));
|
|
7427
|
+
limits.push(await reading(registry.pipeline.limitKey, async () => {
|
|
7428
|
+
const body = await http.request({
|
|
7429
|
+
type: "limits",
|
|
7430
|
+
path: "pipelines"
|
|
7431
|
+
});
|
|
7432
|
+
const custom = body?.customObjectTypes;
|
|
7433
|
+
const overall = figures(custom?.overallLimit, custom?.overallUsage);
|
|
7434
|
+
const listed = body?.hubspotDefinedObjectTypes;
|
|
7435
|
+
const byObjectType = (Array.isArray(listed) ? listed : []).flatMap((item) => {
|
|
7436
|
+
const { objectTypeId, limit, usage } = item ?? {};
|
|
7437
|
+
const entry = typeof objectTypeId === "string" && observed.has(objectTypeId) && figures(limit, usage);
|
|
7438
|
+
return entry ? [{
|
|
7439
|
+
objectTypeId,
|
|
7440
|
+
...entry
|
|
7441
|
+
}] : [];
|
|
7442
|
+
}).sort((a, b) => byCodeUnit(a.objectTypeId, b.objectTypeId));
|
|
7443
|
+
return overall && (byObjectType.length > 0 ? {
|
|
7444
|
+
...overall,
|
|
7445
|
+
byObjectType
|
|
7446
|
+
} : overall);
|
|
7447
|
+
}));
|
|
7448
|
+
}
|
|
6402
7449
|
return { limits };
|
|
6403
7450
|
}
|
|
6404
7451
|
/**
|
|
@@ -6414,6 +7461,7 @@ function headroom(limits, creates, objectTypeIds, target) {
|
|
|
6414
7461
|
blocked: {},
|
|
6415
7462
|
issues: []
|
|
6416
7463
|
};
|
|
7464
|
+
pipelineRoom(out, limits, creates, objectTypeIds, target);
|
|
6417
7465
|
const ofType = (type) => creates.filter((address) => parseAddress(address).type === type);
|
|
6418
7466
|
const objectTypes = readOf(limits, registry.object.limitKey);
|
|
6419
7467
|
if (objectTypes) check$2(out, ofType("object"), objectTypes, "custom objects", target);
|
|
@@ -6429,6 +7477,24 @@ function headroom(limits, creates, objectTypeIds, target) {
|
|
|
6429
7477
|
}
|
|
6430
7478
|
return out;
|
|
6431
7479
|
}
|
|
7480
|
+
/**
|
|
7481
|
+
* The room the pipelines reading leaves for pipeline creates: a standard object's against its own entry, a custom
|
|
7482
|
+
* object's against the overall custom object figures. Called by headroom.
|
|
7483
|
+
*/
|
|
7484
|
+
function pipelineRoom(out, limits, creates, ids, target) {
|
|
7485
|
+
const pipelines = readOf(limits, registry.pipeline.limitKey);
|
|
7486
|
+
if (!pipelines) return;
|
|
7487
|
+
const byObject = /* @__PURE__ */ new Map();
|
|
7488
|
+
for (const address of creates.filter((a) => parseAddress(a).type === "pipeline")) byObject.set(objectOf(address), [...byObject.get(objectOf(address)) ?? [], address]);
|
|
7489
|
+
const custom = [];
|
|
7490
|
+
for (const [key, list] of byObject) {
|
|
7491
|
+
const id = Object.hasOwn(ids, key) ? ids[key] : void 0;
|
|
7492
|
+
const entry = pipelines.byObjectType?.find((e) => e.objectTypeId === id);
|
|
7493
|
+
if (entry) check$2(out, list, entry, `pipelines on ${key}`, target);
|
|
7494
|
+
else if (!(id ?? "").startsWith("0-")) custom.push(...list);
|
|
7495
|
+
}
|
|
7496
|
+
check$2(out, custom, pipelines, "custom object pipelines", target);
|
|
7497
|
+
}
|
|
6432
7498
|
function check$2(out, creates, { limit, usage }, what, target) {
|
|
6433
7499
|
const room = limit - usage;
|
|
6434
7500
|
if (creates.length === 0 || room >= creates.length) return;
|
|
@@ -6620,16 +7686,37 @@ function schemaNames(definition) {
|
|
|
6620
7686
|
function own$3(record, key) {
|
|
6621
7687
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
6622
7688
|
}
|
|
7689
|
+
function indexPipelines(observation) {
|
|
7690
|
+
const out = {
|
|
7691
|
+
pipelines: /* @__PURE__ */ new Map(),
|
|
7692
|
+
stages: /* @__PURE__ */ new Map()
|
|
7693
|
+
};
|
|
7694
|
+
for (const address of Object.keys(observation.resources)) {
|
|
7695
|
+
const kind = kindOf$3(address);
|
|
7696
|
+
if (kind === "pipeline" || kind === "stage") {
|
|
7697
|
+
const by = out[kind === "pipeline" ? "pipelines" : "stages"];
|
|
7698
|
+
by.set(objectOf(address), [...by.get(objectOf(address)) ?? [], address]);
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
return out;
|
|
7702
|
+
}
|
|
6623
7703
|
const CREATED_FIRST = [
|
|
6624
7704
|
"label",
|
|
6625
7705
|
"group",
|
|
6626
7706
|
"fieldType"
|
|
6627
7707
|
];
|
|
6628
7708
|
const CREATE_ONLY = /* @__PURE__ */ new Set(["hasUniqueValue", "dataSensitivity"]);
|
|
7709
|
+
const HIDDEN_CREATED = /* @__PURE__ */ new Set([
|
|
7710
|
+
"type",
|
|
7711
|
+
"options",
|
|
7712
|
+
"stages"
|
|
7713
|
+
]);
|
|
6629
7714
|
const KINDS = [
|
|
6630
7715
|
"object",
|
|
6631
7716
|
"group",
|
|
6632
|
-
"property"
|
|
7717
|
+
"property",
|
|
7718
|
+
"pipeline",
|
|
7719
|
+
"stage"
|
|
6633
7720
|
];
|
|
6634
7721
|
const TRANSPORT = "public-api";
|
|
6635
7722
|
const SETS = /* @__PURE__ */ new Set(["requiredProperties", "searchableProperties"]);
|
|
@@ -6641,9 +7728,13 @@ const PROVENANCE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
6641
7728
|
]);
|
|
6642
7729
|
const NOT_COVERED = {
|
|
6643
7730
|
object: "Not copied, HubSpot has no API: record page layouts, saved views.",
|
|
6644
|
-
property: "Not copied, HubSpot has no API: conditional property logic, field-level permissions."
|
|
7731
|
+
property: "Not copied, HubSpot has no API: conditional property logic, field-level permissions.",
|
|
7732
|
+
pipeline: "Not copied, HubSpot has no API: stage required properties, conditional stage properties, pipeline automation, pipeline permissions."
|
|
6645
7733
|
};
|
|
6646
7734
|
const NO_SCHEMA_WRITES = "custom object schema writes are not supported in this release";
|
|
7735
|
+
function noPipelineWrites(object) {
|
|
7736
|
+
return `Kalup reads and compares the pipelines of ${object} and does not write them in this release`;
|
|
7737
|
+
}
|
|
6647
7738
|
const INCOMPLETE_FIX = "give the key the scopes the plan warns about, then plan again";
|
|
6648
7739
|
const TITLE_MAX$1 = 160;
|
|
6649
7740
|
const TEXT_MAX$2 = 400;
|
|
@@ -6756,6 +7847,7 @@ function planReads(input) {
|
|
|
6756
7847
|
limits: {
|
|
6757
7848
|
objectTypes: decided.steps.some((s) => s.action === "create" && s.risk !== "blocked" && kindOf$3(s.address) === "object"),
|
|
6758
7849
|
properties: creates.length > 0 || gone.some((g) => g.taken),
|
|
7850
|
+
pipelines: decided.steps.some((s) => s.action === "create" && s.risk !== "blocked" && kindOf$3(s.address) === "pipeline"),
|
|
6759
7851
|
objectTypeIds: typeIdsOf(coverage)
|
|
6760
7852
|
}
|
|
6761
7853
|
};
|
|
@@ -6816,11 +7908,13 @@ function stepLines(doc, step, project) {
|
|
|
6816
7908
|
const labels = step.labels?.length ? ` [${step.labels.join(", ")}]` : "";
|
|
6817
7909
|
const block = step.blocked ? [` ${step.blocked.detail}`, ...step.blocked.fix === void 0 ? [] : [` fix: ${step.blocked.fix}`]] : [];
|
|
6818
7910
|
const created = step.action === "create" && step.risk !== "blocked" ? createdLine(step.desired ?? {}) : [];
|
|
7911
|
+
const carriedLines = (step.risk === "blocked" ? [] : step.stages ?? []).map((st) => ` + stage ${show$2(st.desired.label)} (${show$2(ownId(st.address))})${stageFields(st.desired)}`);
|
|
6819
7912
|
return [
|
|
6820
7913
|
`${step.id} ${step.risk}${labels} ${step.title}`,
|
|
6821
7914
|
...created,
|
|
6822
|
-
...
|
|
6823
|
-
...(step.
|
|
7915
|
+
...carriedLines,
|
|
7916
|
+
...(step.changes ?? []).map((c) => ` ${c.unit === "stages" ? orderLine(step, c) : changeLine(c)}`),
|
|
7917
|
+
...(step.held ?? []).map((h) => h.unit === "stages" ? ` held stage order ${h.class}: ${heldOrder(step, h)}. ${exits(doc, step, h)}` : ` held ${h.unit} ${h.class}: ${heldValues(h)}. ${exits(doc, step, h)}`),
|
|
6824
7918
|
...sharedLine(doc, step, project),
|
|
6825
7919
|
...(step.notes ?? []).map((n) => ` note ${n.unit}: ${n.note}`),
|
|
6826
7920
|
...block
|
|
@@ -6833,6 +7927,26 @@ function changeLine(c) {
|
|
|
6833
7927
|
}
|
|
6834
7928
|
return `${c.unit}: ${shown$1(c.before)} -> ${shown$1(c.after)}`;
|
|
6835
7929
|
}
|
|
7930
|
+
function orderLine(step, c) {
|
|
7931
|
+
return `stage order: ${byLabel(step, c.before)} -> ${byLabel(step, c.after)}`;
|
|
7932
|
+
}
|
|
7933
|
+
function heldOrder(step, h) {
|
|
7934
|
+
const base = Object.hasOwn(h, "base") ? `, last agreed ${byLabel(step, h.base)}` : "";
|
|
7935
|
+
return `config ${byLabel(step, h.config)}, portal ${byLabel(step, h.live)}${base}`;
|
|
7936
|
+
}
|
|
7937
|
+
function byLabel(step, list) {
|
|
7938
|
+
const labels = step.stageLabels ?? {};
|
|
7939
|
+
const ids = Array.isArray(list) ? list : [];
|
|
7940
|
+
const shared = (label) => Object.values(labels).filter((l) => l === label).length > 1;
|
|
7941
|
+
return ids.map((id) => {
|
|
7942
|
+
const label = own$2(labels, id);
|
|
7943
|
+
if (label === void 0) return show$2(id);
|
|
7944
|
+
return shared(label) ? `${show$2(label)} (${id})` : show$2(label);
|
|
7945
|
+
}).join(", ");
|
|
7946
|
+
}
|
|
7947
|
+
function stageFields(desired) {
|
|
7948
|
+
return Object.entries(desired).filter(([field]) => field !== "label").map(([field, value]) => `, ${field} ${shown$1(value)}`).join("");
|
|
7949
|
+
}
|
|
6836
7950
|
function heldValues(h) {
|
|
6837
7951
|
const base = Object.hasOwn(h, "base") ? `, last agreed ${shown$1(h.base)}` : "";
|
|
6838
7952
|
return `config ${shown$1(h.config)}, portal ${shown$1(h.live)}${base}`;
|
|
@@ -6840,7 +7954,7 @@ function heldValues(h) {
|
|
|
6840
7954
|
function createdLine(desired) {
|
|
6841
7955
|
const options = desired.options;
|
|
6842
7956
|
const given = (field) => Object.hasOwn(desired, field) && desired[field] !== void 0;
|
|
6843
|
-
const rest = Object.keys(desired).filter((field) => !(CREATED_FIRST.includes(field) || field
|
|
7957
|
+
const rest = Object.keys(desired).filter((field) => !(CREATED_FIRST.includes(field) || HIDDEN_CREATED.has(field)) && given(field)).sort(byCodeUnit);
|
|
6844
7958
|
const fields = [...[...CREATED_FIRST.filter(given), ...rest].map((field) => `${field} ${shown$1(desired[field])}${CREATE_ONLY.has(field) ? " (create only)" : ""}`), ...options && options.length > 0 ? [`options ${options.map((o) => show$2(o.label)).join(", ")}`] : []];
|
|
6845
7959
|
return fields.length > 0 ? [` ${fields.join(", ")}`] : [];
|
|
6846
7960
|
}
|
|
@@ -6887,6 +8001,7 @@ function decide$1(input, coverage) {
|
|
|
6887
8001
|
matched: /* @__PURE__ */ new Set(),
|
|
6888
8002
|
missing: [],
|
|
6889
8003
|
overrides: settings.overrides ?? {},
|
|
8004
|
+
pipelines: indexPipelines(observation),
|
|
6890
8005
|
policy: {
|
|
6891
8006
|
drift,
|
|
6892
8007
|
allowDestroy,
|
|
@@ -6954,6 +8069,7 @@ function stepFor(context, address, resource, status) {
|
|
|
6954
8069
|
return blocked(address, actionOf(status), "dependency-blocked", detail, detail);
|
|
6955
8070
|
}
|
|
6956
8071
|
if (status === "unsupported") return unsupported(context, address);
|
|
8072
|
+
if (status === "absent" && kindOf$3(address) === "stage" && carried(context, address)) return;
|
|
6957
8073
|
const owner = ownerOf(context, address);
|
|
6958
8074
|
if (status === "absent" && owner.entry && kindOf$3(address) !== "object") return ownedAbsent(context, address, resource, override, owner.entry);
|
|
6959
8075
|
const group = missingGroup(context, resource);
|
|
@@ -6973,6 +8089,14 @@ function stepFor(context, address, resource, status) {
|
|
|
6973
8089
|
function unread$1(context, address, status) {
|
|
6974
8090
|
const key = objectOf(address);
|
|
6975
8091
|
const object = own$2(context.coverage.objects, key);
|
|
8092
|
+
const kind = kindOf$3(address);
|
|
8093
|
+
if ((kind === "pipeline" || kind === "stage") && object?.status === "read") {
|
|
8094
|
+
const scope = object.pipelines?.missingScope;
|
|
8095
|
+
if (object.pipelines === void 0) return blocked(address, "unknown", "scope", "pipelines not read", `the pipelines of ${key} are not in scope, so the plan did not read them`, `set objects.${key}.pipelines: true`);
|
|
8096
|
+
const detail = `the pipelines list of ${key} answered 403, so what the portal holds there is unknown`;
|
|
8097
|
+
const fix = scope === void 0 ? "check the scopes of the key" : `add the scope ${scope} to the key`;
|
|
8098
|
+
return blocked(address, "unknown", "scope", `the key cannot read the pipelines of ${key}`, detail, fix);
|
|
8099
|
+
}
|
|
6976
8100
|
if (object?.status === "read") return blocked(address, "unknown", "unsupported", "its portal group has no address", "the portal has it in a group whose name holds whitespace, which no address can hold, so what it holds is unknown", "rename the group in HubSpot to a name without spaces");
|
|
6977
8101
|
if (status === "not-observed") {
|
|
6978
8102
|
const detail = `${key} is not under objects in kalup.config.ts, so the plan did not read it`;
|
|
@@ -6985,7 +8109,11 @@ function unread$1(context, address, status) {
|
|
|
6985
8109
|
}
|
|
6986
8110
|
function blockedParents(context, address, resource) {
|
|
6987
8111
|
if (kindOf$3(address) === "object") return [];
|
|
6988
|
-
return [
|
|
8112
|
+
return [
|
|
8113
|
+
(resource.definition?.group)?.$ref,
|
|
8114
|
+
kindOf$3(address) === "stage" ? pipelineOf(address) : void 0,
|
|
8115
|
+
`object:${objectOf(address)}`
|
|
8116
|
+
].flatMap((parent) => {
|
|
6989
8117
|
const found = parent === void 0 ? void 0 : context.decided.get(parent);
|
|
6990
8118
|
return found?.risk === "blocked" ? [found] : [];
|
|
6991
8119
|
});
|
|
@@ -7010,6 +8138,7 @@ function absent(context, address, resource, override) {
|
|
|
7010
8138
|
const kind = kindOf$3(address);
|
|
7011
8139
|
const name = nameOf(address);
|
|
7012
8140
|
if (kind === "object") return blocked(address, "create", "unsupported", "schema writes not supported", `the portal has no custom object ${name}, and ${NO_SCHEMA_WRITES}`, `create it in HubSpot, or ${skipFix(address, input.target)}`);
|
|
8141
|
+
if (kind === "pipeline" || kind === "stage") return pipelineCreate(context, address, resource);
|
|
7013
8142
|
if (kind === "property" && own$2(input.archivedProperties, objectOf(address))?.some((p) => p.name === name)) {
|
|
7014
8143
|
const [detail, fix] = archivedName$1(name);
|
|
7015
8144
|
return blocked(address, "create", "unsupported", "archived name", detail, fix);
|
|
@@ -7022,6 +8151,100 @@ function absent(context, address, resource, override) {
|
|
|
7022
8151
|
expect: { exists: false }
|
|
7023
8152
|
};
|
|
7024
8153
|
}
|
|
8154
|
+
function pipelineCreate(context, address, resource) {
|
|
8155
|
+
const { target } = context.input;
|
|
8156
|
+
const object = objectOf(address);
|
|
8157
|
+
if (!writesPipelines(object)) {
|
|
8158
|
+
const fix = `create it in HubSpot and run ${bin} pull, or ${skipFix(address, target)}`;
|
|
8159
|
+
return blocked(address, "create", "unsupported", "pipelines not written", noPipelineWrites(object), fix);
|
|
8160
|
+
}
|
|
8161
|
+
const stages = kindOf$3(address) === "pipeline" ? carriedStages(context, address) : [];
|
|
8162
|
+
if (kindOf$3(address) === "pipeline" && stages.length === 0) return blocked(address, "create", "override", "every stage skipped", `every stage of ${address} is skipped on target ${target}, and HubSpot refuses a pipeline without one`, `skip ${address} too, or keep one of its stages on target ${target}`);
|
|
8163
|
+
for (const at of [address, ...stages.map((st) => st.address)]) {
|
|
8164
|
+
const holder = idHolder(context, at);
|
|
8165
|
+
if (holder) return blocked(address, "create", "unsupported", "ID taken", `${holder} holds the ID ${ownId(at)} in this portal, and HubSpot keeps ${kindOf$3(at)} IDs unique ${kindOf$3(at) === "pipeline" ? "across objects" : `across the pipelines of ${object}`}`, `give ${at} another ID in config`);
|
|
8166
|
+
if (at !== address && own$2(context.overrides, at)?.name) {
|
|
8167
|
+
const detail = `the pipeline is absent, and ${at} carries a name override, so no stage of that name can exist`;
|
|
8168
|
+
return blocked(address, "create", "override", detail, detail, `correct or remove the name override for ${at} under targets.${target}.overrides`);
|
|
8169
|
+
}
|
|
8170
|
+
}
|
|
8171
|
+
const notes = [
|
|
8172
|
+
...assignedNotes(context, address, stages),
|
|
8173
|
+
...unreadClash(context, address),
|
|
8174
|
+
...firstPipelineNote(context, address)
|
|
8175
|
+
];
|
|
8176
|
+
const what = stages.length > 0 ? ` with ${plural(stages.length, "stage")}` : "";
|
|
8177
|
+
return {
|
|
8178
|
+
...head(address, "create", "safe", `Create ${described(address, resource)}${what}`),
|
|
8179
|
+
desired: resource.definition,
|
|
8180
|
+
...stages.length > 0 ? { stages } : {},
|
|
8181
|
+
...notes.length > 0 ? { notes } : {},
|
|
8182
|
+
expect: { exists: false }
|
|
8183
|
+
};
|
|
8184
|
+
}
|
|
8185
|
+
/** Whether Kalup writes the pipelines of an object: deals, tickets and custom objects. */
|
|
8186
|
+
function carriedStages(context, pipeline) {
|
|
8187
|
+
const { resources } = context.input.loaded.ir;
|
|
8188
|
+
return (own$2(resources, pipeline)?.definition?.stages ?? []).flatMap((id) => {
|
|
8189
|
+
const address = `stage:${pipeline.slice(9)}/${id}`;
|
|
8190
|
+
const stage = own$2(resources, address);
|
|
8191
|
+
return stage?.managed && own$2(context.overrides, address)?.skip !== true ? [{
|
|
8192
|
+
address,
|
|
8193
|
+
desired: stage.definition ?? {}
|
|
8194
|
+
}] : [];
|
|
8195
|
+
});
|
|
8196
|
+
}
|
|
8197
|
+
function carried(context, stage) {
|
|
8198
|
+
const pipeline = pipelineOf(stage);
|
|
8199
|
+
const step = context.decided.get(pipeline);
|
|
8200
|
+
return step?.action === "create" && step.risk !== "blocked" || context.missing.some((m) => m.address === pipeline);
|
|
8201
|
+
}
|
|
8202
|
+
function idHolder(context, address) {
|
|
8203
|
+
const id = ownId(address);
|
|
8204
|
+
const object = objectOf(address);
|
|
8205
|
+
const { pipelines, stages } = context.pipelines;
|
|
8206
|
+
if (kindOf$3(address) === "pipeline") return [...pipelines].flatMap(([key, held]) => key === object ? [] : held).find((a) => ownId(a) === id);
|
|
8207
|
+
const pipeline = pipelineOf(address);
|
|
8208
|
+
return (stages.get(object) ?? []).find((a) => pipelineOf(a) !== pipeline && ownId(a) === id);
|
|
8209
|
+
}
|
|
8210
|
+
function unreadClash(context, address) {
|
|
8211
|
+
const other = {
|
|
8212
|
+
deals: "tickets",
|
|
8213
|
+
tickets: "deals"
|
|
8214
|
+
}[objectOf(address)];
|
|
8215
|
+
if (kindOf$3(address) !== "pipeline" || other === void 0) return [];
|
|
8216
|
+
if (own$2(context.coverage.objects, other)?.pipelines?.status === "read") return [];
|
|
8217
|
+
return [{
|
|
8218
|
+
unit: "id",
|
|
8219
|
+
live: null,
|
|
8220
|
+
note: `HubSpot keeps pipeline IDs unique across deals and tickets, and this plan did not read the pipelines of ${other}: HubSpot refuses the create if ${other === "tickets" ? "a ticket pipeline" : "a deal pipeline"} holds ${ownId(address)}`
|
|
8221
|
+
}];
|
|
8222
|
+
}
|
|
8223
|
+
function assignedNotes(context, address, stages) {
|
|
8224
|
+
const ids = [address, ...stages.map((st) => st.address)].filter((a) => ASSIGNED.test(ownId(a)));
|
|
8225
|
+
if (ids.length === 0) return [];
|
|
8226
|
+
const { observation, target } = context.input;
|
|
8227
|
+
const pipeline = kindOf$3(address) === "stage" ? pipelineOf(address) : address;
|
|
8228
|
+
const label = String(own$2(observation.resources, pipeline)?.definition?.label ?? own$2(context.input.loaded.ir.resources, pipeline)?.definition?.label);
|
|
8229
|
+
const matching = (context.pipelines.pipelines.get(objectOf(address)) ?? []).filter((a) => String(own$2(observation.resources, a)?.definition?.label).toLowerCase() === label.toLowerCase());
|
|
8230
|
+
const [only] = matching;
|
|
8231
|
+
const match = matching.length === 1 && only ? `; the portal holds a pipeline "${label}" as ${ownId(only)}` : "";
|
|
8232
|
+
return [{
|
|
8233
|
+
unit: "id",
|
|
8234
|
+
live: null,
|
|
8235
|
+
note: sanitize(`HubSpot assigned the ID ${ids.map(ownId).join(", ")} in another portal${match}. If this portal holds the same pipeline under other IDs, add a name override for the pipeline and each stage under targets.${target}.overrides instead of creating a copy`, TEXT_MAX$2)
|
|
8236
|
+
}];
|
|
8237
|
+
}
|
|
8238
|
+
function firstPipelineNote(context, address) {
|
|
8239
|
+
const object = objectOf(address);
|
|
8240
|
+
const first = kindOf$3(address) === "pipeline" && !STANDARD_OBJECTS.has(object) && !context.pipelines.pipelines.has(object);
|
|
8241
|
+
const note = `the first pipeline on ${object}: HubSpot adds its own properties hs_pipeline and hs_pipeline_stage to the object, and keeps them after the pipeline is gone`;
|
|
8242
|
+
return first ? [{
|
|
8243
|
+
unit: "object",
|
|
8244
|
+
live: null,
|
|
8245
|
+
note
|
|
8246
|
+
}] : [];
|
|
8247
|
+
}
|
|
7025
8248
|
function ownedAbsent(context, address, resource, override, entry) {
|
|
7026
8249
|
const { input } = context;
|
|
7027
8250
|
const kind = kindOf$3(address);
|
|
@@ -7032,7 +8255,7 @@ function ownedAbsent(context, address, resource, override, entry) {
|
|
|
7032
8255
|
});
|
|
7033
8256
|
const found = kind === "property" ? archivedOf(input.archivedProperties, objectOf(address), portalName$1(context, address)) : null;
|
|
7034
8257
|
const archived = found === null ? null : found !== void 0;
|
|
7035
|
-
const recreatable = kind
|
|
8258
|
+
const recreatable = kind !== "property" || archived === false;
|
|
7036
8259
|
if (!taken) {
|
|
7037
8260
|
const flag = targetFlag(input.target);
|
|
7038
8261
|
context.missing.push({
|
|
@@ -7115,9 +8338,13 @@ function settle$1(context, r) {
|
|
|
7115
8338
|
};
|
|
7116
8339
|
for (const u of units) place$1(context, r, u, bins);
|
|
7117
8340
|
const { baseUnits, changes, held, notes } = bins;
|
|
7118
|
-
if (bins.refused)
|
|
8341
|
+
if (bins.refused) {
|
|
8342
|
+
const detail = `${readOnlyOf(address)}, so --take config cannot write its units`;
|
|
8343
|
+
return blocked(address, action, "unsupported", kind === "object" ? "schema writes not supported" : "pipelines not written", detail, "leave it out of --take");
|
|
8344
|
+
}
|
|
7119
8345
|
const meta = context.input.observation.meta?.[address];
|
|
7120
8346
|
reorder(r, changes, meta);
|
|
8347
|
+
if (kind === "pipeline") reorderStages(context, r, changes);
|
|
7121
8348
|
const block = writeBlock(kind, units, changes.map((c) => c.unit), meta);
|
|
7122
8349
|
if (block) return blocked(address, action, "unsupported", block.short, block.detail, block.fix);
|
|
7123
8350
|
const takeover = takeoverUnits(context, r, changes);
|
|
@@ -7135,11 +8362,26 @@ function settle$1(context, r) {
|
|
|
7135
8362
|
...held.length > 0 ? { held } : {},
|
|
7136
8363
|
...notes.length > 0 ? { notes } : {},
|
|
7137
8364
|
...baseUnits.length > 0 ? { baseUnits } : {},
|
|
8365
|
+
...stageLabelsOf(context, address, changes, held),
|
|
7138
8366
|
expect: expectOf(kind, changes, observed)
|
|
7139
8367
|
}, context.policy, owner.entry, { takeoverUnits: takeover });
|
|
7140
8368
|
}
|
|
8369
|
+
function stageLabelsOf(context, address, changes, held) {
|
|
8370
|
+
const lists = [...changes.filter((c) => c.unit === "stages").flatMap((c) => [c.before, c.after]), ...held.filter((h) => h.unit === "stages").flatMap((h) => [
|
|
8371
|
+
h.config,
|
|
8372
|
+
h.live,
|
|
8373
|
+
h.base
|
|
8374
|
+
])];
|
|
8375
|
+
const ids = [...new Set(lists.flatMap((list) => Array.isArray(list) ? list : []))].sort(byCodeUnit);
|
|
8376
|
+
if (ids.length === 0) return {};
|
|
8377
|
+
const prefix = `${address.replace("pipeline:", "stage:")}/`;
|
|
8378
|
+
const { loaded, observation } = context.input;
|
|
8379
|
+
const label = (id) => own$2(loaded.ir.resources, `${prefix}${id}`)?.definition?.label ?? own$2(observation.resources, `${prefix}${id}`)?.definition?.label;
|
|
8380
|
+
return { stageLabels: Object.fromEntries(ids.map((id) => [id, String(label(id) ?? id)])) };
|
|
8381
|
+
}
|
|
7141
8382
|
function place$1(context, r, u, bins) {
|
|
7142
|
-
const { action, address,
|
|
8383
|
+
const { action, address, owner } = r;
|
|
8384
|
+
const readOnly = readOnlyOf(address);
|
|
7143
8385
|
const rewrite = owner.entry?.rewrites && own$2(owner.entry.rewrites, u.unit);
|
|
7144
8386
|
if (rewrite && u.class !== "converged" && same$2(u.desired, rewrite.sent) && same$2(u.observed, rewrite.stored)) {
|
|
7145
8387
|
const note = `HubSpot stores ${show$2(rewrite.stored)} when sent ${show$2(rewrite.sent)}; change config to match`;
|
|
@@ -7155,9 +8397,9 @@ function place$1(context, r, u, bins) {
|
|
|
7155
8397
|
const unwritten = {
|
|
7156
8398
|
unit: u.unit,
|
|
7157
8399
|
live: u.observed,
|
|
7158
|
-
note: `not written: ${
|
|
8400
|
+
note: `not written: ${readOnly}`
|
|
7159
8401
|
};
|
|
7160
|
-
if (
|
|
8402
|
+
if (readOnly !== void 0 && disposition === "write") {
|
|
7161
8403
|
bins.refused ||= taken;
|
|
7162
8404
|
if (REVERTING.has(u.class)) {
|
|
7163
8405
|
bins.held.push(heldOf(context, address, u, unpulledOf$1(context, r, u)));
|
|
@@ -7167,15 +8409,15 @@ function place$1(context, r, u, bins) {
|
|
|
7167
8409
|
disposition = "note";
|
|
7168
8410
|
}
|
|
7169
8411
|
if (disposition === "none") {
|
|
7170
|
-
if (!(u.unit === "options.order" && u.desired.length === 0) && (action === "adopt" || !(Object.hasOwn(u, "base") && same$2(u.base, u.desired, SETS.has(u.unit))))) bins.baseUnits.push(u.unit);
|
|
8412
|
+
if (!((u.unit === "options.order" || ORDERS.has(u.unit)) && u.desired.length === 0) && (action === "adopt" || !(Object.hasOwn(u, "base") && same$2(u.base, u.desired, SETS.has(u.unit))))) bins.baseUnits.push(u.unit);
|
|
7171
8413
|
} else if (disposition === "write") bins.changes.push(changeOf(u));
|
|
7172
8414
|
else if (disposition === "hold") hold(context, r, u, bins);
|
|
7173
|
-
else if (
|
|
7174
|
-
else bins.notes.push({
|
|
8415
|
+
else if (readOnly === void 0) bins.notes.push({
|
|
7175
8416
|
unit: u.unit,
|
|
7176
8417
|
live: u.observed,
|
|
7177
8418
|
note: keepNote(context, address, u, unpulledOf$1(context, r, u))
|
|
7178
8419
|
});
|
|
8420
|
+
else bins.notes.push(unwritten);
|
|
7179
8421
|
}
|
|
7180
8422
|
function hold(context, r, u, bins) {
|
|
7181
8423
|
const unpulled = unpulledOf$1(context, r, u);
|
|
@@ -7253,6 +8495,42 @@ function keepNote(context, address, u, unpulled) {
|
|
|
7253
8495
|
if (unpulled === void 0) return keptNote(target, address);
|
|
7254
8496
|
return sanitize(`kept; ${unpulled === SHADOWED_NOTE ? shadowedNote(target) : unpulled}`, TEXT_MAX$2);
|
|
7255
8497
|
}
|
|
8498
|
+
function reorderStages(context, r, changes) {
|
|
8499
|
+
const desired = r.owned.stages ?? [];
|
|
8500
|
+
const live = r.observed.definition?.stages ?? [];
|
|
8501
|
+
const prefix = `stage:${r.address.slice(9)}/`;
|
|
8502
|
+
const added = new Set(desired.filter((id) => !live.includes(id) && creatable(context, `${prefix}${id}`)));
|
|
8503
|
+
const after = desired.filter((id) => live.includes(id) || added.has(id));
|
|
8504
|
+
const written = changes.find((c) => c.unit === "stages");
|
|
8505
|
+
if (written) {
|
|
8506
|
+
written.after = after;
|
|
8507
|
+
return;
|
|
8508
|
+
}
|
|
8509
|
+
const order = r.units.find((u) => u.unit === "stages");
|
|
8510
|
+
if (added.size === 0 || order?.class !== "converged") return;
|
|
8511
|
+
const closing = (id) => closesStage(own$2(context.input.loaded.ir.resources, `${prefix}${id}`)?.definition);
|
|
8512
|
+
const created = [...added].sort((a, b) => Number(closing(b)) - Number(closing(a)) || byCodeUnit(a, b));
|
|
8513
|
+
if (!same$2([...after.filter((id) => !added.has(id)), ...created], after)) {
|
|
8514
|
+
changes.push({
|
|
8515
|
+
unit: "stages",
|
|
8516
|
+
class: "add",
|
|
8517
|
+
op: "set",
|
|
8518
|
+
before: order.observed,
|
|
8519
|
+
after
|
|
8520
|
+
});
|
|
8521
|
+
changes.sort((a, b) => byCodeUnit(a.unit, b.unit));
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
function creatable(context, stage) {
|
|
8525
|
+
const resource = own$2(context.input.loaded.ir.resources, stage);
|
|
8526
|
+
const override = own$2(context.overrides, stage);
|
|
8527
|
+
return resource?.managed === true && override?.skip !== true && override?.name === void 0 && statusOf(context.input.observation, stage) === "absent" && writesPipelines(objectOf(stage)) && idHolder(context, stage) === void 0;
|
|
8528
|
+
}
|
|
8529
|
+
function readOnlyOf(address) {
|
|
8530
|
+
const kind = kindOf$3(address);
|
|
8531
|
+
if (kind === "object") return NO_SCHEMA_WRITES;
|
|
8532
|
+
return (kind === "pipeline" || kind === "stage") && !writesPipelines(objectOf(address)) ? noPipelineWrites(objectOf(address)) : void 0;
|
|
8533
|
+
}
|
|
7256
8534
|
function reorder(r, changes, meta) {
|
|
7257
8535
|
const desired = r.owned.options;
|
|
7258
8536
|
if (desired === void 0) return;
|
|
@@ -7287,7 +8565,11 @@ function expectOf(kind, changes, observed) {
|
|
|
7287
8565
|
if (changes.length === 0) return { exists: true };
|
|
7288
8566
|
const live = capturedSpec(observed).fields;
|
|
7289
8567
|
const values = {};
|
|
7290
|
-
for (const
|
|
8568
|
+
for (const change of changes) {
|
|
8569
|
+
const field = fieldOf(change.unit);
|
|
8570
|
+
if (field === "options") values.options = observed.definition?.options ?? [];
|
|
8571
|
+
else values[field] = ORDERS.has(field) ? change.before : live[field];
|
|
8572
|
+
}
|
|
7291
8573
|
if (kind === "property") {
|
|
7292
8574
|
values.type = live.type;
|
|
7293
8575
|
values.fieldType = live.fieldType;
|
|
@@ -7304,21 +8586,28 @@ function titleOf(r, changes, baseUnits) {
|
|
|
7304
8586
|
return baseUnits.length > 0 ? `Record the agreed values of ${what}` : `No change to ${what}`;
|
|
7305
8587
|
}
|
|
7306
8588
|
function writesTitle(changes) {
|
|
7307
|
-
return writesTail(changes, (c) => `"${(c.op === "add" ? c.after : c.before).label}"`,
|
|
8589
|
+
return writesTail(changes, (c) => `"${(c.op === "add" ? c.after : c.before).label}"`, fieldWords);
|
|
7308
8590
|
}
|
|
7309
8591
|
function removals(context) {
|
|
7310
8592
|
const entries = Object.entries(context.input.loaded.ir.tombstones).sort(([a], [b]) => byCodeUnit(a, b));
|
|
7311
8593
|
const releases = [];
|
|
7312
8594
|
const deletes = [];
|
|
7313
8595
|
const deleted = /* @__PURE__ */ new Map();
|
|
7314
|
-
for (const kind of [
|
|
8596
|
+
for (const kind of [
|
|
8597
|
+
"property",
|
|
8598
|
+
"group",
|
|
8599
|
+
"stage",
|
|
8600
|
+
"pipeline"
|
|
8601
|
+
]) {
|
|
7315
8602
|
for (const [address, tombstone] of entries.filter(([a]) => kindOf$3(a) === kind)) {
|
|
7316
|
-
const
|
|
8603
|
+
const covering = kind === "stage" ? own$2(context.input.loaded.ir.tombstones, pipelineOf(address)) : void 0;
|
|
8604
|
+
if (covering?.action === tombstone.action) continue;
|
|
8605
|
+
const step = covering ? uncovered(address, tombstone, pipelineOf(address)) : removal(context, address, tombstone, deleted);
|
|
7317
8606
|
if (step?.action === "release") releases.push(step);
|
|
7318
8607
|
else if (step) deletes.push(step);
|
|
7319
8608
|
countDeleted(context, step, deleted);
|
|
7320
8609
|
}
|
|
7321
|
-
for (const address of
|
|
8610
|
+
for (const address of takeoverOf$1(context, kind)) {
|
|
7322
8611
|
const step = takeoverDelete(context, address, deleted);
|
|
7323
8612
|
deletes.push(step);
|
|
7324
8613
|
countDeleted(context, step, deleted);
|
|
@@ -7326,9 +8615,17 @@ function removals(context) {
|
|
|
7326
8615
|
}
|
|
7327
8616
|
return [...releases, ...deletes];
|
|
7328
8617
|
}
|
|
8618
|
+
function uncovered(address, tombstone, pipeline) {
|
|
8619
|
+
if (tombstone.action === "destroy") return blocked(address, "delete", "not-owned", "pipeline released", `the tombstone of ${pipeline} releases the pipeline with its stages, so Kalup no longer owns this stage to delete it`, `set the action of ${pipeline} to destroy to delete it with its stages, or this stage's to release`);
|
|
8620
|
+
return blocked(address, "release", "unsupported", "pipeline deleted", `the tombstone of ${pipeline} deletes the pipeline with its stages, so this stage cannot be kept`, `set the action of ${pipeline} to release to keep it with its stages, or this stage's to destroy`);
|
|
8621
|
+
}
|
|
8622
|
+
function takeoverOf$1(context, kind) {
|
|
8623
|
+
if (kind === "property") return context.takeover.properties;
|
|
8624
|
+
return kind === "group" ? context.takeover.groups : [];
|
|
8625
|
+
}
|
|
7329
8626
|
function countDeleted(context, step, deleted) {
|
|
7330
8627
|
if (step?.action === "delete" && (step.risk !== "blocked" || step.blocked?.reason === "policy")) {
|
|
7331
|
-
const key = objectOf(step.address);
|
|
8628
|
+
const key = kindOf$3(step.address) === "stage" ? pipelineOf(step.address) : objectOf(step.address);
|
|
7332
8629
|
deleted.set(key, /* @__PURE__ */ new Set([...deleted.get(key) ?? [], portalName$1(context, step.address)]));
|
|
7333
8630
|
}
|
|
7334
8631
|
}
|
|
@@ -7413,7 +8710,7 @@ function removal(context, address, tombstone, deleted) {
|
|
|
7413
8710
|
return destroy(context, address, entry, deleted);
|
|
7414
8711
|
}
|
|
7415
8712
|
function releaseOf(address, tombstone, owner, status) {
|
|
7416
|
-
const plain = `${
|
|
8713
|
+
const plain = `${nounOf(address)} ${shownName(address)}${placeOf(address)}`;
|
|
7417
8714
|
if (owner.entry && tombstone.action === "release") {
|
|
7418
8715
|
let where = "";
|
|
7419
8716
|
if (status === "present") where = "; it stays in HubSpot";
|
|
@@ -7433,23 +8730,33 @@ function destroy(context, address, entry, deleted) {
|
|
|
7433
8730
|
const observed = input.observation.resources[address];
|
|
7434
8731
|
const key = objectOf(address);
|
|
7435
8732
|
if (!observed.managed) return blocked(address, "delete", "unsupported", "HubSpot-defined or calculated", "HubSpot-defined or calculated in this portal, so Kalup does not delete it");
|
|
8733
|
+
const readOnly = readOnlyOf(address);
|
|
8734
|
+
if (readOnly !== void 0) return blocked(address, "delete", "unsupported", "not written in this release", readOnly, "change the tombstone's action to release, which stops managing it and leaves it in HubSpot");
|
|
7436
8735
|
const name = portalName$1(context, address);
|
|
7437
8736
|
const members = kindOf$3(address) === "group" ? {
|
|
7438
8737
|
active: own$2(input.observation.members?.[key] ?? {}, name) ?? [],
|
|
7439
8738
|
deleted: deleted.get(key) ?? /* @__PURE__ */ new Set()
|
|
7440
8739
|
} : void 0;
|
|
7441
|
-
const block = deleteBlock(input.observation.meta?.[address], members);
|
|
8740
|
+
const block = deleteBlock(input.observation.meta?.[address], members) ?? stageDeleteBlock(context, address, deleted);
|
|
7442
8741
|
if (block) return blocked(address, "delete", "unsupported", block.short, block.detail, block.fix);
|
|
7443
8742
|
if (!policy.allowDestroy) return blocked(address, "delete", "policy", "deletes not allowed", `target ${input.target} does not allow deletes`, `set allowDestroy: true under targets.${input.target} in kalup.config.ts, or change the tombstone's action to release`);
|
|
7444
8743
|
const values = baseValues(entry, observed);
|
|
8744
|
+
if (kindOf$3(address) === "pipeline") values.stages = observed.definition?.stages ?? [];
|
|
8745
|
+
const purged = kindOf$3(address) === "pipeline" || kindOf$3(address) === "stage";
|
|
7445
8746
|
return finish$1({
|
|
7446
|
-
...head(address, "delete", "destructive",
|
|
8747
|
+
...head(address, "delete", "destructive", `${purged ? "Delete" : "Archive"} ${described(address, observed)}${purged ? PURGED : ""}`),
|
|
7447
8748
|
expect: Object.keys(values).length > 0 ? {
|
|
7448
8749
|
exists: true,
|
|
7449
8750
|
values
|
|
7450
8751
|
} : { exists: true }
|
|
7451
8752
|
}, context.policy, entry);
|
|
7452
8753
|
}
|
|
8754
|
+
function stageDeleteBlock(context, address, deleted) {
|
|
8755
|
+
if (kindOf$3(address) !== "stage") return;
|
|
8756
|
+
const gone = deleted.get(pipelineOf(address)) ?? /* @__PURE__ */ new Set();
|
|
8757
|
+
const before = [...context.decided.values()].filter(hasEffect);
|
|
8758
|
+
return stageDeleteRule(afterSteps(context.input.observation.resources, before), address, gone, bin);
|
|
8759
|
+
}
|
|
7453
8760
|
function baseValues(entry, observed) {
|
|
7454
8761
|
const live = capturedSpec(observed).fields;
|
|
7455
8762
|
const values = {};
|
|
@@ -7471,7 +8778,7 @@ function release(address, title, expect) {
|
|
|
7471
8778
|
function orphansOf(input, steps) {
|
|
7472
8779
|
const { loaded, state, target } = input;
|
|
7473
8780
|
const overrides = loaded.config.targets[target]?.overrides ?? {};
|
|
7474
|
-
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) && !steps.some((s) => s.address === address)).sort(([a], [b]) => byCodeUnit(a, b)).map(([address, entry]) => {
|
|
8781
|
+
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) && !(kindOf$3(address) === "stage" && Object.hasOwn(loaded.ir.tombstones, pipelineOf(address))) && !steps.some((s) => s.address === address)).sort(([a], [b]) => byCodeUnit(a, b)).map(([address, entry]) => {
|
|
7475
8782
|
const name = resolvedName(overrides, address);
|
|
7476
8783
|
const rm = `${bin} rm ${shellWord(address)}`;
|
|
7477
8784
|
let note = `no longer in config: run ${rm} to delete it in HubSpot, or ${rm} --release to stop managing it`;
|
|
@@ -7502,10 +8809,14 @@ function staleNote(context, address, stale) {
|
|
|
7502
8809
|
function portalName$1(context, address) {
|
|
7503
8810
|
return resolvedName(context.overrides, address);
|
|
7504
8811
|
}
|
|
7505
|
-
/**
|
|
8812
|
+
/**
|
|
8813
|
+
* The portal name an address resolves to on a target: its name override, else its own name. A stage's is its stage ID;
|
|
8814
|
+
* its pipeline's ID is the pipeline address's.
|
|
8815
|
+
*/
|
|
7506
8816
|
function resolvedName(overrides, address) {
|
|
7507
8817
|
const override = own$2(overrides, address);
|
|
7508
|
-
|
|
8818
|
+
const mine = kindOf$3(address) === "stage" ? ownId(address) : nameOf(address);
|
|
8819
|
+
return (override?.skip === true ? void 0 : override?.name) ?? mine;
|
|
7509
8820
|
}
|
|
7510
8821
|
function archivedOf(lists, key, name) {
|
|
7511
8822
|
const list = own$2(lists, key);
|
|
@@ -7573,10 +8884,8 @@ function head(address, action, risk, title) {
|
|
|
7573
8884
|
};
|
|
7574
8885
|
}
|
|
7575
8886
|
function blocked(address, action, reason, short, detail, fix) {
|
|
7576
|
-
const name = nameOf(address);
|
|
7577
|
-
const kind = kindOf$3(address);
|
|
7578
8887
|
return {
|
|
7579
|
-
...head(address, action, "blocked",
|
|
8888
|
+
...head(address, action, "blocked", `Cannot plan ${kindOf$3(address)} ${shownName(address)}${placeOf(address)}: ${short}`),
|
|
7580
8889
|
expect: blockedExpect(action),
|
|
7581
8890
|
blocked: {
|
|
7582
8891
|
reason,
|
|
@@ -7597,7 +8906,14 @@ function actionOf(status) {
|
|
|
7597
8906
|
function described(address, resource) {
|
|
7598
8907
|
const d = resource.definition ?? {};
|
|
7599
8908
|
if (kindOf$3(address) === "object") return `custom object "${d.labels.singular}" (${nameOf(address)})`;
|
|
7600
|
-
return `${
|
|
8909
|
+
return `${nounOf(address)} "${String(d.label)}" (${shownName(address)})${placeOf(address)}`;
|
|
8910
|
+
}
|
|
8911
|
+
function nounOf(address) {
|
|
8912
|
+
return {
|
|
8913
|
+
group: "property group",
|
|
8914
|
+
property: "property",
|
|
8915
|
+
pipeline: "pipeline"
|
|
8916
|
+
}[kindOf$3(address)] ?? kindOf$3(address);
|
|
7601
8917
|
}
|
|
7602
8918
|
function skipFix(address, target) {
|
|
7603
8919
|
return `leave it out on this target: add { '${address}': { skip: true } } under targets.${target}.overrides`;
|
|
@@ -7626,7 +8942,11 @@ function bindingsFor(open, overrides, idOf) {
|
|
|
7626
8942
|
}
|
|
7627
8943
|
return Object.fromEntries([...found].sort(([a], [b]) => byCodeUnit(a, b)));
|
|
7628
8944
|
}
|
|
7629
|
-
/**
|
|
8945
|
+
/**
|
|
8946
|
+
* A step's own address, every $ref it carries, the pipeline a stage is under, the stages a pipeline create carries or
|
|
8947
|
+
* its stage order names (live and approved), and the object a group, property, pipeline or stage is on. A stage's name
|
|
8948
|
+
* override is so a binding of each pipeline step that moves or names the stage: apply moves it by its portal ID.
|
|
8949
|
+
*/
|
|
7630
8950
|
function dependencies(step) {
|
|
7631
8951
|
const out = [step.address];
|
|
7632
8952
|
collectRefs([
|
|
@@ -7634,9 +8954,18 @@ function dependencies(step) {
|
|
|
7634
8954
|
step.changes,
|
|
7635
8955
|
step.expect
|
|
7636
8956
|
], out);
|
|
8957
|
+
if (kindOf$3(step.address) === "stage") out.push(pipelineOf(step.address));
|
|
8958
|
+
out.push(...(step.stages ?? []).map((st) => st.address));
|
|
8959
|
+
if (kindOf$3(step.address) === "pipeline") out.push(...orderedStages(step));
|
|
7637
8960
|
if (kindOf$3(step.address) !== "object") out.push(`object:${objectOf(step.address)}`);
|
|
7638
8961
|
return out;
|
|
7639
8962
|
}
|
|
8963
|
+
function orderedStages(step) {
|
|
8964
|
+
const lists = [step.expect.values?.stages, ...(step.changes ?? []).filter((c) => c.unit === "stages").map((c) => c.after)];
|
|
8965
|
+
const prefix = `${step.address.replace("pipeline:", "stage:")}/`;
|
|
8966
|
+
const ids = lists.flatMap((list) => Array.isArray(list) ? list.filter((id) => typeof id === "string") : []);
|
|
8967
|
+
return [...new Set(ids)].map((id) => `${prefix}${id}`);
|
|
8968
|
+
}
|
|
7640
8969
|
function collectRefs(value, out) {
|
|
7641
8970
|
if (Array.isArray(value)) {
|
|
7642
8971
|
for (const item of value) collectRefs(item, out);
|
|
@@ -7666,7 +8995,9 @@ function callsOf(steps, missing, bindings) {
|
|
|
7666
8995
|
const objects = new Set(effects.map((s) => objectOf(s.address)));
|
|
7667
8996
|
const archived = /* @__PURE__ */ new Set([...effects.filter((s) => s.action === "create" || s.action === "delete").map((s) => objectOf(s.address)), ...missing.map((m) => objectOf(m.address))]);
|
|
7668
8997
|
const schemas = Object.values(bindings).some((b) => b.id !== void 0) ? 1 : 0;
|
|
7669
|
-
|
|
8998
|
+
const reorders = effects.flatMap((s) => s.changes ?? []).filter((c) => c.unit === "stages").reduce((sum, c) => sum + 2 * (c.after?.length ?? 0), 0);
|
|
8999
|
+
const pipelines = new Set(effects.filter((s) => kindOf$3(s.address) === "pipeline" || kindOf$3(s.address) === "stage").map((s) => objectOf(s.address)));
|
|
9000
|
+
return 3 * effects.filter(writes$1).length + 4 * objects.size + 3 * archived.size + schemas + 1 + reorders + pipelines.size;
|
|
7670
9001
|
}
|
|
7671
9002
|
function countsOf$1(steps) {
|
|
7672
9003
|
const counts = {
|
|
@@ -7685,11 +9016,12 @@ function countsOf$1(steps) {
|
|
|
7685
9016
|
}
|
|
7686
9017
|
function planCoverage(coverage, steps, excluded, unobserved) {
|
|
7687
9018
|
const objects = Object.entries(coverage.objects).sort(([a], [b]) => byCodeUnit(a, b));
|
|
9019
|
+
const scopeOf = (o) => o.status === "unreadable" ? o.missingScope : o.pipelines?.missingScope;
|
|
7688
9020
|
return {
|
|
7689
9021
|
complete: coverage.complete && !steps.some((s) => s.action === "unknown"),
|
|
7690
|
-
unreadable: [...objects.filter(([, o]) => o.status === "unreadable").map(([object, o]) => o
|
|
9022
|
+
unreadable: [...objects.filter(([, o]) => o.status === "unreadable" || o.pipelines?.status === "unreadable").map(([object, o]) => scopeOf(o) === void 0 ? { object } : {
|
|
7691
9023
|
object,
|
|
7692
|
-
scope: o
|
|
9024
|
+
scope: scopeOf(o)
|
|
7693
9025
|
}), ...unobserved.map((object) => ({ object }))].sort((a, b) => byCodeUnit(a.object, b.object)),
|
|
7694
9026
|
unsupported: objects.flatMap(([key, o]) => [...(o.unsupported ?? []).map((u) => `property:${key}/${u.name}`), ...o.unsupportedSchema ? [`object:${key}`] : []]).sort(byCodeUnit),
|
|
7695
9027
|
excluded
|
|
@@ -7775,11 +9107,15 @@ function namesOf(plan, overrides) {
|
|
|
7775
9107
|
const bound = [...renamed].find(([address, name]) => address.startsWith(prefix) && name === portalName);
|
|
7776
9108
|
return bound ? bound[0].slice(prefix.length) : portalName;
|
|
7777
9109
|
};
|
|
9110
|
+
const portalOf = (address) => renamed.get(address) ?? (kindOf$2(address) === "stage" ? ownId(address) : nameOf(address));
|
|
7778
9111
|
return {
|
|
7779
|
-
portalName:
|
|
9112
|
+
portalName: portalOf,
|
|
7780
9113
|
objectType: (key) => own(`object:${key}`)?.id ?? key,
|
|
7781
|
-
localGroup: (key,
|
|
7782
|
-
localProperty: (key,
|
|
9114
|
+
localGroup: (key, name) => local(`group:${key}/`, name),
|
|
9115
|
+
localProperty: (key, name) => local(`property:${key}/`, name),
|
|
9116
|
+
localPipeline: (key, id) => local(`pipeline:${key}/`, id),
|
|
9117
|
+
localStage: (key, pipeline, id) => local(`stage:${key}/${pipeline}/`, id),
|
|
9118
|
+
pipelineId: (address) => portalOf(kindOf$2(address) === "stage" ? pipelineOf(address) : address)
|
|
7783
9119
|
};
|
|
7784
9120
|
}
|
|
7785
9121
|
const SENSITIVITIES = [
|
|
@@ -7823,8 +9159,60 @@ async function observeForApply(http, plan, overrides) {
|
|
|
7823
9159
|
return out;
|
|
7824
9160
|
}
|
|
7825
9161
|
async function observeObject(http, read, observing, key, out) {
|
|
9162
|
+
const effects = observing.plan.steps.filter((s) => hasEffect(s) && objectOf(s.address) === key);
|
|
9163
|
+
const pipelines = effects.filter((s) => PIPELINE_TYPES.has(kindOf$2(s.address)));
|
|
9164
|
+
if (pipelines.length > 0) await observePipelines(read, observing, key, pipelines, out);
|
|
9165
|
+
if (pipelines.length < effects.length) await observeProperties(http, read, observing, key, out);
|
|
9166
|
+
}
|
|
9167
|
+
async function observePipelines(read, { names }, key, steps, out) {
|
|
9168
|
+
const live = localPipelines(key, normalizePipelines(key, (await read({
|
|
9169
|
+
type: "pipeline",
|
|
9170
|
+
path: "list",
|
|
9171
|
+
params: { objectType: names.objectType(key) }
|
|
9172
|
+
}, `the pipelines list of ${key}`)).results, !STANDARD_OBJECTS.has(key)), names);
|
|
9173
|
+
for (const step of steps) {
|
|
9174
|
+
const pipeline = kindOf$2(step.address) === "stage" ? pipelineOf(step.address) : step.address;
|
|
9175
|
+
Object.assign(out.resources, Object.fromEntries(pipelineResources(live, pipeline)));
|
|
9176
|
+
}
|
|
9177
|
+
}
|
|
9178
|
+
/** The pipelines of one object under the plan's local pipeline and stage IDs. */
|
|
9179
|
+
function localPipelines(key, live, names) {
|
|
9180
|
+
return live.map((p) => {
|
|
9181
|
+
const id = names.localPipeline(key, p.id);
|
|
9182
|
+
return {
|
|
9183
|
+
...p,
|
|
9184
|
+
id,
|
|
9185
|
+
stages: p.stages.map((st) => ({
|
|
9186
|
+
...st,
|
|
9187
|
+
id: names.localStage(key, id, st.id)
|
|
9188
|
+
}))
|
|
9189
|
+
};
|
|
9190
|
+
});
|
|
9191
|
+
}
|
|
9192
|
+
/** One pipeline, `pipeline:<key>/<id>`, and its stages as resources, as observe captures them. None when absent. */
|
|
9193
|
+
function pipelineResources(live, pipeline) {
|
|
9194
|
+
const id = pipeline.slice(pipeline.lastIndexOf("/") + 1);
|
|
9195
|
+
const found = live.find((p) => p.id === id);
|
|
9196
|
+
if (found === void 0) return [];
|
|
9197
|
+
const definition = {
|
|
9198
|
+
label: found.label,
|
|
9199
|
+
displayOrder: found.displayOrder,
|
|
9200
|
+
stages: found.stages.map((st) => st.id)
|
|
9201
|
+
};
|
|
9202
|
+
const prefix = `${pipeline.replace("pipeline:", "stage:")}/`;
|
|
9203
|
+
return [[pipeline, {
|
|
9204
|
+
type: "pipeline",
|
|
9205
|
+
managed: true,
|
|
9206
|
+
definition
|
|
9207
|
+
}], ...found.stages.map(({ id: stage, ...fields }) => [`${prefix}${stage}`, {
|
|
9208
|
+
type: "stage",
|
|
9209
|
+
managed: true,
|
|
9210
|
+
definition: fields
|
|
9211
|
+
}])];
|
|
9212
|
+
}
|
|
9213
|
+
async function observeProperties(http, read, observing, key, out) {
|
|
7826
9214
|
const { plan, names, schemas } = observing;
|
|
7827
|
-
const effects = plan.steps.filter((s) => hasEffect(s) && objectOf(s.address) === key);
|
|
9215
|
+
const effects = plan.steps.filter((s) => hasEffect(s) && objectOf(s.address) === key && !PIPELINE_TYPES.has(kindOf$2(s.address)));
|
|
7828
9216
|
const objectType = names.objectType(key);
|
|
7829
9217
|
const properties = await listProperties(read, key, objectType);
|
|
7830
9218
|
const { groups: live } = normalizeGroups((await read({
|
|
@@ -8435,38 +9823,48 @@ function staleUnits(step, observed, observation, portalName = nameOf(step.addres
|
|
|
8435
9823
|
if (expect.values === void 0 || observed === void 0) return [];
|
|
8436
9824
|
const live = capturedSpec(observed).fields;
|
|
8437
9825
|
return Object.entries(expect.values).filter(([field, value]) => {
|
|
8438
|
-
|
|
9826
|
+
let now = field === "options" ? observed.definition?.options ?? [] : live[field];
|
|
9827
|
+
if (step.action !== "delete" && ORDERS.has(field) && Array.isArray(now) && Array.isArray(value)) now = now.filter((member) => value.includes(member));
|
|
9828
|
+
return stableStringify(now) !== stableStringify(value);
|
|
8439
9829
|
}).map(([field]) => field).sort(byCodeUnit);
|
|
8440
9830
|
}
|
|
8441
9831
|
/**
|
|
8442
9832
|
* A step's title from its own data, never the plan's: the kind, the label config gives (for a delete, the label it
|
|
8443
9833
|
* expects to find), the name and the object. With `names`, the portal name follows the name wherever the two differ.
|
|
9834
|
+
* `warned` adds the warnings the plan's title carries, for a person about to confirm the step: that a pipeline or stage
|
|
9835
|
+
* delete cannot be restored, and that a field change's effect on existing values is not checked.
|
|
8444
9836
|
*/
|
|
8445
|
-
function stepTitle(step, names) {
|
|
9837
|
+
function stepTitle(step, names, warned = false) {
|
|
8446
9838
|
const { address, action } = step;
|
|
8447
9839
|
const kind = kindOf$1(address);
|
|
8448
|
-
const own =
|
|
9840
|
+
const own = shownName(address);
|
|
8449
9841
|
const portal = names?.portalName(address);
|
|
8450
9842
|
const name = portal === void 0 || portal === own ? own : `${own}, portal name ${portal}`;
|
|
8451
|
-
const noun =
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
const where = kind === "object" ? "" : ` on ${objectOf(address)}`;
|
|
9843
|
+
const noun = NOUNS[kind];
|
|
9844
|
+
const where = placeOf(address);
|
|
9845
|
+
const purged = kind === "pipeline" || kind === "stage";
|
|
9846
|
+
const removes = purged ? "Delete" : "Archive";
|
|
9847
|
+
const carried = (step.stages ?? []).length > 0 ? ` with ${plural((step.stages ?? []).length, "stage")}` : "";
|
|
8457
9848
|
const values = step.action === "delete" ? step.expect.values : step.desired;
|
|
8458
9849
|
const shown = kind === "object" ? (values?.labels)?.singular : values?.label;
|
|
8459
9850
|
const label = typeof shown === "string" ? ` "${shown}"` : "";
|
|
8460
9851
|
const what = `${noun}${label} (${name})${where}`;
|
|
8461
9852
|
const title = {
|
|
8462
|
-
create: () => `${step.labels?.includes("reverts-ui-edit") ? "Recreate" : "Create"} ${what}`,
|
|
8463
|
-
adopt: () => `Adopt ${what}${writes(step)}`,
|
|
8464
|
-
update: () => (step.changes ?? []).length > 0 ? `Update ${what}${writes(step)}` : `Record the agreed values of ${what}`,
|
|
8465
|
-
delete: () => label ?
|
|
9853
|
+
create: () => `${step.labels?.includes("reverts-ui-edit") ? "Recreate" : "Create"} ${what}${carried}`,
|
|
9854
|
+
adopt: () => `Adopt ${what}${writes(step, warned)}`,
|
|
9855
|
+
update: () => (step.changes ?? []).length > 0 ? `Update ${what}${writes(step, warned)}` : `Record the agreed values of ${what}`,
|
|
9856
|
+
delete: () => `${label ? `${removes} ${what}` : `${removes} ${noun} ${portal === void 0 || portal === own ? own : `${own} (portal name ${portal})`}${where}`}${purged && warned ? PURGED : ""}`,
|
|
8466
9857
|
release: () => `Stop managing ${noun} ${own}${where}; nothing changes in HubSpot`
|
|
8467
9858
|
}[action];
|
|
8468
9859
|
return sanitize(title ? title() : `${action} ${address}`, TITLE_MAX);
|
|
8469
9860
|
}
|
|
9861
|
+
const NOUNS = {
|
|
9862
|
+
object: "custom object",
|
|
9863
|
+
group: "property group",
|
|
9864
|
+
property: "property",
|
|
9865
|
+
pipeline: "pipeline",
|
|
9866
|
+
stage: "stage"
|
|
9867
|
+
};
|
|
8470
9868
|
/** The value config gives a unit in a step's desired values, or undefined when it gives none. */
|
|
8471
9869
|
function desiredValue(desired, unit) {
|
|
8472
9870
|
const member = memberOf(unit);
|
|
@@ -8478,6 +9876,8 @@ function desiredValue(desired, unit) {
|
|
|
8478
9876
|
function blockOf(plan, step, held, observation) {
|
|
8479
9877
|
const { entry, owned } = held;
|
|
8480
9878
|
if (kindOf$1(step.address) === "object" && step.action !== "adopt" && step.action !== "update") return "custom object schema writes are not supported in this release";
|
|
9879
|
+
const readOnly = readOnlyPipeline(step);
|
|
9880
|
+
if (readOnly !== void 0) return readOnly;
|
|
8481
9881
|
const observed = observation.resources[step.address];
|
|
8482
9882
|
const names = namesOf(plan);
|
|
8483
9883
|
switch (step.action) {
|
|
@@ -8494,6 +9894,11 @@ function blockOf(plan, step, held, observation) {
|
|
|
8494
9894
|
default: return `${sanitize(step.action)} is not a step apply runs`;
|
|
8495
9895
|
}
|
|
8496
9896
|
}
|
|
9897
|
+
function readOnlyPipeline(step) {
|
|
9898
|
+
const kind = kindOf$1(step.address);
|
|
9899
|
+
if (!(kind === "pipeline" || kind === "stage") || writesPipelines(objectOf(step.address))) return;
|
|
9900
|
+
return (step.action === "adopt" || step.action === "update" ? (step.changes ?? []).length : 1) > 0 && step.action !== "release" ? `Kalup does not write the pipelines of ${objectOf(step.address)} in this release` : void 0;
|
|
9901
|
+
}
|
|
8497
9902
|
function writeRefusal(step, trusted, observation) {
|
|
8498
9903
|
const { owned } = trusted;
|
|
8499
9904
|
if (step.action === "adopt" && owned) return "state owns it already";
|
|
@@ -8514,12 +9919,20 @@ function deleteRefusal(plan, step, { owner, overrides }, observation) {
|
|
|
8514
9919
|
const observed = observation.resources[step.address];
|
|
8515
9920
|
if (observation.unsupported.includes(step.address) || observed?.managed === false) return "its values cannot be checked before the delete";
|
|
8516
9921
|
if (takeover && observation.meta[step.address]?.hubspotDefined === true) return "HubSpot defines it, and takeover never archives what HubSpot defines";
|
|
9922
|
+
switch (kindOf$1(step.address)) {
|
|
9923
|
+
case "stage": return stageDeleteRefusal(plan, step, observation);
|
|
9924
|
+
case "pipeline": return;
|
|
9925
|
+
case "group": return groupDeleteRefusal(plan, step, takeover, observation);
|
|
9926
|
+
default: {
|
|
9927
|
+
const named = observation.schemaNamed[objectOf(step.address)] ?? [];
|
|
9928
|
+
const kept = takeover ? keptByRead(overrides, plan.target.name, step.address, observed, named) : void 0;
|
|
9929
|
+
return kept === void 0 ? deleteBlock(observation.meta[step.address])?.detail : `takeover never archives it: ${kept}`;
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
}
|
|
9933
|
+
function groupDeleteRefusal(plan, step, takeover, observation) {
|
|
8517
9934
|
const names = namesOf(plan);
|
|
8518
9935
|
const key = objectOf(step.address);
|
|
8519
|
-
if (kindOf$1(step.address) !== "group") {
|
|
8520
|
-
const kept = takeover ? keptByRead(overrides, plan.target.name, step.address, observed, observation.schemaNamed[key] ?? []) : void 0;
|
|
8521
|
-
return kept === void 0 ? deleteBlock(observation.meta[step.address])?.detail : `takeover never archives it: ${kept}`;
|
|
8522
|
-
}
|
|
8523
9936
|
const name = names.portalName(step.address);
|
|
8524
9937
|
if (takeover && (observation.members[key]?.[name] ?? []).length === 0) return "it held no property when apply read it, and HubSpot marks no group as its own, so takeover never archives an empty group";
|
|
8525
9938
|
const deleted = new Set(runOrder(plan).filter((s) => s.action === "delete" && kindOf$1(s.address) === "property" && objectOf(s.address) === key).map((s) => names.portalName(s.address)));
|
|
@@ -8528,24 +9941,32 @@ function deleteRefusal(plan, step, { owner, overrides }, observation) {
|
|
|
8528
9941
|
deleted
|
|
8529
9942
|
})?.detail;
|
|
8530
9943
|
}
|
|
9944
|
+
function stageDeleteRefusal(plan, step, observation) {
|
|
9945
|
+
const order = runOrder(plan);
|
|
9946
|
+
const before = order.slice(0, order.indexOf(step));
|
|
9947
|
+
const prefix = step.address.slice(0, step.address.lastIndexOf("/") + 1);
|
|
9948
|
+
const gone = new Set(before.filter((s) => s.action === "delete" && s.address.startsWith(prefix)).map((s) => s.address.slice(prefix.length)));
|
|
9949
|
+
return stageDeleteRule(afterSteps(observation.resources, before), step.address, gone, bin)?.detail;
|
|
9950
|
+
}
|
|
8531
9951
|
function uncheckedFields(step, owner) {
|
|
8532
9952
|
const captured = CAPTURED[kindOf$1(step.address)];
|
|
8533
9953
|
const needed = new Set(Object.keys(owner.base ?? {}).flatMap((unit) => {
|
|
8534
9954
|
if (unit === "options" || unit === "optionsOrder") return ["options"];
|
|
8535
9955
|
return captured.includes(unit) ? [unit] : [];
|
|
8536
9956
|
}));
|
|
9957
|
+
if (kindOf$1(step.address) === "pipeline") needed.add("stages");
|
|
8537
9958
|
const values = step.expect.values ?? {};
|
|
8538
9959
|
const missing = [...needed].filter((field) => !Object.hasOwn(values, field)).sort(byCodeUnit);
|
|
8539
9960
|
return step.expect.exists === true ? missing : ["exists", ...missing];
|
|
8540
9961
|
}
|
|
8541
9962
|
function recreates(step, observed, observation, portalName) {
|
|
8542
9963
|
if (!step.labels?.includes("reverts-ui-edit") || observed !== void 0) return false;
|
|
8543
|
-
if (kindOf$1(step.address)
|
|
9964
|
+
if (kindOf$1(step.address) !== "property") return true;
|
|
8544
9965
|
const archived = observation.archived[objectOf(step.address)];
|
|
8545
9966
|
return archived !== void 0 && !archived.some((p) => p.name === portalName);
|
|
8546
9967
|
}
|
|
8547
9968
|
function archivedName(step, observation, portalName) {
|
|
8548
|
-
if (kindOf$1(step.address)
|
|
9969
|
+
if (kindOf$1(step.address) !== "property") return false;
|
|
8549
9970
|
return observation?.archived[objectOf(step.address)]?.some((p) => p.name === portalName) === true;
|
|
8550
9971
|
}
|
|
8551
9972
|
function classesOf(step, trusted, observation) {
|
|
@@ -8616,16 +10037,24 @@ function checkNames(plan, config) {
|
|
|
8616
10037
|
}
|
|
8617
10038
|
function portalResource(address, names) {
|
|
8618
10039
|
const kind = kindOf$1(address);
|
|
8619
|
-
return `${kind}:${kind === "object" ? "" : `${names.objectType(objectOf(address))}/`}${names.portalName(address)}`;
|
|
10040
|
+
return `${kind}:${kind === "object" ? "" : `${names.objectType(objectOf(address))}/`}${kind === "stage" ? `${names.pipelineId(address)}/` : ""}${names.portalName(address)}`;
|
|
8620
10041
|
}
|
|
8621
10042
|
/**
|
|
8622
10043
|
* The effect steps in the order apply runs them, from their actions and addresses alone, never the file's order:
|
|
8623
|
-
* groups, then properties (a property may name a group the run creates), then
|
|
8624
|
-
*
|
|
10044
|
+
* groups, then properties (a property may name a group the run creates), then pipeline creates, then the other stage
|
|
10045
|
+
* steps (those that close a stage first, since a ticket pipeline keeps a closed stage), then the other pipeline steps
|
|
10046
|
+
* (a stage order is written once the pipeline's new stages exist), then releases, then property deletes, then group
|
|
10047
|
+
* deletes, so a group is deleted only after the deletes of its properties, then stage deletes, then pipeline deletes.
|
|
10048
|
+
* Plan order within each phase.
|
|
8625
10049
|
*/
|
|
8626
10050
|
function runOrder(plan) {
|
|
8627
|
-
|
|
10051
|
+
const known = ORDERED.get(plan.steps);
|
|
10052
|
+
if (known) return known;
|
|
10053
|
+
const order = formulasLast(plan.steps.filter(hasEffect).sort((a, b) => phase(a) - phase(b)));
|
|
10054
|
+
ORDERED.set(plan.steps, order);
|
|
10055
|
+
return order;
|
|
8628
10056
|
}
|
|
10057
|
+
const ORDERED = /* @__PURE__ */ new WeakMap();
|
|
8629
10058
|
const FORMULA_WORDS = /[A-Za-z0-9_]+/g;
|
|
8630
10059
|
/**
|
|
8631
10060
|
* A property step that writes a calculation formula runs after every other property step, and after each formula step
|
|
@@ -8653,8 +10082,16 @@ function formulasLast(steps) {
|
|
|
8653
10082
|
}
|
|
8654
10083
|
function phase(step) {
|
|
8655
10084
|
const kind = kindOf$1(step.address);
|
|
8656
|
-
if (step.action === "release") return
|
|
8657
|
-
if (step.action === "delete") return
|
|
10085
|
+
if (step.action === "release") return 6;
|
|
10086
|
+
if (step.action === "delete") return {
|
|
10087
|
+
object: 7,
|
|
10088
|
+
property: 7,
|
|
10089
|
+
group: 8,
|
|
10090
|
+
stage: 9,
|
|
10091
|
+
pipeline: 10
|
|
10092
|
+
}[kind];
|
|
10093
|
+
if (kind === "pipeline") return step.action === "create" ? 3 : 5;
|
|
10094
|
+
if (kind === "stage") return closesStage(step.desired) ? 4 : 4.5;
|
|
8658
10095
|
return {
|
|
8659
10096
|
object: 0,
|
|
8660
10097
|
group: 1,
|
|
@@ -8662,8 +10099,9 @@ function phase(step) {
|
|
|
8662
10099
|
}[kind];
|
|
8663
10100
|
}
|
|
8664
10101
|
function disagreement(step) {
|
|
8665
|
-
|
|
10102
|
+
const changed = (step.changes ?? []).flatMap((change) => {
|
|
8666
10103
|
if (change.unit === "options.order") return [];
|
|
10104
|
+
if (ORDERS.has(change.unit)) return orderDisagreement(step, change);
|
|
8667
10105
|
const member = memberOf(change.unit);
|
|
8668
10106
|
const desired = desiredValue(step.desired, change.unit);
|
|
8669
10107
|
let fits;
|
|
@@ -8672,6 +10110,19 @@ function disagreement(step) {
|
|
|
8672
10110
|
else fits = member?.field !== void 0 || fieldOf(change.unit) === change.unit ? sameValue(change.after, desired) : false;
|
|
8673
10111
|
return fits ? [] : [`step ${step.id} changes ${sanitize(change.unit)} to a value its desired values do not hold`];
|
|
8674
10112
|
});
|
|
10113
|
+
return [...carriedDisagreement(step), ...changed];
|
|
10114
|
+
}
|
|
10115
|
+
function orderDisagreement(step, change) {
|
|
10116
|
+
const desired = step.desired?.[change.unit] ?? [];
|
|
10117
|
+
const { after } = change;
|
|
10118
|
+
return Array.isArray(after) && stableStringify(desired.filter((id) => after.includes(id))) === stableStringify(after) ? [] : [`step ${step.id} writes a ${sanitize(change.unit)} order its desired values do not hold`];
|
|
10119
|
+
}
|
|
10120
|
+
function carriedDisagreement(step) {
|
|
10121
|
+
if (step.stages === void 0) return [];
|
|
10122
|
+
const prefix = `${step.address.replace("pipeline:", "stage:")}/`;
|
|
10123
|
+
const ids = step.stages.map((st) => st.address.slice(prefix.length));
|
|
10124
|
+
const desired = (step.desired?.stages ?? []).filter((id) => ids.includes(id));
|
|
10125
|
+
return step.action === "create" && kindOf$1(step.address) === "pipeline" && step.stages.every((st) => st.address.startsWith(prefix) && !st.address.slice(prefix.length).includes("/")) && stableStringify(desired) === stableStringify(ids) ? [] : [`step ${step.id} carries stages that are not its own pipeline's, in its order`];
|
|
8675
10126
|
}
|
|
8676
10127
|
function sameValue(a, b) {
|
|
8677
10128
|
return b !== void 0 && stableStringify(a) === stableStringify(b);
|
|
@@ -8693,11 +10144,11 @@ function destination(message, fix) {
|
|
|
8693
10144
|
function kindOf$1(address) {
|
|
8694
10145
|
return parseAddress(address).type;
|
|
8695
10146
|
}
|
|
8696
|
-
function writes(step) {
|
|
10147
|
+
function writes(step, warned) {
|
|
8697
10148
|
const changes = step.changes ?? [];
|
|
8698
10149
|
const live = step.expect.values?.options ?? [];
|
|
8699
10150
|
const label = (option, unit) => typeof option?.label === "string" ? `"${option.label}"` : String(memberOf(unit)?.value);
|
|
8700
|
-
return writesTail(changes, (c) => c.op === "add" ? label(c.after, c.unit) : label(Array.isArray(live) ? live.find((o) => o.value === memberOf(c.unit)?.value) : void 0, c.unit));
|
|
10151
|
+
return writesTail(changes, (c) => c.op === "add" ? label(c.after, c.unit) : label(Array.isArray(live) ? live.find((o) => o.value === memberOf(c.unit)?.value) : void 0, c.unit), warned ? fieldWords : (unit) => unit === "stages" ? fieldWords(unit) : unit);
|
|
8701
10152
|
}
|
|
8702
10153
|
/** How long a read-back waits before it tells a person watching that it is still waiting, once per step. */
|
|
8703
10154
|
const PROGRESS_MS = 3e3;
|
|
@@ -8874,12 +10325,16 @@ async function runSteps(run) {
|
|
|
8874
10325
|
reports.set(step.id, done.report);
|
|
8875
10326
|
run.issues.push(...done.issues ?? []);
|
|
8876
10327
|
stop ||= done.stop === true;
|
|
8877
|
-
|
|
8878
|
-
|
|
10328
|
+
const entries = entriesOf(step, done);
|
|
10329
|
+
if (entries.length > 0 && !sends) recorded.push(...entries);
|
|
10330
|
+
else if (entries.length > 0 && !saveEntries(run, entries)) stop = true;
|
|
8879
10331
|
}
|
|
8880
10332
|
saveEntries(run, recorded);
|
|
8881
10333
|
return reports;
|
|
8882
10334
|
}
|
|
10335
|
+
function entriesOf(step, done) {
|
|
10336
|
+
return [...done.entry === void 0 ? [] : [[step.address, done.entry]], ...done.also ?? []];
|
|
10337
|
+
}
|
|
8883
10338
|
function recordsOnly(step) {
|
|
8884
10339
|
return step.action === "release" || (step.action === "adopt" || step.action === "update") && (step.changes ?? []).length === 0;
|
|
8885
10340
|
}
|
|
@@ -8894,7 +10349,8 @@ function heldBack(run, step, reports) {
|
|
|
8894
10349
|
async function runStep(run, step) {
|
|
8895
10350
|
if (step.action === "release") return {
|
|
8896
10351
|
report: report$1(step, "done"),
|
|
8897
|
-
entry: null
|
|
10352
|
+
entry: null,
|
|
10353
|
+
also: stagesGone(run, step)
|
|
8898
10354
|
};
|
|
8899
10355
|
if (recordsOnly(step)) return record$2(run, step);
|
|
8900
10356
|
return await write$1(run, step);
|
|
@@ -8931,6 +10387,7 @@ async function attemptWrite(run, step, tries) {
|
|
|
8931
10387
|
const moved = before.present && before.resource === void 0 ? ["type"] : staleUnits(step, before.resource);
|
|
8932
10388
|
if (moved.length > 0) return stale(run, step, moved);
|
|
8933
10389
|
if (halted(run)) return stopped(run, step);
|
|
10390
|
+
if (kindOf(step.address) === "pipeline" && (step.changes ?? []).some((c) => c.unit === "stages")) return await reorderWrite(run, step, before, tries);
|
|
8934
10391
|
const sent = await send(run, payload(run, step, before));
|
|
8935
10392
|
if (sent.kind === "wait") return waited(run, step, sent, tries);
|
|
8936
10393
|
if (sent.kind !== "rejected") return await settle(run, step, sent);
|
|
@@ -8997,7 +10454,7 @@ async function settle(run, step, sent) {
|
|
|
8997
10454
|
function proven(step, seen, acknowledged, before) {
|
|
8998
10455
|
if (step.action === "delete") return seen.archived === true;
|
|
8999
10456
|
if (!(seen.present && seen.resource)) return false;
|
|
9000
|
-
if (unverifiedUnits(step, seen.resource).length === 0) return true;
|
|
10457
|
+
if (unverifiedUnits(step, seen.resource).length === 0 && carriedUnverified(step, seen).length === 0) return true;
|
|
9001
10458
|
if (!acknowledged) return false;
|
|
9002
10459
|
if (step.action === "create" || before === void 0) return true;
|
|
9003
10460
|
const fields = (step.changes ?? []).map((c) => fieldOf(c.unit));
|
|
@@ -9008,20 +10465,24 @@ function proven(step, seen, acknowledged, before) {
|
|
|
9008
10465
|
function verified(run, step, seen) {
|
|
9009
10466
|
if (step.action === "delete") return {
|
|
9010
10467
|
report: report$1(step, "done"),
|
|
9011
|
-
entry: null
|
|
10468
|
+
entry: null,
|
|
10469
|
+
also: stagesGone(run, step)
|
|
9012
10470
|
};
|
|
9013
10471
|
const readBack = seen.resource;
|
|
9014
|
-
const
|
|
10472
|
+
const own = unverifiedUnits(step, readBack);
|
|
10473
|
+
const bad = [...own, ...carriedUnverified(step, seen)];
|
|
9015
10474
|
if (step.action === "create") run.created.add(step.address);
|
|
9016
10475
|
const { rewrites: before, ...entry } = entryOf$1(run, step, verifiedBase(run, step, readBack));
|
|
9017
|
-
const rewrites = rewritesAfter(before, writtenUnits(step, readBack),
|
|
10476
|
+
const rewrites = rewritesAfter(before, writtenUnits(step, readBack), own);
|
|
9018
10477
|
const saved = rewrites === void 0 ? entry : {
|
|
9019
10478
|
...entry,
|
|
9020
10479
|
rewrites
|
|
9021
10480
|
};
|
|
10481
|
+
const also = carriedEntries(step, seen);
|
|
9022
10482
|
if (bad.length === 0) return {
|
|
9023
10483
|
report: report$1(step, "done"),
|
|
9024
|
-
entry: saved
|
|
10484
|
+
entry: saved,
|
|
10485
|
+
also
|
|
9025
10486
|
};
|
|
9026
10487
|
const units = bad.map((u) => u.unit);
|
|
9027
10488
|
const stored = bad.map((u) => `${u.unit} as ${show$1$1(u.observed)}, not ${show$1$1(u.desired)} as sent`).join("; ");
|
|
@@ -9036,9 +10497,41 @@ function verified(run, step, seen) {
|
|
|
9036
10497
|
issue: "W_UNVERIFIED"
|
|
9037
10498
|
}),
|
|
9038
10499
|
entry: saved,
|
|
10500
|
+
also,
|
|
9039
10501
|
issues: [issue]
|
|
9040
10502
|
};
|
|
9041
10503
|
}
|
|
10504
|
+
function carriedEntries(step, seen) {
|
|
10505
|
+
return (step.stages ?? []).flatMap((st) => {
|
|
10506
|
+
const live = seen.stages?.[st.address];
|
|
10507
|
+
if (live === void 0) return [];
|
|
10508
|
+
const units = classify(void 0, specOf(st.desired), capturedSpec(live), { options: "additive" });
|
|
10509
|
+
const base = advanceBase(void 0, specOf(st.desired), capturedSpec(live));
|
|
10510
|
+
const rewrites = rewritesAfter(void 0, units.map((u) => u.unit), units.filter((u) => u.class !== "converged"));
|
|
10511
|
+
const entry = {
|
|
10512
|
+
origin: "created",
|
|
10513
|
+
id: ownId(st.address),
|
|
10514
|
+
normVersion: NORM_VERSIONS.stage,
|
|
10515
|
+
...base === void 0 ? {} : { base },
|
|
10516
|
+
...rewrites === void 0 ? {} : { rewrites }
|
|
10517
|
+
};
|
|
10518
|
+
return [[st.address, entry]];
|
|
10519
|
+
});
|
|
10520
|
+
}
|
|
10521
|
+
function carriedUnverified(step, seen) {
|
|
10522
|
+
return (step.stages ?? []).flatMap((st) => {
|
|
10523
|
+
const live = seen.stages?.[st.address];
|
|
10524
|
+
if (live === void 0) return [{
|
|
10525
|
+
unit: `${st.address}`,
|
|
10526
|
+
class: "diverged",
|
|
10527
|
+
desired: st.desired
|
|
10528
|
+
}];
|
|
10529
|
+
return classify(void 0, specOf(st.desired), capturedSpec(live), { options: "additive" }).filter((u) => u.class !== "converged").map((u) => ({
|
|
10530
|
+
...u,
|
|
10531
|
+
unit: `${ownId(st.address)}.${u.unit}`
|
|
10532
|
+
}));
|
|
10533
|
+
});
|
|
10534
|
+
}
|
|
9042
10535
|
function verifiedBase(run, step, readBack) {
|
|
9043
10536
|
const live = capturedSpec(readBack);
|
|
9044
10537
|
if (step.action === "create") {
|
|
@@ -9146,12 +10639,38 @@ function refusal(run, step, sent, plan, otherwise) {
|
|
|
9146
10639
|
why: "HubSpot refuses to archive a group that still holds properties",
|
|
9147
10640
|
fix: `run ${plan}: it names the properties the group holds`
|
|
9148
10641
|
};
|
|
10642
|
+
const pipelineRefused = pipelineRefusal(step, sent, plan);
|
|
10643
|
+
if (pipelineRefused !== void 0) return pipelineRefused;
|
|
9149
10644
|
if (reason === "PROPERTY_WITH_NAME_EXISTS") return {
|
|
9150
10645
|
why: `HubSpot refuses the create because a property named ${portalName(run, step)} already exists`,
|
|
9151
10646
|
fix: `run ${plan}: it reads the portal again`
|
|
9152
10647
|
};
|
|
9153
10648
|
return otherwise;
|
|
9154
10649
|
}
|
|
10650
|
+
function pipelineRefusal(step, sent, plan) {
|
|
10651
|
+
const reason = reasonOf(sent);
|
|
10652
|
+
if (reason === "STAGE_ID_IN_USE") {
|
|
10653
|
+
const stages = sent.context?.stageIds?.[0];
|
|
10654
|
+
const records = sent.context?.objectIds?.[0];
|
|
10655
|
+
const held = [stages && `stages ${stages}`, records && `records ${records}`].filter(Boolean).join(", ");
|
|
10656
|
+
return {
|
|
10657
|
+
why: `HubSpot never deletes a stage a record sits in${held ? ` (${held})` : ""}`,
|
|
10658
|
+
fix: `move those records to another stage in HubSpot, or delete them, then run ${plan}`
|
|
10659
|
+
};
|
|
10660
|
+
}
|
|
10661
|
+
if (reason === "MISSING_CLOSED_STAGE") return {
|
|
10662
|
+
why: "HubSpot keeps a closed stage in every ticket pipeline",
|
|
10663
|
+
fix: `mark another stage ticketState: 'CLOSED' first, then run ${plan}`
|
|
10664
|
+
};
|
|
10665
|
+
if (reason === "STAGE_LABEL_EXISTS") return {
|
|
10666
|
+
why: "another stage of the pipeline has that label, ignoring case",
|
|
10667
|
+
fix: `run ${plan}: it reads the portal again`
|
|
10668
|
+
};
|
|
10669
|
+
return (reason === "STAGE_ID_EXISTS_IN_ANOTHER_PIPELINE" || sent.message.includes("already using stage id") || sent.message.includes("There's another pipeline")) && (kindOf(step.address) === "pipeline" || kindOf(step.address) === "stage") ? {
|
|
10670
|
+
why: "HubSpot refuses the ID because another pipeline or stage holds it; deals and tickets share pipeline IDs",
|
|
10671
|
+
fix: `give the pipeline or stage another ID in config, then run ${plan}`
|
|
10672
|
+
} : void 0;
|
|
10673
|
+
}
|
|
9155
10674
|
function usesOf(sent) {
|
|
9156
10675
|
const uses = (sent.errors ?? []).filter((e) => e.subCategory?.endsWith("PROPERTY_USAGE")).map((e) => {
|
|
9157
10676
|
return sanitize(`${(e.context?.parentDisplayType?.[0] ?? e.context?.parentType?.[0] ?? "use").toLowerCase().replaceAll("_", " ")} ${e.context?.parentName?.[0] ?? ""}`.trim(), USE_MAX);
|
|
@@ -9200,6 +10719,7 @@ async function find(run, step, archived) {
|
|
|
9200
10719
|
const key = objectOf(step.address);
|
|
9201
10720
|
const objectType = run.names.objectType(key);
|
|
9202
10721
|
const name = portalName(run, step);
|
|
10722
|
+
if (kind === "pipeline" || kind === "stage") return await findPipeline(run, step, archived);
|
|
9203
10723
|
if (kind === "group") {
|
|
9204
10724
|
const group = (await read$1(run, {
|
|
9205
10725
|
type: "group",
|
|
@@ -9253,10 +10773,51 @@ async function find(run, step, archived) {
|
|
|
9253
10773
|
raw
|
|
9254
10774
|
};
|
|
9255
10775
|
}
|
|
10776
|
+
async function findPipeline(run, step, archived) {
|
|
10777
|
+
const key = objectOf(step.address);
|
|
10778
|
+
const pipeline = kindOf(step.address) === "stage" ? pipelineOf(step.address) : step.address;
|
|
10779
|
+
let raw;
|
|
10780
|
+
try {
|
|
10781
|
+
raw = await read$1(run, {
|
|
10782
|
+
type: "pipeline",
|
|
10783
|
+
path: "read",
|
|
10784
|
+
params: {
|
|
10785
|
+
objectType: run.names.objectType(key),
|
|
10786
|
+
pipelineId: run.names.pipelineId(step.address)
|
|
10787
|
+
}
|
|
10788
|
+
});
|
|
10789
|
+
} catch (error) {
|
|
10790
|
+
if (error instanceof HubSpotApiError && error.status === 404) return archived ? {
|
|
10791
|
+
present: false,
|
|
10792
|
+
archived: true
|
|
10793
|
+
} : { present: false };
|
|
10794
|
+
throw error;
|
|
10795
|
+
}
|
|
10796
|
+
const [live] = localPipelines(key, normalizePipelines(key, [raw], !STANDARD_OBJECTS.has(key)), run.names);
|
|
10797
|
+
const resources = Object.fromEntries(pipelineResources(live ? [{
|
|
10798
|
+
...live,
|
|
10799
|
+
id: pipeline.slice(pipeline.lastIndexOf("/") + 1)
|
|
10800
|
+
}] : [], pipeline));
|
|
10801
|
+
const resource = resources[step.address];
|
|
10802
|
+
if (archived) return {
|
|
10803
|
+
present: resource !== void 0,
|
|
10804
|
+
archived: resource === void 0
|
|
10805
|
+
};
|
|
10806
|
+
const stages = Object.fromEntries(Object.entries(resources).filter(([address]) => address !== pipeline));
|
|
10807
|
+
return resource ? {
|
|
10808
|
+
present: true,
|
|
10809
|
+
raw,
|
|
10810
|
+
resource,
|
|
10811
|
+
stages
|
|
10812
|
+
} : {
|
|
10813
|
+
present: false,
|
|
10814
|
+
raw
|
|
10815
|
+
};
|
|
10816
|
+
}
|
|
9256
10817
|
async function appeared(run, step) {
|
|
9257
10818
|
try {
|
|
9258
10819
|
if ((await find(run, step, false)).present) return true;
|
|
9259
|
-
if (kindOf(step.address)
|
|
10820
|
+
if (kindOf(step.address) !== "property") return false;
|
|
9260
10821
|
const objectType = run.names.objectType(objectOf(step.address));
|
|
9261
10822
|
const name = portalName(run, step);
|
|
9262
10823
|
for (const dataSensitivity of [
|
|
@@ -9277,43 +10838,175 @@ async function appeared(run, step) {
|
|
|
9277
10838
|
if (error instanceof Stopped || error instanceof KalupError) return;
|
|
9278
10839
|
throw error;
|
|
9279
10840
|
}
|
|
9280
|
-
}
|
|
9281
|
-
function payload(run, step, before) {
|
|
9282
|
-
const kind = kindOf(step.address);
|
|
9283
|
-
const objectType = run.names.objectType(objectOf(step.address));
|
|
9284
|
-
const name = portalName(run, step);
|
|
10841
|
+
}
|
|
10842
|
+
function payload(run, step, before) {
|
|
10843
|
+
const kind = kindOf(step.address);
|
|
10844
|
+
const objectType = run.names.objectType(objectOf(step.address));
|
|
10845
|
+
const name = portalName(run, step);
|
|
10846
|
+
if (kind === "pipeline" || kind === "stage") return pipelinePayload(run, step, before, objectType);
|
|
10847
|
+
if (step.action === "create") {
|
|
10848
|
+
const group = (step.desired?.group)?.$ref;
|
|
10849
|
+
const body = createBody(step, {
|
|
10850
|
+
name,
|
|
10851
|
+
...group === void 0 ? {} : { group: run.names.portalName(group) }
|
|
10852
|
+
});
|
|
10853
|
+
return {
|
|
10854
|
+
type: kind,
|
|
10855
|
+
path: "create",
|
|
10856
|
+
params: { objectType },
|
|
10857
|
+
body
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
if (step.action === "delete") return {
|
|
10861
|
+
type: kind,
|
|
10862
|
+
path: "delete",
|
|
10863
|
+
params: {
|
|
10864
|
+
objectType,
|
|
10865
|
+
name
|
|
10866
|
+
}
|
|
10867
|
+
};
|
|
10868
|
+
const body = kind === "group" ? groupPatch(step.changes ?? []) : propertyPatch(step, before.raw, (ref) => run.names.portalName(ref));
|
|
10869
|
+
return {
|
|
10870
|
+
type: kind,
|
|
10871
|
+
path: "update",
|
|
10872
|
+
params: {
|
|
10873
|
+
objectType,
|
|
10874
|
+
name
|
|
10875
|
+
},
|
|
10876
|
+
body
|
|
10877
|
+
};
|
|
10878
|
+
}
|
|
10879
|
+
function pipelinePayload(run, step, before, objectType) {
|
|
10880
|
+
const pipelineId = run.names.pipelineId(step.address);
|
|
10881
|
+
if (kindOf(step.address) === "pipeline") {
|
|
10882
|
+
if (step.action === "create") return {
|
|
10883
|
+
type: "pipeline",
|
|
10884
|
+
path: "create",
|
|
10885
|
+
params: { objectType },
|
|
10886
|
+
body: pipelineCreateBody(step, pipelineId)
|
|
10887
|
+
};
|
|
10888
|
+
if (step.action === "delete") return {
|
|
10889
|
+
type: "pipeline",
|
|
10890
|
+
path: "delete",
|
|
10891
|
+
params: {
|
|
10892
|
+
objectType,
|
|
10893
|
+
pipelineId
|
|
10894
|
+
}
|
|
10895
|
+
};
|
|
10896
|
+
const body = pipelinePatch(step.changes ?? []);
|
|
10897
|
+
return {
|
|
10898
|
+
type: "pipeline",
|
|
10899
|
+
path: "update",
|
|
10900
|
+
params: {
|
|
10901
|
+
objectType,
|
|
10902
|
+
pipelineId
|
|
10903
|
+
},
|
|
10904
|
+
body
|
|
10905
|
+
};
|
|
10906
|
+
}
|
|
10907
|
+
const stageId = portalName(run, step);
|
|
9285
10908
|
if (step.action === "create") {
|
|
9286
|
-
const
|
|
9287
|
-
const
|
|
9288
|
-
|
|
9289
|
-
...group === void 0 ? {} : { group: run.names.portalName(group) }
|
|
9290
|
-
});
|
|
10909
|
+
const raw = before.raw;
|
|
10910
|
+
const highest = Math.max(-1, ...(raw?.stages ?? []).map((st) => st.displayOrder));
|
|
10911
|
+
const body = stageCreateBody(step.desired ?? {}, stageId, highest + 1);
|
|
9291
10912
|
return {
|
|
9292
|
-
type:
|
|
10913
|
+
type: "stage",
|
|
9293
10914
|
path: "create",
|
|
9294
|
-
params: {
|
|
10915
|
+
params: {
|
|
10916
|
+
objectType,
|
|
10917
|
+
pipelineId
|
|
10918
|
+
},
|
|
9295
10919
|
body
|
|
9296
10920
|
};
|
|
9297
10921
|
}
|
|
9298
10922
|
if (step.action === "delete") return {
|
|
9299
|
-
type:
|
|
10923
|
+
type: "stage",
|
|
9300
10924
|
path: "delete",
|
|
9301
10925
|
params: {
|
|
9302
10926
|
objectType,
|
|
9303
|
-
|
|
10927
|
+
pipelineId,
|
|
10928
|
+
stageId
|
|
9304
10929
|
}
|
|
9305
10930
|
};
|
|
9306
|
-
const body = kind === "group" ? groupPatch(step.changes ?? []) : propertyPatch(step, before.raw, (ref) => run.names.portalName(ref));
|
|
9307
10931
|
return {
|
|
9308
|
-
type:
|
|
10932
|
+
type: "stage",
|
|
9309
10933
|
path: "update",
|
|
9310
10934
|
params: {
|
|
9311
10935
|
objectType,
|
|
9312
|
-
|
|
10936
|
+
pipelineId,
|
|
10937
|
+
stageId
|
|
9313
10938
|
},
|
|
9314
|
-
body
|
|
10939
|
+
body: stagePatch(step.changes ?? [])
|
|
9315
10940
|
};
|
|
9316
10941
|
}
|
|
10942
|
+
/**
|
|
10943
|
+
* A pipeline step that writes the stage order: its label and displayOrder first, in one PATCH, then the stage moves.
|
|
10944
|
+
* HubSpot never stores two stages at one displayOrder: a write to a taken slot places the stage right after the stage
|
|
10945
|
+
* that held it and renumbers the pipeline 0..n-1 (observed 2026-10-05). So with D the order the step leaves
|
|
10946
|
+
* (stageOrder), each stage that is not right after its predecessor in D is moved onto that predecessor's slot, one
|
|
10947
|
+
* PATCH each, sent once, with a read of the pipeline before each move. The one write of several requests: a pipeline
|
|
10948
|
+
* PUT would drop any stage it does not name. Nothing follows a request whose outcome is uncertain: the step settles on
|
|
10949
|
+
* it. A wait counts against the step's `tries`; once any request of the step has landed, the retry's read no longer
|
|
10950
|
+
* meets the step's expect (the label or the stage order moved), so the step stops stale and the report says to plan
|
|
10951
|
+
* again, which shows what is left.
|
|
10952
|
+
*/
|
|
10953
|
+
async function reorderWrite(run, step, before, tries) {
|
|
10954
|
+
const objectType = run.names.objectType(objectOf(step.address));
|
|
10955
|
+
const pipelineId = run.names.pipelineId(step.address);
|
|
10956
|
+
const fields = pipelinePatch(step.changes ?? []);
|
|
10957
|
+
let last = {
|
|
10958
|
+
kind: "ok",
|
|
10959
|
+
status: 200,
|
|
10960
|
+
body: before.raw
|
|
10961
|
+
};
|
|
10962
|
+
if (Object.keys(fields).length > 0) {
|
|
10963
|
+
const sent = await send(run, {
|
|
10964
|
+
type: "pipeline",
|
|
10965
|
+
path: "update",
|
|
10966
|
+
params: {
|
|
10967
|
+
objectType,
|
|
10968
|
+
pipelineId
|
|
10969
|
+
},
|
|
10970
|
+
body: fields
|
|
10971
|
+
});
|
|
10972
|
+
if (sent.kind !== "ok") return await reorderStopped(run, step, sent, tries);
|
|
10973
|
+
last = sent;
|
|
10974
|
+
}
|
|
10975
|
+
const approved = step.changes?.find((c) => c.unit === "stages")?.after ?? [];
|
|
10976
|
+
let current = before;
|
|
10977
|
+
for (let move = 0;; move += 1) {
|
|
10978
|
+
const live = (current.resource?.definition?.stages ?? []).slice();
|
|
10979
|
+
const target = stageOrder(live, approved);
|
|
10980
|
+
const at = target.findIndex((id, i) => i > 0 && live.indexOf(id) !== live.indexOf(target[i - 1]) + 1);
|
|
10981
|
+
if (at === -1 || move > live.length || halted(run)) break;
|
|
10982
|
+
const raw = current.raw;
|
|
10983
|
+
const portal = (id) => run.names.portalName(`${step.address.replace("pipeline:", "stage:")}/${id}`);
|
|
10984
|
+
const slot = raw.stages.find((st) => st.id === portal(target[at - 1]))?.displayOrder;
|
|
10985
|
+
const sent = await send(run, {
|
|
10986
|
+
type: "stage",
|
|
10987
|
+
path: "update",
|
|
10988
|
+
params: {
|
|
10989
|
+
objectType,
|
|
10990
|
+
pipelineId,
|
|
10991
|
+
stageId: portal(target[at])
|
|
10992
|
+
},
|
|
10993
|
+
body: { displayOrder: slot }
|
|
10994
|
+
});
|
|
10995
|
+
if (sent.kind !== "ok") return await reorderStopped(run, step, sent, tries);
|
|
10996
|
+
last = sent;
|
|
10997
|
+
try {
|
|
10998
|
+
current = await find(run, step, false);
|
|
10999
|
+
} catch (error) {
|
|
11000
|
+
return failedRead(run, step, error);
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
return await settle(run, step, last);
|
|
11004
|
+
}
|
|
11005
|
+
async function reorderStopped(run, step, sent, tries) {
|
|
11006
|
+
if (sent.kind === "rejected") return rejected(run, step, sent);
|
|
11007
|
+
if (sent.kind === "wait") return waited(run, step, sent, tries);
|
|
11008
|
+
return await settle(run, step, sent);
|
|
11009
|
+
}
|
|
9317
11010
|
function dependent(run, step) {
|
|
9318
11011
|
return refsOf(step).some((ref) => run.created.has(ref));
|
|
9319
11012
|
}
|
|
@@ -9349,13 +11042,18 @@ function entryOf$1(run, step, base) {
|
|
|
9349
11042
|
if (base !== void 0) entry.base = base;
|
|
9350
11043
|
return entry;
|
|
9351
11044
|
}
|
|
11045
|
+
function stagesGone(run, step) {
|
|
11046
|
+
if (kindOf(step.address) !== "pipeline") return [];
|
|
11047
|
+
const prefix = `${step.address.replace("pipeline:", "stage:")}/`;
|
|
11048
|
+
return Object.keys(run.state.resources).filter((address) => address.startsWith(prefix)).map((address) => [address, null]);
|
|
11049
|
+
}
|
|
9352
11050
|
function saveEntries(run, entries) {
|
|
9353
11051
|
if (entries.length === 0) return true;
|
|
9354
11052
|
try {
|
|
9355
11053
|
const changed = save(run, (s) => {
|
|
9356
11054
|
const resources = { ...s.resources };
|
|
9357
|
-
for (const [
|
|
9358
|
-
else resources[
|
|
11055
|
+
for (const [address, entry] of entries) if (entry === null) delete resources[address];
|
|
11056
|
+
else resources[address] = entry;
|
|
9359
11057
|
return {
|
|
9360
11058
|
...s,
|
|
9361
11059
|
resources
|
|
@@ -9652,7 +11350,8 @@ function covers(written, unit) {
|
|
|
9652
11350
|
return unit === written || unit.startsWith(`${written}.`) || unit.startsWith(`${written}[`);
|
|
9653
11351
|
}
|
|
9654
11352
|
function names(body, name) {
|
|
9655
|
-
|
|
11353
|
+
const named = body;
|
|
11354
|
+
return typeof body === "object" && named !== null && (named.name === name || named.id === name);
|
|
9656
11355
|
}
|
|
9657
11356
|
function portalName(run, step) {
|
|
9658
11357
|
return run.names.portalName(step.address);
|
|
@@ -10069,7 +11768,7 @@ const OVERRIDE_COLUMNS = [
|
|
|
10069
11768
|
"Target",
|
|
10070
11769
|
"Value"
|
|
10071
11770
|
];
|
|
10072
|
-
const OVERRIDE_FIELDS = OVERRIDABLE.property;
|
|
11771
|
+
const OVERRIDE_FIELDS = [...OVERRIDABLE.property, ...OVERRIDABLE.stage.filter((f) => !OVERRIDABLE.property.includes(f))];
|
|
10073
11772
|
const LIFECYCLE_FIELDS = [
|
|
10074
11773
|
"options",
|
|
10075
11774
|
"removedOptions",
|
|
@@ -10110,6 +11809,19 @@ const OPTION_COLUMNS = {
|
|
|
10110
11809
|
"Hidden"
|
|
10111
11810
|
]
|
|
10112
11811
|
};
|
|
11812
|
+
const STAGE_COLUMNS = {
|
|
11813
|
+
config: [
|
|
11814
|
+
"Stage ID",
|
|
11815
|
+
"Key",
|
|
11816
|
+
"Label",
|
|
11817
|
+
"Closes"
|
|
11818
|
+
],
|
|
11819
|
+
snapshot: [
|
|
11820
|
+
"Stage ID",
|
|
11821
|
+
"Label",
|
|
11822
|
+
"Closes"
|
|
11823
|
+
]
|
|
11824
|
+
};
|
|
10113
11825
|
/**
|
|
10114
11826
|
* The page for a config IR, or for a snapshot with the coverage of its read. A config definition lists only the fields
|
|
10115
11827
|
* it states; a snapshot fills HubSpot's defaults for what the portal left out.
|
|
@@ -10182,6 +11894,11 @@ function coverageLines(coverage) {
|
|
|
10182
11894
|
objects.flatMap(([k, o]) => (o.unaddressable ?? []).map((name) => `${md(name)} on ${md(k)}`)),
|
|
10183
11895
|
", "
|
|
10184
11896
|
],
|
|
11897
|
+
[
|
|
11898
|
+
"Pipelines not read",
|
|
11899
|
+
objects.filter(([, o]) => o.pipelines?.status === "unreadable").map(([k, o]) => o.pipelines?.missingScope === void 0 ? md(k) : `${md(k)} (missing scope ${md(o.pipelines.missingScope)})`),
|
|
11900
|
+
", "
|
|
11901
|
+
],
|
|
10185
11902
|
[
|
|
10186
11903
|
"Shadowed by name overrides, not captured",
|
|
10187
11904
|
objects.flatMap(([k, o]) => o.shadowed ? [`${plural(o.shadowed.length, "name")} on ${md(k)}`] : []),
|
|
@@ -10193,8 +11910,13 @@ function coverageLines(coverage) {
|
|
|
10193
11910
|
const fields = [
|
|
10194
11911
|
"property",
|
|
10195
11912
|
"group",
|
|
10196
|
-
"object"
|
|
10197
|
-
|
|
11913
|
+
"object",
|
|
11914
|
+
"pipeline",
|
|
11915
|
+
"stage"
|
|
11916
|
+
].flatMap((type) => {
|
|
11917
|
+
const listed = notCaptured[type];
|
|
11918
|
+
return listed === void 0 ? [] : [`${type}: ${[...listed].sort(byCodeUnit).map(md).join(", ")}`];
|
|
11919
|
+
}).join("; ");
|
|
10198
11920
|
return [
|
|
10199
11921
|
coverage.complete ? COMPLETE : INCOMPLETE,
|
|
10200
11922
|
"",
|
|
@@ -10211,17 +11933,22 @@ function byObject(resources) {
|
|
|
10211
11933
|
const key = objectOf(address);
|
|
10212
11934
|
const entry = objects.get(key) ?? {
|
|
10213
11935
|
groups: [],
|
|
10214
|
-
properties: []
|
|
11936
|
+
properties: [],
|
|
11937
|
+
pipelines: [],
|
|
11938
|
+
stages: /* @__PURE__ */ new Map()
|
|
10215
11939
|
};
|
|
10216
11940
|
objects.set(key, entry);
|
|
10217
11941
|
const resource = resources[address];
|
|
10218
11942
|
if (type === "object") entry.object = resource;
|
|
10219
11943
|
else if (type === "group") entry.groups.push([nameOf(address), resource]);
|
|
10220
11944
|
else if (type === "property") entry.properties.push([nameOf(address), resource]);
|
|
11945
|
+
else if (type === "pipeline") entry.pipelines.push([nameOf(address), resource]);
|
|
11946
|
+
else if (type === "stage") entry.stages.set(address, resource);
|
|
10221
11947
|
}
|
|
10222
11948
|
return [...objects].sort(([a], [b]) => byCodeUnit(a, b));
|
|
10223
11949
|
}
|
|
10224
|
-
function objectLines(key,
|
|
11950
|
+
function objectLines(key, resources, config) {
|
|
11951
|
+
const { object, groups, properties } = resources;
|
|
10225
11952
|
const lines = [`## ${escapeMarkdown(key)}`];
|
|
10226
11953
|
if (object?.definition) lines.push("", ...schemaLines(object.definition));
|
|
10227
11954
|
if (groups.length > 0) {
|
|
@@ -10240,8 +11967,33 @@ function objectLines(key, { object, groups, properties }, config) {
|
|
|
10240
11967
|
lines.push("", `#### Options of ${escapeMarkdown(name)}`, "", ...table(columns, rows));
|
|
10241
11968
|
}
|
|
10242
11969
|
}
|
|
11970
|
+
for (const [id, p] of resources.pipelines) lines.push("", ...pipelineLines(key, id, p, resources.stages, config));
|
|
10243
11971
|
return lines;
|
|
10244
11972
|
}
|
|
11973
|
+
function pipelineLines(key, id, p, stages, config) {
|
|
11974
|
+
const d = p.definition ?? {};
|
|
11975
|
+
const rows = (d.stages ?? []).flatMap((stage) => {
|
|
11976
|
+
const s = stages.get(`stage:${key}/${id}/${stage}`);
|
|
11977
|
+
if (!s) return [];
|
|
11978
|
+
return [[
|
|
11979
|
+
escapeMarkdown(stage),
|
|
11980
|
+
...config ? [escapeMarkdown(s.binding?.key ?? "")] : [],
|
|
11981
|
+
escapeMarkdown(stringOf(s.definition?.label)),
|
|
11982
|
+
closes(s.definition ?? {})
|
|
11983
|
+
]];
|
|
11984
|
+
});
|
|
11985
|
+
return [
|
|
11986
|
+
`### Pipeline ${escapeMarkdown(stringOf(d.label))} (${escapeMarkdown(id)})`,
|
|
11987
|
+
"",
|
|
11988
|
+
`- Display order: ${typeof d.displayOrder === "number" ? d.displayOrder : ""}`,
|
|
11989
|
+
...rows.length > 0 ? ["", ...table(STAGE_COLUMNS[config ? "config" : "snapshot"], rows)] : []
|
|
11990
|
+
];
|
|
11991
|
+
}
|
|
11992
|
+
function closes(d) {
|
|
11993
|
+
if (typeof d.probability === "number") return `probability ${d.probability}`;
|
|
11994
|
+
const state = d.ticketState ?? d.state;
|
|
11995
|
+
return typeof state === "string" ? escapeMarkdown(state) : "";
|
|
11996
|
+
}
|
|
10245
11997
|
function schemaLines(definition) {
|
|
10246
11998
|
const labels = definition.labels ?? {};
|
|
10247
11999
|
const names = (value, sort) => {
|
|
@@ -10552,7 +12304,8 @@ const scopeKeys = every()([
|
|
|
10552
12304
|
"include",
|
|
10553
12305
|
"exclude",
|
|
10554
12306
|
"custom",
|
|
10555
|
-
"as"
|
|
12307
|
+
"as",
|
|
12308
|
+
"pipelines"
|
|
10556
12309
|
]);
|
|
10557
12310
|
const targetKeys = every()([
|
|
10558
12311
|
"portalId",
|
|
@@ -10592,6 +12345,12 @@ const definitionKeys = every()([
|
|
|
10592
12345
|
"dataSensitivity",
|
|
10593
12346
|
"lifecycle"
|
|
10594
12347
|
]);
|
|
12348
|
+
const overrideDefinitionKeys = every()([
|
|
12349
|
+
...definitionKeys,
|
|
12350
|
+
"probability",
|
|
12351
|
+
"ticketState",
|
|
12352
|
+
"state"
|
|
12353
|
+
]);
|
|
10595
12354
|
const optionKeys = every()([
|
|
10596
12355
|
"value",
|
|
10597
12356
|
"label",
|
|
@@ -10599,6 +12358,13 @@ const optionKeys = every()([
|
|
|
10599
12358
|
"hidden",
|
|
10600
12359
|
"description"
|
|
10601
12360
|
]);
|
|
12361
|
+
const stageKeys = every()([
|
|
12362
|
+
"id",
|
|
12363
|
+
"label",
|
|
12364
|
+
"probability",
|
|
12365
|
+
"ticketState",
|
|
12366
|
+
"state"
|
|
12367
|
+
]);
|
|
10602
12368
|
const lifecycleKeys = every()([
|
|
10603
12369
|
"options",
|
|
10604
12370
|
"removedOptions",
|
|
@@ -10629,6 +12395,7 @@ function escapeString(s, quote = "'") {
|
|
|
10629
12395
|
}
|
|
10630
12396
|
function write(kind, data) {
|
|
10631
12397
|
if (kind === "object") return writeObjectFile(data);
|
|
12398
|
+
if (kind === "pipeline") return writePipelineFile(data);
|
|
10632
12399
|
if (kind === "config") return writeConfigFile(data);
|
|
10633
12400
|
if (kind === "removed") return writeRemovedFile(data);
|
|
10634
12401
|
return writeBarrel(data);
|
|
@@ -10765,9 +12532,31 @@ function writeObjectFile(f) {
|
|
|
10765
12532
|
});
|
|
10766
12533
|
return `${out.join("\n")}\n`;
|
|
10767
12534
|
}
|
|
12535
|
+
function writePipelineFile(f) {
|
|
12536
|
+
const out = [
|
|
12537
|
+
...header(f.header),
|
|
12538
|
+
"import { definePipeline } from '@kalup/core'",
|
|
12539
|
+
...f.imports,
|
|
12540
|
+
""
|
|
12541
|
+
];
|
|
12542
|
+
f.exports.forEach((e, i) => {
|
|
12543
|
+
const body = [
|
|
12544
|
+
...wrap("id: ", e.id, ",", " "),
|
|
12545
|
+
...wrap("label: ", e.label, ",", " "),
|
|
12546
|
+
` displayOrder: ${e.displayOrder},`,
|
|
12547
|
+
...block(" stages: ", e.stages.flatMap((st) => [...comment(st.comments, " "), ...wrap(`${key(st.key)}: `, pick$1(st, stageKeys), ",", " ")]), " ", ",")
|
|
12548
|
+
];
|
|
12549
|
+
if (i) out.push("");
|
|
12550
|
+
out.push(...comment(e.comments, ""), ...block(`export const ${e.name} = definePipeline(${q(e.object)}, `, body, "", ")"));
|
|
12551
|
+
});
|
|
12552
|
+
return `${out.join("\n")}\n`;
|
|
12553
|
+
}
|
|
10768
12554
|
function override(o) {
|
|
10769
12555
|
const out = pick$1(o, overrideKeys);
|
|
10770
|
-
if (o.definition) out.definition =
|
|
12556
|
+
if (o.definition) out.definition = {
|
|
12557
|
+
...canon(o.definition),
|
|
12558
|
+
...pick$1(o.definition, overrideDefinitionKeys.slice(definitionKeys.length))
|
|
12559
|
+
};
|
|
10771
12560
|
return out;
|
|
10772
12561
|
}
|
|
10773
12562
|
function target(t) {
|
|
@@ -10823,9 +12612,11 @@ function writeRemovedFile(r) {
|
|
|
10823
12612
|
function writeBarrel(entries) {
|
|
10824
12613
|
const byFrom = /* @__PURE__ */ new Map();
|
|
10825
12614
|
for (const e of [...entries].sort((a, b) => cmp$1(a.from, b.from) || cmp$1(a.name, b.name))) byFrom.set(e.from, [...byFrom.get(e.from) ?? [], e.name]);
|
|
12615
|
+
const pipelines = new Set(entries.filter((e) => e.pipeline).map((e) => `${e.from}\0${e.name}`));
|
|
10826
12616
|
const out = [];
|
|
10827
12617
|
for (const [from, names] of byFrom) {
|
|
10828
|
-
|
|
12618
|
+
const typed = names.filter((n) => !pipelines.has(`${from}\0${n}`));
|
|
12619
|
+
if (typed.length) out.push(`export type { ${typed.map((n) => `${n}Data`).join(", ")} } from ${q(`${from}.js`)}`);
|
|
10829
12620
|
out.push(`export { ${names.join(", ")} } from ${q(`${from}.js`)}`);
|
|
10830
12621
|
}
|
|
10831
12622
|
return `${out.length ? out.join("\n") : "export {}"}\n`;
|
|
@@ -11849,7 +13640,7 @@ function createReport() {
|
|
|
11849
13640
|
};
|
|
11850
13641
|
const changes = [];
|
|
11851
13642
|
const note = (change) => {
|
|
11852
|
-
changes.push(scrub$
|
|
13643
|
+
changes.push(scrub$1(change));
|
|
11853
13644
|
};
|
|
11854
13645
|
return {
|
|
11855
13646
|
counts,
|
|
@@ -12219,7 +14010,7 @@ function resolveUnits(address, mine, merged, fields, r) {
|
|
|
12219
14010
|
const member = MEMBER.exec(u.unit);
|
|
12220
14011
|
if (u.unit === "options.order") resolveOrder(at, u);
|
|
12221
14012
|
else if (member) resolveMember(at, u, member[1], member[2]);
|
|
12222
|
-
else if (u.unit !== "type" && keepsFile(at, u)) {
|
|
14013
|
+
else if (u.unit !== "type" && keepsFile$1(at, u)) {
|
|
12223
14014
|
noteUnit(at, u, mine[u.unit], merged[u.unit]);
|
|
12224
14015
|
drop(at, "changed", u.unit);
|
|
12225
14016
|
put$1(merged, u.unit, mine[u.unit]);
|
|
@@ -12228,7 +14019,7 @@ function resolveUnits(address, mine, merged, fields, r) {
|
|
|
12228
14019
|
if (merged.options?.length === 0 && mine.options === void 0) merged.options = void 0;
|
|
12229
14020
|
compactInPlace(merged);
|
|
12230
14021
|
}
|
|
12231
|
-
function keepsFile(at, u) {
|
|
14022
|
+
function keepsFile$1(at, u) {
|
|
12232
14023
|
return (u.class === "config-change" || u.class === "conflict") && !at.r.accept(u.unit);
|
|
12233
14024
|
}
|
|
12234
14025
|
function noteUnit(at, u, before, after) {
|
|
@@ -12246,8 +14037,8 @@ function drop(at, kind, field) {
|
|
|
12246
14037
|
}
|
|
12247
14038
|
function resolveOrder(at, u) {
|
|
12248
14039
|
const { merged, mine } = at;
|
|
12249
|
-
if (merged.options && keepsFile(at, u)) {
|
|
12250
|
-
const ordered = inFileOrder(mine.options, merged.options);
|
|
14040
|
+
if (merged.options && keepsFile$1(at, u)) {
|
|
14041
|
+
const ordered = inFileOrder$1(mine.options, merged.options);
|
|
12251
14042
|
noteUnit(at, u, commonValues(ordered, mine.options), commonValues(merged.options, mine.options));
|
|
12252
14043
|
merged.options = ordered;
|
|
12253
14044
|
}
|
|
@@ -12260,7 +14051,7 @@ function resolveMember(at, u, value, field) {
|
|
|
12260
14051
|
merged.options = merged.options?.filter((o) => o.value !== value);
|
|
12261
14052
|
};
|
|
12262
14053
|
if (field !== void 0) {
|
|
12263
|
-
if (theirs && ours && keepsFile(at, u)) {
|
|
14054
|
+
if (theirs && ours && keepsFile$1(at, u)) {
|
|
12264
14055
|
noteUnit(at, u, ours[field], theirs[field]);
|
|
12265
14056
|
drop(at, "changed", u.unit);
|
|
12266
14057
|
put$1(theirs, field, ours[field]);
|
|
@@ -12313,7 +14104,7 @@ function resolveMember(at, u, value, field) {
|
|
|
12313
14104
|
});
|
|
12314
14105
|
}
|
|
12315
14106
|
}
|
|
12316
|
-
function inFileOrder(mine, merged) {
|
|
14107
|
+
function inFileOrder$1(mine, merged) {
|
|
12317
14108
|
const inFile = new Set((mine ?? []).map((o) => o.value));
|
|
12318
14109
|
const byValue = new Map(merged.filter((o) => inFile.has(o.value)).map((o) => [o.value, o]));
|
|
12319
14110
|
const queue = (mine ?? []).flatMap((o) => {
|
|
@@ -12441,7 +14232,7 @@ function cmp(a, b) {
|
|
|
12441
14232
|
if (a < b) return -1;
|
|
12442
14233
|
return a > b ? 1 : 0;
|
|
12443
14234
|
}
|
|
12444
|
-
function scrub$
|
|
14235
|
+
function scrub$1(change) {
|
|
12445
14236
|
return compact({
|
|
12446
14237
|
...change,
|
|
12447
14238
|
address: sanitize(change.address),
|
|
@@ -12551,6 +14342,59 @@ function targetOnly(file, overrides) {
|
|
|
12551
14342
|
};
|
|
12552
14343
|
};
|
|
12553
14344
|
}
|
|
14345
|
+
/** A pipeline export as target T sees it: the pipeline's and each stage's overridden fields in place of the file's. */
|
|
14346
|
+
function pipelineAsTarget(e, overrides) {
|
|
14347
|
+
const at = `${e.object}/${e.id}`;
|
|
14348
|
+
const d = definitionOf(overrides, `pipeline:${at}`);
|
|
14349
|
+
return {
|
|
14350
|
+
...e,
|
|
14351
|
+
...d ? pickFields(d, OVERRIDABLE.pipeline) : {},
|
|
14352
|
+
stages: e.stages.map((st) => {
|
|
14353
|
+
const sd = definitionOf(overrides, `stage:${at}/${st.id}`);
|
|
14354
|
+
return sd ? {
|
|
14355
|
+
...st,
|
|
14356
|
+
...pickFields(sd, OVERRIDABLE.stage)
|
|
14357
|
+
} : st;
|
|
14358
|
+
})
|
|
14359
|
+
};
|
|
14360
|
+
}
|
|
14361
|
+
/**
|
|
14362
|
+
* Splits a pipeline export merged from pipelineAsTarget's view against `file`, the export as written: each field T
|
|
14363
|
+
* overrides takes the merged value into T's override and the file's own value back.
|
|
14364
|
+
*/
|
|
14365
|
+
function pipelineFromTarget(merged, file, overrides) {
|
|
14366
|
+
const changed = {};
|
|
14367
|
+
const split = (address, fields, value, mine) => {
|
|
14368
|
+
const override = own(overrides, address);
|
|
14369
|
+
const d = definitionOf(overrides, address);
|
|
14370
|
+
if (!(override && d && mine)) return value;
|
|
14371
|
+
const taken = fields.filter((f) => d[f] !== void 0);
|
|
14372
|
+
const definition = { ...d };
|
|
14373
|
+
const out = { ...value };
|
|
14374
|
+
for (const field of taken) {
|
|
14375
|
+
definition[field] = value[field];
|
|
14376
|
+
out[field] = mine[field];
|
|
14377
|
+
}
|
|
14378
|
+
if (stableStringify(definition) !== stableStringify(d)) changed[address] = {
|
|
14379
|
+
...override,
|
|
14380
|
+
definition
|
|
14381
|
+
};
|
|
14382
|
+
return out;
|
|
14383
|
+
};
|
|
14384
|
+
const at = `${merged.object}/${merged.id}`;
|
|
14385
|
+
const pipeline = split(`pipeline:${at}`, OVERRIDABLE.pipeline, merged, file);
|
|
14386
|
+
const stages = merged.stages.map((st) => split(`stage:${at}/${st.id}`, OVERRIDABLE.stage, st, file.stages.find((f) => f.id === st.id)));
|
|
14387
|
+
return {
|
|
14388
|
+
export: {
|
|
14389
|
+
...pipeline,
|
|
14390
|
+
stages
|
|
14391
|
+
},
|
|
14392
|
+
overrides: changed
|
|
14393
|
+
};
|
|
14394
|
+
}
|
|
14395
|
+
function pickFields(d, fields) {
|
|
14396
|
+
return Object.fromEntries(fields.filter((f) => d[f] !== void 0).map((f) => [f, d[f]]));
|
|
14397
|
+
}
|
|
12554
14398
|
function definitionOf(overrides, address) {
|
|
12555
14399
|
const override = own(overrides, address);
|
|
12556
14400
|
return override?.skip === true ? void 0 : override?.definition;
|
|
@@ -12569,6 +14413,297 @@ function pick$4(d, fields) {
|
|
|
12569
14413
|
function own(record, key) {
|
|
12570
14414
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
12571
14415
|
}
|
|
14416
|
+
const SLUG = /^[a-z][a-z0-9_]*$/;
|
|
14417
|
+
const NOT_ALNUM = /[^A-Za-z0-9]+/;
|
|
14418
|
+
const LETTER = /[A-Za-z]/;
|
|
14419
|
+
const DIGIT_FIRST = /^[0-9]/;
|
|
14420
|
+
function mergePipelines(input) {
|
|
14421
|
+
const { live, local, object, only, removed } = input;
|
|
14422
|
+
const out = {
|
|
14423
|
+
changes: [],
|
|
14424
|
+
counts: {
|
|
14425
|
+
added: 0,
|
|
14426
|
+
changed: 0,
|
|
14427
|
+
unchanged: 0,
|
|
14428
|
+
missing: 0
|
|
14429
|
+
},
|
|
14430
|
+
fresh: [],
|
|
14431
|
+
merged: /* @__PURE__ */ new Map()
|
|
14432
|
+
};
|
|
14433
|
+
const note = (change) => out.changes.push(scrub$3(change));
|
|
14434
|
+
const settle = (fields) => {
|
|
14435
|
+
if (fields.some((c) => c.kind === "changed" || c.kind === "added")) out.counts.changed += 1;
|
|
14436
|
+
else out.counts.unchanged += 1;
|
|
14437
|
+
fields.forEach(note);
|
|
14438
|
+
};
|
|
14439
|
+
const ours = new Set(local.map((p) => p.id));
|
|
14440
|
+
for (const p of local) out.merged.set(p.name, mergeOne$1(input, p, {
|
|
14441
|
+
note,
|
|
14442
|
+
settle,
|
|
14443
|
+
counts: out.counts
|
|
14444
|
+
}));
|
|
14445
|
+
for (const l of live) {
|
|
14446
|
+
const address = `pipeline:${object}/${l.id}`;
|
|
14447
|
+
if (!input.all || ours.has(l.id) || !(only(address) || l.stages.some((st) => only(stageAt(object, l.id, st.id))))) continue;
|
|
14448
|
+
if (removed?.has(address)) {
|
|
14449
|
+
note({
|
|
14450
|
+
kind: "removed",
|
|
14451
|
+
address
|
|
14452
|
+
});
|
|
14453
|
+
continue;
|
|
14454
|
+
}
|
|
14455
|
+
const fresh = freshExport$1(input, l);
|
|
14456
|
+
out.fresh.push(fresh);
|
|
14457
|
+
out.counts.added += 1 + fresh.stages.length;
|
|
14458
|
+
note({
|
|
14459
|
+
kind: "added",
|
|
14460
|
+
address
|
|
14461
|
+
});
|
|
14462
|
+
for (const st of fresh.stages) note({
|
|
14463
|
+
kind: "added",
|
|
14464
|
+
address: stageAt(object, l.id, st.id)
|
|
14465
|
+
});
|
|
14466
|
+
}
|
|
14467
|
+
return out;
|
|
14468
|
+
}
|
|
14469
|
+
function mergeOne$1(input, p, report) {
|
|
14470
|
+
const { object, only, excluded } = input;
|
|
14471
|
+
const address = `pipeline:${object}/${p.id}`;
|
|
14472
|
+
const l = input.live.find((x) => x.id === p.id);
|
|
14473
|
+
if (!(only(address) || p.stages.some((st) => only(stageAt(object, p.id, st.id))))) return p;
|
|
14474
|
+
if (excluded.has(address)) {
|
|
14475
|
+
report.note({
|
|
14476
|
+
kind: "excluded",
|
|
14477
|
+
address
|
|
14478
|
+
});
|
|
14479
|
+
return p;
|
|
14480
|
+
}
|
|
14481
|
+
if (!l) {
|
|
14482
|
+
report.counts.missing += 1;
|
|
14483
|
+
report.note({
|
|
14484
|
+
kind: "missing",
|
|
14485
|
+
address
|
|
14486
|
+
});
|
|
14487
|
+
return p;
|
|
14488
|
+
}
|
|
14489
|
+
const next = { ...p };
|
|
14490
|
+
if (only(address)) {
|
|
14491
|
+
const fields = [];
|
|
14492
|
+
for (const field of ["label", "displayOrder"]) {
|
|
14493
|
+
if (p[field] !== l[field]) fields.push({
|
|
14494
|
+
kind: "changed",
|
|
14495
|
+
address,
|
|
14496
|
+
field,
|
|
14497
|
+
before: p[field],
|
|
14498
|
+
after: l[field]
|
|
14499
|
+
});
|
|
14500
|
+
Object.assign(next, { [field]: l[field] });
|
|
14501
|
+
}
|
|
14502
|
+
resolveFields(address, p, next, fields, input.resolve?.(address));
|
|
14503
|
+
next.stages = mergeStages(input, p, l, next, fields, report);
|
|
14504
|
+
report.settle(fields);
|
|
14505
|
+
} else next.stages = mergeStages(input, p, l, next, [], report);
|
|
14506
|
+
return next;
|
|
14507
|
+
}
|
|
14508
|
+
function mergeStages(input, p, l, next, fields, report) {
|
|
14509
|
+
const merged = portalStages(input, p, l, report);
|
|
14510
|
+
keepFileOnly(input, p, merged, report);
|
|
14511
|
+
return orderStages(input, p, merged, next, fields);
|
|
14512
|
+
}
|
|
14513
|
+
function portalStages(input, p, l, report) {
|
|
14514
|
+
const { object, only, removed } = input;
|
|
14515
|
+
const mine = new Map(p.stages.map((st) => [st.id, st]));
|
|
14516
|
+
const keys = new Set(p.stages.map((st) => st.key));
|
|
14517
|
+
const merged = [];
|
|
14518
|
+
for (const ls of l.stages) {
|
|
14519
|
+
const address = stageAt(object, p.id, ls.id);
|
|
14520
|
+
const st = mine.get(ls.id);
|
|
14521
|
+
const taken = only(address);
|
|
14522
|
+
if (st) merged.push(taken ? mergeStage(input, st, ls, address, report) : st);
|
|
14523
|
+
else if (removed?.has(address)) report.note({
|
|
14524
|
+
kind: "removed",
|
|
14525
|
+
address
|
|
14526
|
+
});
|
|
14527
|
+
else if (taken) {
|
|
14528
|
+
const key = stageKey(ls, keys);
|
|
14529
|
+
keys.add(key);
|
|
14530
|
+
merged.push(stageOf(ls, key, []));
|
|
14531
|
+
report.counts.added += 1;
|
|
14532
|
+
report.note({
|
|
14533
|
+
kind: "added",
|
|
14534
|
+
address
|
|
14535
|
+
});
|
|
14536
|
+
}
|
|
14537
|
+
}
|
|
14538
|
+
return merged;
|
|
14539
|
+
}
|
|
14540
|
+
function keepFileOnly(input, p, merged, report) {
|
|
14541
|
+
const order = p.stages.map((st) => st.id);
|
|
14542
|
+
for (const [index, st] of p.stages.entries()) {
|
|
14543
|
+
if (merged.some((m) => m.id === st.id)) continue;
|
|
14544
|
+
const address = stageAt(input.object, p.id, st.id);
|
|
14545
|
+
if (input.only(address) && input.excluded.has(address)) report.note({
|
|
14546
|
+
kind: "excluded",
|
|
14547
|
+
address
|
|
14548
|
+
});
|
|
14549
|
+
else if (input.only(address)) {
|
|
14550
|
+
report.counts.missing += 1;
|
|
14551
|
+
report.note({
|
|
14552
|
+
kind: "missing",
|
|
14553
|
+
address
|
|
14554
|
+
});
|
|
14555
|
+
}
|
|
14556
|
+
const before = order.slice(0, index).reverse().find((id) => merged.some((m) => m.id === id));
|
|
14557
|
+
merged.splice(before === void 0 ? 0 : merged.findIndex((m) => m.id === before) + 1, 0, st);
|
|
14558
|
+
}
|
|
14559
|
+
}
|
|
14560
|
+
function orderStages(input, p, merged, next, fields) {
|
|
14561
|
+
const pipeline = `pipeline:${input.object}/${p.id}`;
|
|
14562
|
+
const order = p.stages.map((st) => st.id);
|
|
14563
|
+
const resolution = input.resolve?.(pipeline);
|
|
14564
|
+
const unit = resolution?.units.find((u) => u.unit === "stages");
|
|
14565
|
+
const ids = (stages) => stages.map((st) => st.id).filter((id) => order.includes(id));
|
|
14566
|
+
if (!input.only(pipeline)) return inFileOrder(order, merged);
|
|
14567
|
+
if (unit && resolution && keepsFile(unit, resolution)) {
|
|
14568
|
+
const kept = inFileOrder(order, merged);
|
|
14569
|
+
const kind = unit.class === "conflict" ? "conflict" : "kept";
|
|
14570
|
+
fields.push({
|
|
14571
|
+
kind,
|
|
14572
|
+
address: pipeline,
|
|
14573
|
+
field: "stages",
|
|
14574
|
+
before: ids(kept),
|
|
14575
|
+
after: ids(merged)
|
|
14576
|
+
});
|
|
14577
|
+
next.stages = kept;
|
|
14578
|
+
return kept;
|
|
14579
|
+
}
|
|
14580
|
+
const was = order.filter((id) => merged.some((m) => m.id === id));
|
|
14581
|
+
const now = ids(merged);
|
|
14582
|
+
if (was.join("\0") !== now.join("\0")) fields.push({
|
|
14583
|
+
kind: "changed",
|
|
14584
|
+
address: pipeline,
|
|
14585
|
+
field: "stages",
|
|
14586
|
+
before: was,
|
|
14587
|
+
after: now
|
|
14588
|
+
});
|
|
14589
|
+
return merged;
|
|
14590
|
+
}
|
|
14591
|
+
function mergeStage(input, st, ls, address, report) {
|
|
14592
|
+
const fields = [];
|
|
14593
|
+
const next = stageOf(ls, st.key, st.comments);
|
|
14594
|
+
if (st.label !== ls.label) fields.push({
|
|
14595
|
+
kind: "changed",
|
|
14596
|
+
address,
|
|
14597
|
+
field: "label",
|
|
14598
|
+
before: st.label,
|
|
14599
|
+
after: ls.label
|
|
14600
|
+
});
|
|
14601
|
+
for (const field of STAGE_FIELDS) if (st[field] !== ls[field]) fields.push({
|
|
14602
|
+
kind: "changed",
|
|
14603
|
+
address,
|
|
14604
|
+
field,
|
|
14605
|
+
before: st[field],
|
|
14606
|
+
after: ls[field]
|
|
14607
|
+
});
|
|
14608
|
+
const merged = { ...next };
|
|
14609
|
+
resolveFields(address, st, merged, fields, input.resolve?.(address));
|
|
14610
|
+
report.settle(fields);
|
|
14611
|
+
return merged;
|
|
14612
|
+
}
|
|
14613
|
+
function resolveFields(address, mine, merged, fields, r) {
|
|
14614
|
+
for (const u of r?.units ?? []) {
|
|
14615
|
+
if (u.unit === "stages" || !keepsFile(u, r)) continue;
|
|
14616
|
+
const at = fields.findIndex((c) => c.kind === "changed" && c.field === u.unit);
|
|
14617
|
+
if (at >= 0) fields.splice(at, 1);
|
|
14618
|
+
fields.push({
|
|
14619
|
+
kind: u.class === "conflict" ? "conflict" : "kept",
|
|
14620
|
+
address,
|
|
14621
|
+
field: u.unit,
|
|
14622
|
+
before: mine[u.unit],
|
|
14623
|
+
after: merged[u.unit]
|
|
14624
|
+
});
|
|
14625
|
+
if (mine[u.unit] === void 0) delete merged[u.unit];
|
|
14626
|
+
else merged[u.unit] = mine[u.unit];
|
|
14627
|
+
}
|
|
14628
|
+
}
|
|
14629
|
+
function keepsFile(u, r) {
|
|
14630
|
+
return (u.class === "config-change" || u.class === "conflict") && !r.accept(u.unit);
|
|
14631
|
+
}
|
|
14632
|
+
function inFileOrder(order, merged) {
|
|
14633
|
+
const queue = order.flatMap((id) => merged.filter((st) => st.id === id));
|
|
14634
|
+
return merged.map((st) => order.includes(st.id) ? queue.shift() ?? st : st);
|
|
14635
|
+
}
|
|
14636
|
+
function stageOf(ls, key, comments) {
|
|
14637
|
+
const out = {
|
|
14638
|
+
key,
|
|
14639
|
+
id: ls.id,
|
|
14640
|
+
label: ls.label,
|
|
14641
|
+
comments
|
|
14642
|
+
};
|
|
14643
|
+
for (const field of STAGE_FIELDS) if (ls[field] !== void 0) Object.assign(out, { [field]: ls[field] });
|
|
14644
|
+
return out;
|
|
14645
|
+
}
|
|
14646
|
+
function freshExport$1(input, l) {
|
|
14647
|
+
const keys = /* @__PURE__ */ new Set();
|
|
14648
|
+
const stages = l.stages.map((ls) => {
|
|
14649
|
+
const key = stageKey(ls, keys);
|
|
14650
|
+
keys.add(key);
|
|
14651
|
+
return stageOf(ls, key, []);
|
|
14652
|
+
});
|
|
14653
|
+
const name = unique$1(pipelineExportName(l.label, l.id), input.taken);
|
|
14654
|
+
input.taken.add(name);
|
|
14655
|
+
return {
|
|
14656
|
+
name,
|
|
14657
|
+
object: input.object,
|
|
14658
|
+
id: l.id,
|
|
14659
|
+
label: l.label,
|
|
14660
|
+
displayOrder: l.displayOrder,
|
|
14661
|
+
stages,
|
|
14662
|
+
comments: []
|
|
14663
|
+
};
|
|
14664
|
+
}
|
|
14665
|
+
/**
|
|
14666
|
+
* The export name pull gives a pipeline: PascalCase of the words of its label, followed by `Pipeline` unless the label
|
|
14667
|
+
* ends with that word, `SalesPipeline` for "Sales Pipeline"; the ID's words when the label has no letter.
|
|
14668
|
+
*/
|
|
14669
|
+
function pipelineExportName(label, id) {
|
|
14670
|
+
const words = (text) => text.split(NOT_ALNUM).filter(Boolean);
|
|
14671
|
+
const name = (words(label).some((w) => LETTER.test(w)) ? words(label) : words(id)).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
14672
|
+
const base = DIGIT_FIRST.test(name) || name === "" ? `Pipeline${name}` : name;
|
|
14673
|
+
return base.endsWith("Pipeline") ? base : `${base}Pipeline`;
|
|
14674
|
+
}
|
|
14675
|
+
/**
|
|
14676
|
+
* The key pull gives a stage: camelCase of its ID when the ID is a lowercase slug, else of its label, with `stage` in
|
|
14677
|
+
* front of one that would start with a digit, unique in the pipeline.
|
|
14678
|
+
*/
|
|
14679
|
+
function stageKey(ls, taken) {
|
|
14680
|
+
const words = (text) => text.toLowerCase().split(NOT_ALNUM).filter(Boolean).join("_");
|
|
14681
|
+
const base = camelCase(SLUG.test(ls.id) ? ls.id : words(ls.label) || words(ls.id)) || "stage";
|
|
14682
|
+
return unique$1(DIGIT_FIRST.test(base) ? `stage${base}` : base, taken);
|
|
14683
|
+
}
|
|
14684
|
+
function unique$1(name, taken) {
|
|
14685
|
+
if (!taken.has(name)) return name;
|
|
14686
|
+
let n = 2;
|
|
14687
|
+
while (taken.has(`${name}${n}`)) n += 1;
|
|
14688
|
+
return `${name}${n}`;
|
|
14689
|
+
}
|
|
14690
|
+
function stageAt(object, pipeline, stage) {
|
|
14691
|
+
return `stage:${object}/${pipeline}/${stage}`;
|
|
14692
|
+
}
|
|
14693
|
+
function scrub$3(change) {
|
|
14694
|
+
const clean = (value) => {
|
|
14695
|
+
if (typeof value === "string") return sanitize(value);
|
|
14696
|
+
return Array.isArray(value) ? value.map(clean) : value;
|
|
14697
|
+
};
|
|
14698
|
+
const out = {
|
|
14699
|
+
...change,
|
|
14700
|
+
address: sanitize(change.address)
|
|
14701
|
+
};
|
|
14702
|
+
if (change.field !== void 0) out.field = sanitize(change.field);
|
|
14703
|
+
if (change.before !== void 0) out.before = clean(change.before);
|
|
14704
|
+
if (change.after !== void 0) out.after = clean(change.after);
|
|
14705
|
+
return out;
|
|
14706
|
+
}
|
|
12572
14707
|
/** The key's scopes, or undefined when HubSpot refused the request, gave no answer, or answered without a scope list. */
|
|
12573
14708
|
async function readTokenInfo(http, key) {
|
|
12574
14709
|
let answer;
|
|
@@ -13757,8 +15892,9 @@ function fmt(ctx) {
|
|
|
13757
15892
|
}
|
|
13758
15893
|
/**
|
|
13759
15894
|
* The canonical text of every file of a project the loader accepted, in path order: kalup.config.ts, <dir>/removed.ts,
|
|
13760
|
-
* each object file, and the barrel re-exporting every object. The barrel is left out when
|
|
13761
|
-
* re-export. A file that is not TypeScript (the blueprints lock, a stored original) is the
|
|
15895
|
+
* each object and pipeline file, and the barrel re-exporting every object and pipeline. The barrel is left out when
|
|
15896
|
+
* there is no object file to re-export. A file that is not TypeScript (the blueprints lock, a stored original) is the
|
|
15897
|
+
* tool's own JSON: not here.
|
|
13762
15898
|
*/
|
|
13763
15899
|
function canonical(files, at) {
|
|
13764
15900
|
const out = [];
|
|
@@ -13774,8 +15910,17 @@ function canonical(files, at) {
|
|
|
13774
15910
|
out.push([file, write("removed", result.data)]);
|
|
13775
15911
|
continue;
|
|
13776
15912
|
}
|
|
13777
|
-
out.push([file, write("object", result.data)]);
|
|
13778
15913
|
const from = barrelPath(at, file);
|
|
15914
|
+
if (result.kind === "pipeline") {
|
|
15915
|
+
out.push([file, write("pipeline", result.data)]);
|
|
15916
|
+
entries.push(...result.data.exports.map((e) => ({
|
|
15917
|
+
name: e.name,
|
|
15918
|
+
from,
|
|
15919
|
+
pipeline: true
|
|
15920
|
+
})));
|
|
15921
|
+
continue;
|
|
15922
|
+
}
|
|
15923
|
+
out.push([file, write("object", result.data)]);
|
|
13779
15924
|
for (const e of result.data.exports) entries.push({
|
|
13780
15925
|
name: e.name,
|
|
13781
15926
|
from
|
|
@@ -13953,7 +16098,11 @@ async function pull(ctx) {
|
|
|
13953
16098
|
async function pullTarget(ctx, pulling) {
|
|
13954
16099
|
const { root, loaded, http, target, targetName, via, accepting, warnings } = pulling;
|
|
13955
16100
|
const portalId = target.portalId;
|
|
13956
|
-
const
|
|
16101
|
+
const discovering = ctx.flags.discover === true;
|
|
16102
|
+
const portal = await readPortal(http, loaded, target, warnings, {
|
|
16103
|
+
schemas: discovering,
|
|
16104
|
+
pipelines: discovering
|
|
16105
|
+
});
|
|
13957
16106
|
if (portal.absent.length > 0) throw unknownObjects(portal.absent, portal.customObjects ?? [], loaded.configLines);
|
|
13958
16107
|
if (portal.unknownIncludes.length > 0) throw new KalupError(portal.unknownIncludes.map(unknownInclude(loaded.configLines)), exitCodes.invalid);
|
|
13959
16108
|
const incomplete = incompleteIssue(portal.gaps, targetName);
|
|
@@ -14196,12 +16345,93 @@ function mergeFiles(files, portal, loaded, merging, warnings) {
|
|
|
14196
16345
|
parsed.set(file, data);
|
|
14197
16346
|
next[file] = write("object", data);
|
|
14198
16347
|
}
|
|
16348
|
+
mergePipelineFiles({
|
|
16349
|
+
next,
|
|
16350
|
+
objects,
|
|
16351
|
+
overrides,
|
|
16352
|
+
parsed,
|
|
16353
|
+
portal,
|
|
16354
|
+
loaded,
|
|
16355
|
+
merging: {
|
|
16356
|
+
...rest,
|
|
16357
|
+
only,
|
|
16358
|
+
stated,
|
|
16359
|
+
excluded
|
|
16360
|
+
}
|
|
16361
|
+
});
|
|
14199
16362
|
return {
|
|
14200
16363
|
next,
|
|
14201
16364
|
objects,
|
|
14202
16365
|
overrides
|
|
14203
16366
|
};
|
|
14204
16367
|
}
|
|
16368
|
+
function mergePipelineFiles(m) {
|
|
16369
|
+
const { next, objects, overrides, parsed, portal, loaded } = m;
|
|
16370
|
+
const { stated, ...rest } = m.merging;
|
|
16371
|
+
const files = pipelineFiles(next, loaded.layout);
|
|
16372
|
+
const taken = /* @__PURE__ */ new Set([...[...parsed.values()].flatMap((f) => f.exports.map((e) => e.name)), ...[...files.values()].flatMap((f) => f.exports.map((e) => e.name))]);
|
|
16373
|
+
for (const live of portal.objects) {
|
|
16374
|
+
if (live.pipelines === void 0) continue;
|
|
16375
|
+
const { object } = live;
|
|
16376
|
+
const local = [...files.values()].flatMap((f) => f.exports.filter((e) => e.object === object));
|
|
16377
|
+
const merged = mergePipelines({
|
|
16378
|
+
...rest,
|
|
16379
|
+
all: loaded.config.objects[object]?.pipelines === true,
|
|
16380
|
+
object,
|
|
16381
|
+
live: live.pipelines,
|
|
16382
|
+
local: local.map((e) => pipelineAsTarget(e, stated)),
|
|
16383
|
+
taken
|
|
16384
|
+
});
|
|
16385
|
+
const report = objects[object] ?? {
|
|
16386
|
+
added: 0,
|
|
16387
|
+
changed: 0,
|
|
16388
|
+
unchanged: 0,
|
|
16389
|
+
missing: 0,
|
|
16390
|
+
changes: []
|
|
16391
|
+
};
|
|
16392
|
+
for (const count of [
|
|
16393
|
+
"added",
|
|
16394
|
+
"changed",
|
|
16395
|
+
"unchanged",
|
|
16396
|
+
"missing"
|
|
16397
|
+
]) report[count] += merged.counts[count];
|
|
16398
|
+
report.changes.push(...merged.changes);
|
|
16399
|
+
objects[object] = report;
|
|
16400
|
+
for (const [file, data] of files) {
|
|
16401
|
+
const exports = data.exports.map((e) => {
|
|
16402
|
+
const into = e.object === object ? merged.merged.get(e.name) : void 0;
|
|
16403
|
+
if (!into) return e;
|
|
16404
|
+
const split = pipelineFromTarget(into, e, stated);
|
|
16405
|
+
Object.assign(overrides, split.overrides);
|
|
16406
|
+
return split.export;
|
|
16407
|
+
});
|
|
16408
|
+
files.set(file, {
|
|
16409
|
+
...data,
|
|
16410
|
+
exports
|
|
16411
|
+
});
|
|
16412
|
+
}
|
|
16413
|
+
if (merged.fresh.length > 0) {
|
|
16414
|
+
const file = pipelinePath(loaded.layout, object);
|
|
16415
|
+
const data = files.get(file) ?? {
|
|
16416
|
+
imports: [],
|
|
16417
|
+
exports: []
|
|
16418
|
+
};
|
|
16419
|
+
files.set(file, {
|
|
16420
|
+
...data,
|
|
16421
|
+
exports: [...data.exports, ...merged.fresh]
|
|
16422
|
+
});
|
|
16423
|
+
}
|
|
16424
|
+
}
|
|
16425
|
+
for (const [file, data] of files) next[file] = write("pipeline", data);
|
|
16426
|
+
}
|
|
16427
|
+
function pipelineFiles(files, at) {
|
|
16428
|
+
const out = /* @__PURE__ */ new Map();
|
|
16429
|
+
for (const [file, text] of Object.entries(files)) if (inDir(at, file) && inPipelines(at, file)) {
|
|
16430
|
+
const result = read(text, file, "pipeline");
|
|
16431
|
+
if (result.kind === "pipeline") out.set(file, result.data);
|
|
16432
|
+
}
|
|
16433
|
+
return out;
|
|
16434
|
+
}
|
|
14205
16435
|
function writeOverrides(next, config, targetName, changed) {
|
|
14206
16436
|
if (Object.keys(changed).length === 0) return;
|
|
14207
16437
|
const target = config.targets[targetName];
|
|
@@ -14263,7 +16493,7 @@ function incompleteIssue(gaps, target) {
|
|
|
14263
16493
|
function objectFiles(files, at) {
|
|
14264
16494
|
const out = /* @__PURE__ */ new Map();
|
|
14265
16495
|
for (const [file, text] of Object.entries(files)) {
|
|
14266
|
-
if (!inDir(at, file) || file === at.barrel) continue;
|
|
16496
|
+
if (!inDir(at, file) || file === at.barrel || inPipelines(at, file)) continue;
|
|
14267
16497
|
const result = read(text, file);
|
|
14268
16498
|
if (result.kind === "object") out.set(file, result.data);
|
|
14269
16499
|
}
|
|
@@ -14343,6 +16573,17 @@ function portalSide(c, target) {
|
|
|
14343
16573
|
function show$1(value) {
|
|
14344
16574
|
return value === void 0 ? "none" : JSON.stringify(value);
|
|
14345
16575
|
}
|
|
16576
|
+
function outsidePipelines(loaded, portal, lines) {
|
|
16577
|
+
const pipelines = {};
|
|
16578
|
+
for (const live of portal.objects) {
|
|
16579
|
+
const defined = (id) => Object.hasOwn(loaded.ir.resources, `pipeline:${live.object}/${id}`);
|
|
16580
|
+
const outside = loaded.config.objects[live.object]?.pipelines === true ? [] : (live.pipelines ?? []).filter((p) => !defined(p.id));
|
|
16581
|
+
if (outside.length === 0) continue;
|
|
16582
|
+
pipelines[live.object] = outside.map((p) => sanitize(p.id));
|
|
16583
|
+
for (const p of outside) lines.push(` pipeline:${live.object}/${sanitize(p.id)} (${sanitize(`"${p.label}"`, 200)}; set objects.${live.object}.pipelines to true)`);
|
|
16584
|
+
}
|
|
16585
|
+
return pipelines;
|
|
16586
|
+
}
|
|
14346
16587
|
function discover(target, portalId, loaded, portal, warnings) {
|
|
14347
16588
|
const scopes = loaded.config.objects;
|
|
14348
16589
|
const lines = [];
|
|
@@ -14362,6 +16603,7 @@ function discover(target, portalId, loaded, portal, warnings) {
|
|
|
14362
16603
|
lines.push(` property:${live.object}/${sanitize(p.name)} (${sanitize(why, 400)})`);
|
|
14363
16604
|
}
|
|
14364
16605
|
}
|
|
16606
|
+
const pipelines = outsidePipelines(loaded, portal, lines);
|
|
14365
16607
|
let head = `Everything the portal holds for target ${target} is in the pull scope.`;
|
|
14366
16608
|
if (lines.length > 0) head = `Outside the pull scope of target ${target} (portal ${portalId}):`;
|
|
14367
16609
|
else if (portal.gaps.length > 0) head = `Nothing outside the pull scope of target ${target} in the lists the key could read.`;
|
|
@@ -14370,7 +16612,8 @@ function discover(target, portalId, loaded, portal, warnings) {
|
|
|
14370
16612
|
target,
|
|
14371
16613
|
portalId,
|
|
14372
16614
|
objects,
|
|
14373
|
-
properties
|
|
16615
|
+
properties,
|
|
16616
|
+
pipelines
|
|
14374
16617
|
},
|
|
14375
16618
|
issues: warnings,
|
|
14376
16619
|
text: `${[
|
|
@@ -14966,7 +17209,7 @@ async function confirm(prompt, plan, portal) {
|
|
|
14966
17209
|
const { name, protected: guarded } = plan.target;
|
|
14967
17210
|
prompt.tell([
|
|
14968
17211
|
`Apply plan ${plan.planId} to target ${name}, portal ${portal.portalId} (${portal.accountType}, ${guarded ? "protected" : "not protected"}):`,
|
|
14969
|
-
...effects.map((s) => ` ${s.id} ${s.risk}${s.labels?.length ? ` [${s.labels.join(", ")}]` : ""} ${stepTitle(s, names)}`),
|
|
17212
|
+
...effects.map((s) => ` ${s.id} ${s.risk}${s.labels?.length ? ` [${s.labels.join(", ")}]` : ""} ${stepTitle(s, names, true)}`),
|
|
14970
17213
|
[...kinds.filter(([n]) => n > 0).map(([n, noun]) => plural(n, noun)), `${destructive} destructive`].join(", ")
|
|
14971
17214
|
]);
|
|
14972
17215
|
if ((await prompt.ask("Type the target name to apply:"))?.trim() !== name) throw cancelled("the answer was not the target name");
|
|
@@ -15594,6 +17837,8 @@ const LINE_BREAK = /\r?\n/;
|
|
|
15594
17837
|
const JSON_INDENT = /^([ \t]+)"/m;
|
|
15595
17838
|
/** A line of the barrel: `export {}`, or a re-export of one object file's exports. */
|
|
15596
17839
|
const BARREL_LINE = /^export (?:\{\}|(?:type )?\{ [\w, ]+ \} from '\.\/[\w./-]+')$/;
|
|
17840
|
+
/** The objects whose pipelines a new project pulls: those Kalup writes the pipelines of among the standard objects. */
|
|
17841
|
+
const PIPELINED = /* @__PURE__ */ new Set(["deals", "tickets"]);
|
|
15597
17842
|
function init(ctx) {
|
|
15598
17843
|
const { cwd, flags } = ctx;
|
|
15599
17844
|
const portalId = parsePortal$1(flags.portal);
|
|
@@ -15618,7 +17863,7 @@ function init(ctx) {
|
|
|
15618
17863
|
const config = {
|
|
15619
17864
|
imports: [],
|
|
15620
17865
|
dir,
|
|
15621
|
-
objects: Object.fromEntries(objects.map((object) => [object, {}])),
|
|
17866
|
+
objects: Object.fromEntries(objects.map((object) => [object, PIPELINED.has(object) ? { pipelines: true } : {}])),
|
|
15622
17867
|
targets: { [target]: targetConfig }
|
|
15623
17868
|
};
|
|
15624
17869
|
const files = [];
|
|
@@ -16025,8 +18270,6 @@ function ir(ctx) {
|
|
|
16025
18270
|
}
|
|
16026
18271
|
//#endregion
|
|
16027
18272
|
//#region src/commands/rm.ts
|
|
16028
|
-
const REMOVABLE = /* @__PURE__ */ new Set(["property", "group"]);
|
|
16029
|
-
const ON_OBJECT = /^[^\s/]+\/[^\s/]+$/;
|
|
16030
18273
|
function rm(ctx) {
|
|
16031
18274
|
const [given] = ctx.args;
|
|
16032
18275
|
if (given === void 0) throw usageError("missing argument ADDRESS");
|
|
@@ -16079,11 +18322,11 @@ function rm(ctx) {
|
|
|
16079
18322
|
message: `${issue.message}${after}`
|
|
16080
18323
|
})), exitCodes.invalid);
|
|
16081
18324
|
}
|
|
16082
|
-
const changed = Object.keys(next).filter((file) => next[file] !== files[file]);
|
|
18325
|
+
const changed = [.../* @__PURE__ */ new Set([...Object.keys(next), ...Object.keys(files)])].filter((file) => next[file] !== files[file]);
|
|
16083
18326
|
const data = {
|
|
16084
18327
|
address,
|
|
16085
18328
|
action,
|
|
16086
|
-
files: writeStaged(root, Object.fromEntries(changed.map((file) => [file, next[file] ??
|
|
18329
|
+
files: writeStaged(root, Object.fromEntries(changed.map((file) => [file, next[file] ?? null]))),
|
|
16087
18330
|
...from === void 0 ? {} : { from },
|
|
16088
18331
|
...previous === void 0 ? {} : { previous: previous.action }
|
|
16089
18332
|
};
|
|
@@ -16094,18 +18337,19 @@ function rm(ctx) {
|
|
|
16094
18337
|
};
|
|
16095
18338
|
}
|
|
16096
18339
|
function checkAddress(given) {
|
|
16097
|
-
const at = { fix: "pass the address of a property or
|
|
18340
|
+
const at = { fix: "pass the address of a property, group, pipeline or stage, such as 'property:companies/legacy_score'" };
|
|
16098
18341
|
if (!isAddress(given)) throw invalidAddress({
|
|
16099
18342
|
message: `'${sanitize(given)}' is not an address`,
|
|
16100
18343
|
...at
|
|
16101
18344
|
});
|
|
16102
18345
|
const { type, path } = parseAddress(given);
|
|
16103
|
-
|
|
16104
|
-
|
|
18346
|
+
const shape = Object.hasOwn(REMOVABLE, type) ? REMOVABLE[type] : void 0;
|
|
18347
|
+
if (!shape) throw invalidAddress({
|
|
18348
|
+
message: `cannot remove ${sanitize(given)}: this version removes properties, groups, pipelines and stages only`,
|
|
16105
18349
|
fix: "custom objects are not removed or released in this release"
|
|
16106
18350
|
});
|
|
16107
|
-
if (!
|
|
16108
|
-
message: `'${sanitize(given)}' is not of the form ${
|
|
18351
|
+
if (!shape.path.test(path)) throw invalidAddress({
|
|
18352
|
+
message: `'${sanitize(given)}' is not of the form ${shape.form}`,
|
|
16109
18353
|
...at
|
|
16110
18354
|
});
|
|
16111
18355
|
return given;
|
|
@@ -16134,7 +18378,9 @@ function refuse(loaded, address, resource, action) {
|
|
|
16134
18378
|
function dependents(loaded, address) {
|
|
16135
18379
|
const { resources } = loaded.ir;
|
|
16136
18380
|
const key = objectOf(address);
|
|
16137
|
-
|
|
18381
|
+
const { type } = parseAddress(address);
|
|
18382
|
+
if (type === "pipeline" || type === "stage") return [];
|
|
18383
|
+
if (type === "group") return Object.entries(resources).filter(([, r]) => r.type === "property" && (r.definition?.group)?.$ref === address).map(([a]) => a).sort();
|
|
16138
18384
|
const d = (Object.hasOwn(resources, `object:${key}`) ? resources[`object:${key}`] : void 0)?.definition ?? {};
|
|
16139
18385
|
return [
|
|
16140
18386
|
d.primaryDisplayProperty,
|
|
@@ -16147,6 +18393,8 @@ function takeOut(files, loaded, address) {
|
|
|
16147
18393
|
const source = loaded.sources[address];
|
|
16148
18394
|
const text = source === void 0 ? void 0 : files[source.file];
|
|
16149
18395
|
if (source === void 0 || text === void 0) return;
|
|
18396
|
+
const { type } = parseAddress(address);
|
|
18397
|
+
if (type === "pipeline" || type === "stage") return takeOutPipeline(files, source, text);
|
|
16150
18398
|
const result = read(text, source.file);
|
|
16151
18399
|
if (result.kind !== "object") return;
|
|
16152
18400
|
const [owner] = source.configPath.split(".");
|
|
@@ -16168,6 +18416,21 @@ function takeOut(files, loaded, address) {
|
|
|
16168
18416
|
});
|
|
16169
18417
|
return source.file;
|
|
16170
18418
|
}
|
|
18419
|
+
function takeOutPipeline(files, source, text) {
|
|
18420
|
+
const result = read(text, source.file, "pipeline");
|
|
18421
|
+
if (result.kind !== "pipeline") return;
|
|
18422
|
+
const [owner, , key] = source.configPath.split(".");
|
|
18423
|
+
const exports = key === void 0 ? result.data.exports.filter((e) => e.name !== owner) : result.data.exports.map((e) => e.name === owner ? {
|
|
18424
|
+
...e,
|
|
18425
|
+
stages: e.stages.filter((st) => st.key !== key)
|
|
18426
|
+
} : e);
|
|
18427
|
+
if (exports.length === 0) delete files[source.file];
|
|
18428
|
+
else files[source.file] = write("pipeline", {
|
|
18429
|
+
...result.data,
|
|
18430
|
+
exports
|
|
18431
|
+
});
|
|
18432
|
+
return source.file;
|
|
18433
|
+
}
|
|
16171
18434
|
function removedFile(text, file) {
|
|
16172
18435
|
if (text === void 0) return {
|
|
16173
18436
|
imports: [],
|
|
@@ -16243,12 +18506,15 @@ function countsOf(document) {
|
|
|
16243
18506
|
return {
|
|
16244
18507
|
objects: Object.values(document.observation.coverage.objects).filter((o) => o.status === "read").length,
|
|
16245
18508
|
groups: resources.filter((r) => r.type === "group").length,
|
|
16246
|
-
|
|
18509
|
+
pipelines: resources.filter((r) => r.type === "pipeline").length,
|
|
18510
|
+
properties: resources.filter((r) => r.type === "property").length,
|
|
18511
|
+
stages: resources.filter((r) => r.type === "stage").length
|
|
16247
18512
|
};
|
|
16248
18513
|
}
|
|
16249
18514
|
function summary(data) {
|
|
16250
|
-
const { objects, groups, properties } = data.counts;
|
|
16251
|
-
const
|
|
18515
|
+
const { objects, groups, properties, pipelines, stages } = data.counts;
|
|
18516
|
+
const piped = pipelines > 0 ? `, ${plural(pipelines, "pipeline")}, ${plural(stages, "stage")}` : "";
|
|
18517
|
+
const held = `${plural(objects, "object")}, ${plural(groups, "group")}, ${plural(properties, "property", "properties")}${piped}`;
|
|
16252
18518
|
return `${`Snapshot of target ${sanitize(data.target)}, portal ${data.portalId}, observed at ${data.observedAt}: ${held}`}\n${wrote(data.file)}`;
|
|
16253
18519
|
}
|
|
16254
18520
|
//#endregion
|
|
@@ -16460,12 +18726,15 @@ async function status(ctx) {
|
|
|
16460
18726
|
const counts = {
|
|
16461
18727
|
objects: Object.keys(loaded.config.objects).length,
|
|
16462
18728
|
properties: count(loaded.ir, "property"),
|
|
16463
|
-
groups: count(loaded.ir, "group")
|
|
18729
|
+
groups: count(loaded.ir, "group"),
|
|
18730
|
+
pipelines: count(loaded.ir, "pipeline"),
|
|
18731
|
+
stages: count(loaded.ir, "stage")
|
|
16464
18732
|
};
|
|
18733
|
+
const piped = counts.pipelines > 0 ? `, ${plural(counts.pipelines, "pipeline")}, ${plural(counts.stages, "stage")}` : "";
|
|
16465
18734
|
const exitCode = exitCodeOf(targets);
|
|
16466
18735
|
const lines = [
|
|
16467
18736
|
`${bin} ${version}`,
|
|
16468
|
-
`Config: valid (${plural(counts.objects, "object")}, ${plural(counts.properties, "property", "properties")}, ${plural(counts.groups, "group")}) in ${loaded.layout.dir}/`,
|
|
18737
|
+
`Config: valid (${plural(counts.objects, "object")}, ${plural(counts.properties, "property", "properties")}, ${plural(counts.groups, "group")}${piped}) in ${loaded.layout.dir}/`,
|
|
16469
18738
|
...targets.flatMap((t) => describe(root, t, recommended.scope, writeScopes))
|
|
16470
18739
|
];
|
|
16471
18740
|
return {
|
|
@@ -17095,7 +19364,7 @@ var ApplyCommand = class extends KalupCommand {
|
|
|
17095
19364
|
}
|
|
17096
19365
|
};
|
|
17097
19366
|
var RmCommand = class extends KalupCommand {
|
|
17098
|
-
static summary = "Take a
|
|
19367
|
+
static summary = "Take a resource out of config and write its tombstone in removed.ts.";
|
|
17099
19368
|
static args = { address: Args.string({
|
|
17100
19369
|
description: "The address, for example property:companies/legacy_score.",
|
|
17101
19370
|
required: true
|