kalup 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/{commands-CHucC_PV.mjs → commands-DdogKJKg.mjs} +12351 -9161
- package/dist/commands.d.mts +1 -1
- package/dist/commands.mjs +1 -1
- package/dist/{context-B7bI9BjL.d.mts → context-BgFzBPj9.d.mts} +52 -0
- package/dist/{host-D1HtgrVd.mjs → host-BSYg3I66.mjs} +1 -1
- package/dist/host.d.mts +1 -1
- package/dist/host.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/schemas/ir-1.schema.json +688 -194
- package/dist/schemas/plan-1.schema.json +15 -6
- package/dist/schemas/state-1.schema.json +97 -23
- package/docs/apply.md +8 -5
- package/docs/compare.md +1 -1
- package/docs/config.md +34 -8
- package/docs/dictionary.md +2 -2
- package/docs/errors/E_ASSOCIATION_FIELD.md +21 -0
- package/docs/errors/E_ASSOCIATION_NAME.md +17 -0
- package/docs/errors/E_BLUEPRINT_REQUIRES.md +3 -3
- package/docs/errors/E_PLAN_DELETE.md +1 -1
- package/docs/errors/E_PLAN_INVALID.md +3 -1
- package/docs/errors/E_PLAN_RISK.md +1 -1
- package/docs/errors/E_PREVENT_DESTROY.md +1 -1
- package/docs/errors/E_TOMBSTONE_ADDRESS.md +1 -1
- package/docs/errors/E_TOMBSTONE_CONFLICT.md +1 -1
- package/docs/errors/E_UNKNOWN_OBJECT.md +1 -1
- package/docs/errors/W_OBJECT_FIELD.md +25 -0
- package/docs/errors/W_OBJECT_PROPERTY.md +17 -0
- package/docs/errors/W_SETTLING.md +19 -0
- package/docs/plan.md +27 -6
- package/docs/pull.md +10 -5
- package/docs/rm.md +6 -5
- package/docs/snapshot.md +4 -4
- package/docs/state.md +4 -3
- package/docs/targets.md +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kalup.dev/schemas/plan-1.schema.json",
|
|
4
4
|
"title": "Kalup plan, version 1",
|
|
5
|
-
"description": "What `kalup plan` would do to one target, with the context an approval binds to. Closed at every level, so a saved plan carries nothing this schema does not name, and
|
|
5
|
+
"description": "What `kalup plan` would do to one target, with the context an approval binds to. Closed at every level, apart from the free-form values in a step's desired and expect.values, so a saved plan carries nothing this schema does not name. A later 1.x may add fields, values and step types; an earlier 1.x refuses a plan that uses one, before any request (docs/compatibility.md).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": [
|
|
8
8
|
"format",
|
|
@@ -97,7 +97,9 @@
|
|
|
97
97
|
"description": "The objects whose mode on this target is takeover, sorted.",
|
|
98
98
|
"type": "array",
|
|
99
99
|
"uniqueItems": true,
|
|
100
|
-
"items": {
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "string"
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
104
|
},
|
|
103
105
|
"additionalProperties": false
|
|
@@ -476,8 +478,12 @@
|
|
|
476
478
|
"type": "object",
|
|
477
479
|
"required": ["address", "desired"],
|
|
478
480
|
"properties": {
|
|
479
|
-
"address": {
|
|
480
|
-
|
|
481
|
+
"address": {
|
|
482
|
+
"$ref": "#/$defs/address"
|
|
483
|
+
},
|
|
484
|
+
"desired": {
|
|
485
|
+
"type": "object"
|
|
486
|
+
}
|
|
481
487
|
},
|
|
482
488
|
"additionalProperties": false
|
|
483
489
|
}
|
|
@@ -485,7 +491,9 @@
|
|
|
485
491
|
"stageLabels": {
|
|
486
492
|
"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.",
|
|
487
493
|
"type": "object",
|
|
488
|
-
"additionalProperties": {
|
|
494
|
+
"additionalProperties": {
|
|
495
|
+
"type": "string"
|
|
496
|
+
}
|
|
489
497
|
},
|
|
490
498
|
"ignoreChanges": {
|
|
491
499
|
"description": "Create only: set on create, released after.",
|
|
@@ -783,7 +791,8 @@
|
|
|
783
791
|
"override",
|
|
784
792
|
"unsupported",
|
|
785
793
|
"not-owned",
|
|
786
|
-
"policy"
|
|
794
|
+
"policy",
|
|
795
|
+
"settling"
|
|
787
796
|
]
|
|
788
797
|
},
|
|
789
798
|
"detail": {
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://kalup.dev/schemas/state-1.schema.json",
|
|
4
4
|
"title": "Kalup portal state, version 1",
|
|
5
|
-
"description": ".kalup/state/portal-<portalId>.json. Describes one verified portal, not the code, and holds no target name. Covered by the compatibility policy (docs/compatibility.md):
|
|
5
|
+
"description": ".kalup/state/portal-<portalId>.json. Describes one verified portal, not the code, and holds no target name. Covered by the compatibility policy (docs/compatibility.md). Open for additions within kalup.state/1: a later 1.x may add top-level fields, resource entries of types this version does not plan, and fields on an entry. A reader keeps what it does not know: it never drops an entry of a type it does not handle or a top-level field it does not know, and it drops an entry's unknown fields only when it rewrites that entry. lastApply, an entry's attested and rewrites, and the base's option members stay closed. Kalup never writes a key, record data, unowned fields or unmanaged resources: its state writer refuses a file holding anything shaped like a key.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["format", "lineage", "serial", "portalId", "resources"],
|
|
8
8
|
"properties": {
|
|
9
|
-
"format": {
|
|
9
|
+
"format": {
|
|
10
|
+
"const": "kalup.state/1"
|
|
11
|
+
},
|
|
10
12
|
"lineage": {
|
|
11
13
|
"description": "16 lowercase hex characters, new on rebuild and rebind. A plan made against another lineage is refused.",
|
|
12
14
|
"type": "string",
|
|
@@ -26,42 +28,77 @@
|
|
|
26
28
|
"type": "object",
|
|
27
29
|
"required": ["planId", "writesHash", "actor", "at", "outcome"],
|
|
28
30
|
"properties": {
|
|
29
|
-
"planId": {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
31
|
+
"planId": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^pl_[0-9a-f]{12}$"
|
|
34
|
+
},
|
|
35
|
+
"writesHash": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^sha256:[0-9a-f]{64}$"
|
|
38
|
+
},
|
|
39
|
+
"actor": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"at": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
},
|
|
45
|
+
"outcome": {
|
|
46
|
+
"enum": ["running", "done", "partial", "uncertain"]
|
|
47
|
+
}
|
|
34
48
|
},
|
|
35
49
|
"additionalProperties": false
|
|
36
50
|
},
|
|
37
51
|
"resources": {
|
|
38
52
|
"type": "object",
|
|
39
|
-
"patternProperties": {
|
|
53
|
+
"patternProperties": {
|
|
54
|
+
"^[a-z]+:\\S+$": {
|
|
55
|
+
"$ref": "#/$defs/resourceState"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
40
58
|
"additionalProperties": false
|
|
41
59
|
}
|
|
42
60
|
},
|
|
43
|
-
"additionalProperties":
|
|
61
|
+
"additionalProperties": true,
|
|
44
62
|
"$defs": {
|
|
45
63
|
"resourceState": {
|
|
46
64
|
"type": "object",
|
|
47
65
|
"required": ["origin", "id"],
|
|
48
66
|
"properties": {
|
|
49
67
|
"origin": {
|
|
50
|
-
"description": "created and adopted are owned. pulled owns nothing: pull recorded the base of a resource no entry owned, and a plan still adopts it.",
|
|
51
|
-
"
|
|
68
|
+
"description": "created and adopted are owned. pulled owns nothing: pull recorded the base of a resource no entry owned, and a plan still adopts it. reference owns nothing. Another value, from a later 1.x, owns nothing here.",
|
|
69
|
+
"type": "string",
|
|
70
|
+
"pattern": "^[a-z]+$"
|
|
52
71
|
},
|
|
53
72
|
"id": {
|
|
54
73
|
"description": "The portal name the entry owns. It owns the resource only while this equals the name the address resolves to. null for runbook-only types.",
|
|
55
74
|
"type": ["string", "null"]
|
|
56
75
|
},
|
|
57
|
-
"via": {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
76
|
+
"via": {
|
|
77
|
+
"description": "Transport of the last write.",
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
"normVersion": {
|
|
81
|
+
"description": "Normalizer version of the type that wrote base.",
|
|
82
|
+
"type": "integer"
|
|
83
|
+
},
|
|
84
|
+
"base": {
|
|
85
|
+
"$ref": "#/$defs/base"
|
|
86
|
+
},
|
|
87
|
+
"baseHash": {
|
|
88
|
+
"description": "Replaces base for opaque payloads and runbook types.",
|
|
89
|
+
"type": "string"
|
|
90
|
+
},
|
|
61
91
|
"attested": {
|
|
62
92
|
"type": "object",
|
|
63
93
|
"required": ["by", "at"],
|
|
64
|
-
"properties": {
|
|
94
|
+
"properties": {
|
|
95
|
+
"by": {
|
|
96
|
+
"type": "string"
|
|
97
|
+
},
|
|
98
|
+
"at": {
|
|
99
|
+
"type": "string"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
65
102
|
"additionalProperties": false
|
|
66
103
|
},
|
|
67
104
|
"rewrites": {
|
|
@@ -70,27 +107,64 @@
|
|
|
70
107
|
"additionalProperties": {
|
|
71
108
|
"type": "object",
|
|
72
109
|
"required": ["sent", "stored"],
|
|
73
|
-
"properties": {
|
|
110
|
+
"properties": {
|
|
111
|
+
"sent": {},
|
|
112
|
+
"stored": {}
|
|
113
|
+
},
|
|
74
114
|
"additionalProperties": false
|
|
75
115
|
}
|
|
116
|
+
},
|
|
117
|
+
"typeIds": {
|
|
118
|
+
"description": "An association's HubSpot type IDs in this portal, its direction's first: they name it while HubSpot's schema read does not list its name yet.",
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "integer"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"written": {
|
|
125
|
+
"description": "The units apply wrote within the settling window before its last write, each with when it verified the write. For a few minutes HubSpot may serve an older copy, so a read that disagrees on such a unit is settling: unknown, never drift.",
|
|
126
|
+
"type": "object",
|
|
127
|
+
"additionalProperties": {
|
|
128
|
+
"type": "string"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"writtenAt": {
|
|
132
|
+
"description": "When apply last verified a write to the resource, whatever units it named. A read that does not show the resource within the settling window after it is settling: unknown, never absence.",
|
|
133
|
+
"type": "string"
|
|
76
134
|
}
|
|
77
135
|
},
|
|
78
|
-
"additionalProperties":
|
|
136
|
+
"additionalProperties": true
|
|
79
137
|
},
|
|
80
138
|
"base": {
|
|
81
139
|
"description": "Per owned unit, the value config and portal last agreed on; possibly partial. Scalar units by field name. options: a map keyed by option value whose members hold the agreed label, hidden and description; a member with no fields means only its membership is agreed. optionsOrder: the agreed order of the members both sides held.",
|
|
82
140
|
"type": "object",
|
|
83
141
|
"properties": {
|
|
84
|
-
"options": {
|
|
85
|
-
|
|
142
|
+
"options": {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"additionalProperties": {
|
|
145
|
+
"$ref": "#/$defs/baseOption"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
"optionsOrder": {
|
|
149
|
+
"type": "array",
|
|
150
|
+
"items": {
|
|
151
|
+
"type": "string"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
86
154
|
}
|
|
87
155
|
},
|
|
88
156
|
"baseOption": {
|
|
89
157
|
"type": "object",
|
|
90
158
|
"properties": {
|
|
91
|
-
"label": {
|
|
92
|
-
|
|
93
|
-
|
|
159
|
+
"label": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
},
|
|
162
|
+
"hidden": {
|
|
163
|
+
"type": "boolean"
|
|
164
|
+
},
|
|
165
|
+
"description": {
|
|
166
|
+
"type": "string"
|
|
167
|
+
}
|
|
94
168
|
},
|
|
95
169
|
"additionalProperties": false
|
|
96
170
|
}
|
package/docs/apply.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Apply
|
|
2
2
|
|
|
3
|
-
`kalup apply <plan-file> [--yes | --approve <writesHash>]` applies a plan saved by `kalup plan --out` to the target it names. `kalup apply [--target <name>] [--take config <selector>] [--yes]` plans the target now and applies that plan through the same checks: at a terminal it prints the whole plan first, as `plan` does, then asks. Apply writes property groups
|
|
3
|
+
`kalup apply <plan-file> [--yes | --approve <writesHash>]` applies a plan saved by `kalup plan --out` to the target it names. `kalup apply [--target <name>] [--take config <selector>] [--yes]` plans the target now and applies that plan through the same checks: at a terminal it prints the whole plan first, as `plan` does, then asks. Apply writes custom objects, property groups, properties, pipelines, stages and associations.
|
|
4
4
|
|
|
5
5
|
This page is the reference. For the walk-through with examples, see [kalup apply](https://kalup.dev/docs/commands/apply) on the website.
|
|
6
6
|
|
|
@@ -25,23 +25,26 @@ Before any write, in order:
|
|
|
25
25
|
3. The write key (`credentials.write`, else the read key) passes the portal guard. Every request, reads included, uses it.
|
|
26
26
|
4. The policy equals the plan's: `protected`, `drift`, `adopt`, `allowDestroy`, `yesLimit` and the objects in takeover (`E_POLICY_CHANGED`); each step's API version is current and unexpired, and the normalizer versions match (`E_PLAN_VERSION`).
|
|
27
27
|
5. Every step but a release is on an object `kalup.config.ts` declares, the name bindings match the target's name overrides, and no two steps but releases resolve to one portal resource (`E_BINDING_CHANGED`).
|
|
28
|
-
6. Each delete has a `destroy` tombstone in `hubspot/removed.ts` or takeover's leave (takeover mode, in the pull scope, not excluded), is gone from config, and no address in config names its portal resource, read as data (`E_PLAN_DELETE`). The object files also tell takeover's option removals from config's own.
|
|
28
|
+
6. Each delete has a `destroy` tombstone in `hubspot/removed.ts` or takeover's leave (takeover mode, in the pull scope, not excluded), a delete labelled `takeover` has takeover's leave whatever `hubspot/removed.ts` says, is gone from config, and no address in config names its portal resource, read as data (`E_PLAN_DELETE`). The object files also tell takeover's option removals from config's own.
|
|
29
29
|
7. Approval, then the portal lock (`E_LOCKED`).
|
|
30
30
|
8. State: a plan already applied with outcome `done` exits 0 ("Already applied"); otherwise lineage and serial equal the plan's (`E_STATE_CHANGED`).
|
|
31
|
-
9. A fresh read of each object the plan changes, its pipelines when a step touches one, and of the schemas list for a custom object (`E_INCOMPLETE` on a 403, `E_BINDING_CHANGED` for another type ID). Every `expect` must hold (`E_PLAN_STALE`), a delete's covering each field its base holds. Kalup derives each step's risk, labels and blocked status again (`E_PLAN_RISK` when the plan states less); a takeover removal needs `allowDestroy`, and never takes a HubSpot-defined property.
|
|
31
|
+
9. A fresh read of each object the plan changes, its pipelines when a step touches one, and of the schemas list for a custom object (`E_INCOMPLETE` on a 403, `E_BINDING_CHANGED` for another type ID). Every `expect` must hold (`E_PLAN_STALE`), a delete's covering each field its base holds. Kalup derives each step's risk, labels and blocked status again (`E_PLAN_RISK` when the plan states less); a takeover removal needs `allowDestroy`, and never takes a HubSpot-defined property; a delete labelled `takeover` is a property or group on an object in takeover mode that `hubspot/removed.ts` neither names nor covers, never a custom object, pipeline, stage or association; a custom object's display fields name only properties HubSpot gives every custom object, the read found, or the plan creates.
|
|
32
32
|
10. Three calls per write plus the reads use at most half of HubSpot's daily remainder (`E_BUDGET`).
|
|
33
33
|
|
|
34
34
|
## Running the steps
|
|
35
35
|
|
|
36
|
-
Apply records `lastApply.outcome: running`, then runs the steps one at a time: groups, properties, pipeline creates, stage creates and updates (those that close a stage first, so a ticket pipeline keeps a closed stage), pipeline updates, releases, then deletes: properties, groups, stages, pipelines. Each write reads the resource again and compares it with `expect`, builds the request from that read, sends it once, and reads it back for up to 60 seconds, saying so on stderr after a few seconds.
|
|
36
|
+
Apply records `lastApply.outcome: running`, then runs the steps one at a time: custom object creates, groups, properties, the display fields of new custom objects, custom object updates, pipeline creates, stage creates and updates (those that close a stage first, so a ticket pipeline keeps a closed stage), pipeline updates, plain association creates, label creates, label updates, releases, then deletes: association labels, plain associations, properties, groups, stages, pipelines, custom objects. Each write reads the resource again and compares it with `expect`, builds the request from that read, sends it once, and reads it back for up to 60 seconds, saying so on stderr after a few seconds.
|
|
37
37
|
|
|
38
38
|
- A property update sends the approved fields with the live `type` and `fieldType`; options go as the full live list with the approved changes, new ones last.
|
|
39
39
|
- A pipeline create sends its stages with it. A stage update sends only the approved fields. A stage order change moves each stage that is out of place onto the slot of the stage it follows, one request each, reading the pipeline before each move: HubSpot renumbers the pipeline when a stage lands on a taken slot. If a move waits out a rate limit, the step stops stale; plan again to see what is left. Kalup never sends a pipeline PUT, which deletes every stage it does not name.
|
|
40
|
+
- A new custom object is created with its name, labels and description, and its primary display property when HubSpot gives every custom object that property, else `hs_object_id`. Its groups and properties follow, created on the type ID HubSpot returned. HubSpot makes the group `<name>_information` with the object, so when the object file lists that group, apply gives it config's label instead of creating it, and records it adopted. Once they have run, one schema update sets the display, required and searchable properties, since HubSpot refuses a field naming a property it does not hold. Apply derives that update from the create step and reports it inside the create's report as `display` (`outcome`, and `units`, `issue` when present), on an indented line under the create in the text. The create's own `outcome` stays the create's, and the run's outcome and exit code count the update too. When a property it names did not finish, it is `not-run`; the create stays `done`, and the next plan sets the fields as an update. A create HubSpot answers with a custom object a read in the run listed, or one the schemas list shows made before the answer, made nothing: it is `uncertain`, and nothing on the object runs.
|
|
41
|
+
- A custom object update sends every field of the schema, from a read made right before it, with the approved changes: HubSpot can set fields left out of a partial update back to older values. Custom objects are read from the schemas list, never the single read, which lags. A custom object delete archives it: HubSpot refuses while the object holds records, and Kalup never purges it.
|
|
42
|
+
- An association is read from both labels lists of its pair, by the name the schema read gives, else by the type IDs state records or the create answered with: HubSpot's schema read lists a new name only minutes after the create. A label update sends both labels with the type ID of its direction. A delete is done when neither list holds the association. State records each association's two type IDs.
|
|
40
43
|
- A pipeline or stage is read back with a GET of its pipeline. A stage delete is done only when that read lacks the stage: HubSpot answers 204 to a delete of any stage, one that does not exist included.
|
|
41
44
|
- A 429, 423 or 477 is waited out three times, reading again before each resend. A daily 429 stops the run.
|
|
42
45
|
- A timeout, network failure or 5xx is `uncertain` and never resent: HubSpot documents no idempotency keys. Only reading back the approved values settles it (`E_UNCERTAIN_WRITE`).
|
|
43
46
|
- A value HubSpot stores differently is `W_UNVERIFIED`: state records both, and the next plan notes it instead of writing again.
|
|
44
|
-
- A delete runs only once every earlier step verified. HubSpot keeps an archived property restorable in its UI for 90 days; a deleted pipeline or
|
|
47
|
+
- A delete runs only once every earlier step verified. HubSpot keeps an archived property restorable in its UI for 90 days, and an archived custom object in its archive until someone purges it; a deleted pipeline, stage or association is gone for good.
|
|
45
48
|
|
|
46
49
|
State is saved after each step that changes an entry, then the outcome: `done`, `partial` or `uncertain`. Each request is journaled in `.kalup/journal/portal-<id>/`, never with a key or body. SIGINT or SIGTERM stops before the next request and saves state. A second one exits at once, unless it comes within a second (npx passes one Ctrl-C on twice).
|
|
47
50
|
|
package/docs/compare.md
CHANGED
|
@@ -30,7 +30,7 @@ Every address present on either side, including properties Kalup does not write,
|
|
|
30
30
|
- `differs`: `changes[]` lists options to add or remove; `held[]` lists units that differ, `diverged` since there is no base; `notes[]` lists options only `b` holds, kept because options are additive. When `b` is a portal side, the note names the pull command that brings the option into config. `pull` does not write a resource that names a portal name a `name` override shadows (`shadowed:<name>`), so on such a resource the note says to correct or remove that override instead. Nor does it bring in a property the files lack that is outside its object's pull scope, so there the note says to add the name to `objects.<object>.include`; a property the files define is always in scope. A snapshot does not record whether HubSpot defines a property, so on a reference `include` does not name, a snapshot's note says it may be outside the scope and gives the same advice.
|
|
31
31
|
- `only-a` or `only-b`: on one side only.
|
|
32
32
|
- `unmanaged`: only a portal side holds it and the other side is config. Listed and counted, never a difference: absence never deletes.
|
|
33
|
-
- `unknown`: a side could not read its object, never read it, left out a property config names because its group's name holds whitespace (`W_UNADDRESSABLE_NAME`), or a target side has a `lookup` override, since this version manages no lookup resources. `reason` says which side and why.
|
|
33
|
+
- `unknown`: a side could not read its object, never read it, left out a property config names because its group's name holds whitespace (`W_UNADDRESSABLE_NAME`), is settling after an apply (plan.md, `W_SETTLING`; the fix says when to compare again, or for a snapshot side when to take a new one), may be a type HubSpot's schema read does not name yet, or a target side has a `lookup` override, since this version manages no lookup resources. A snapshot a later version of Kalup took may hold a type this version does not handle: against config its addresses are `unmanaged`, since config never names them; against a target or another snapshot they are `unknown`, and the fix says to compare with a later version. When such a snapshot's read was incomplete and its coverage records parts this version does not know, what it lacks is `unknown` too, never absent. `reason` says which side and why.
|
|
34
34
|
- `excluded`: a `skip` override, or outside a side's read scope. Listed, not a difference.
|
|
35
35
|
|
|
36
36
|
Config owns the fields it states, less `ignoreChanges`; a reference owns nothing, so only its presence compares. A portal side owns every field it captured; a field HubSpot left out takes its default, or `null` when it has none. With config as `b`, only the fields config states are compared. Options are compared when the config side states them, and always between two portal sides. Config managing what the portal holds as HubSpot-defined or calculated differs in the unit `managed`.
|
package/docs/config.md
CHANGED
|
@@ -9,14 +9,15 @@ This page is the reference. For the walk-through with examples, see [Config file
|
|
|
9
9
|
- `kalup.config.ts`: one `export default defineConfig({...})` and nothing after it. Fields: `name` (default: the name in the nearest `package.json` up to the repository root, else the directory name), `dir` (the folder of object files, relative to `kalup.config.ts` and inside the project, default `hubspot`; `E_SETTING_VALUE` otherwise), `state` (`'local'`, the default, or `'repo'`, state.md), `prefix`, `defaultTarget` (targets.md), `mode` (below), `objects` (the pull scope, pull.md) and `targets` (targets.md). A setting at a level that does not take it is `E_SETTING_LEVEL`, whose fix lists the levels that do; a value it does not take is `E_SETTING_VALUE`, with the nearest allowed one.
|
|
10
10
|
- `hubspot/objects/<object>.ts`: one or more `export const <Name> = defineObject('<object>', {...})` or `defineCustomObject('<name>', {...})`. The writer adds an `export type <Name>Data = ...` line after each. A file with no such export is `E_MISSING_EXPORT`.
|
|
11
11
|
- `hubspot/pipelines/<object>.ts`: one or more `export const <Name> = definePipeline('<object>', {...})`, below. A `definePipeline` export anywhere else is `E_UNSUPPORTED_FILE`.
|
|
12
|
-
- `hubspot/
|
|
12
|
+
- `hubspot/associations.ts`: one `export const Associations = defineAssociations({...})`, below. A `defineAssociations` export anywhere else is `E_UNSUPPORTED_FILE`.
|
|
13
|
+
- `hubspot/index.ts`: the barrel, written by `pull` and `fmt`. It imports each object file as `./objects/<object>.js`, each pipeline file as `./pipelines/<object>.js` and the associations file as `./associations.js`, which resolves under TypeScript `NodeNext`, `Node16` and `Bundler` resolution, bundlers such as Vite and Next.js, and plain Node running `tsc` output. Under `NodeNext`, import it as `./hubspot/index.js`.
|
|
13
14
|
- `hubspot/removed.ts`: tombstones, below.
|
|
14
15
|
- A `defineConfig` or `defineRemoved` file elsewhere under `hubspot/` is `E_UNSUPPORTED_FILE`.
|
|
15
16
|
- `hubspot/blueprints.lock.json` and `hubspot/.blueprints/`: written by `kalup add` (blueprints.md).
|
|
16
17
|
|
|
17
18
|
The folder belongs to Kalup alone: every `.ts` file in it is read as config, `pull` rewrites its `index.ts`, and `init` takes it out of the formatter's checks. Point `dir` at a folder of its own (`lib/config/hubspot`, not `lib/config` next to the app's modules); `init` refuses a folder that holds other `.ts` files (`E_DIR_IN_USE`). A 0.1 project keeps its `kalup/` folder while `dir` is unset and `hubspot/` holds no `.ts` file, with `W_LEGACY_DIR` on every command until you set `dir: 'kalup'` or move the folder. When both hold `.ts` files, every command stops with `E_DIR_AMBIGUOUS` until `dir` says which.
|
|
18
19
|
|
|
19
|
-
Commit `kalup.config.ts` and the folder: the object and
|
|
20
|
+
Commit `kalup.config.ts` and the folder: the object, pipeline and associations files, `index.ts`, `removed.ts`, the blueprints lock, and `state/` only with `state: 'repo'` (state.md). Never commit `.kalup/` (local state, saved plans, journals, history, snapshots), a plan file, `.env` or any file holding a key.
|
|
20
21
|
|
|
21
22
|
## The grammar
|
|
22
23
|
|
|
@@ -24,10 +25,10 @@ Anything else is `E_NOT_DATA`.
|
|
|
24
25
|
|
|
25
26
|
- `import` lines. Imports from `@kalup/core` and `kalup` are rewritten; others are kept, for `p.json` validators.
|
|
26
27
|
- Object literals of `key: value` entries, arrays, strings in single or double quotes on one line, numbers, `true` and `false`. No template strings, identifiers as values, spreads, computed keys, shorthand, or calls other than the builders.
|
|
27
|
-
- A `//` comment on its own line above an export, a group entry, a property entry
|
|
28
|
+
- A `//` comment on its own line above an export, a group entry, a property entry, a stage entry or an association entry, and a comment block above the imports (the file header). Every other comment is an error, including any in `kalup.config.ts` but the header.
|
|
28
29
|
- `p.<kind>('<internal name>')` or `p.<kind>('<internal name>', {...})`, then any of `.strict()` (`p.enum` and `p.multiEnum` only), `.required()`, `.readonly()` and `.managed(false)`, each once. Any other chain call is `E_BAD_CHAIN`. A kind not listed below is `E_UNKNOWN_BUILDER`.
|
|
29
30
|
- `p.json('<name>', <validator>, {...})`. The validator is opaque text and may not hold a `//` comment.
|
|
30
|
-
- A custom object needs `labels: { singular, plural }` and `primaryDisplayProperty`, and may set `requiredProperties`, `searchableProperties` and `secondaryDisplayProperties`.
|
|
31
|
+
- A custom object needs `labels: { singular, plural }` and `primaryDisplayProperty`, and may set `description`, `requiredProperties`, `searchableProperties` and `secondaryDisplayProperties`. Its name, the first argument of `defineCustomObject`, starts with a letter and holds only letters, digits and underscores, at most 50 characters, and each label at most 50; `secondaryDisplayProperties` holds at most two properties, each once. Validate warns about a value breaking these rules (`W_OBJECT_FIELD`), since an object HubSpot holds can already break them, and plan blocks a create or update that would send one. HubSpot never changes the name once it creates the object. A display, required or searchable field may name a property HubSpot gives every custom object, such as `hs_object_id` or `hs_createdate`, or one the object file lists; any other is `W_OBJECT_PROPERTY`, a warning, since the portal may hold it outside the pull scope. A new custom object gets HubSpot's own properties, the group `<name>_information` and associations with activities only; its associations with other objects go in `hubspot/associations.ts`.
|
|
31
32
|
|
|
32
33
|
`E_DUPLICATE_KEY`: a key twice in one literal, an export name twice in one file, or one internal name under two keys. `E_DUPLICATE_ADDRESS`: one address from two files or two exports.
|
|
33
34
|
|
|
@@ -79,13 +80,13 @@ The object key is the app's name for the property. Two exports of one object usi
|
|
|
79
80
|
|
|
80
81
|
## Per-target definitions
|
|
81
82
|
|
|
82
|
-
A target's override `definition` (targets.md) replaces each field it states there, whole, and owns it, empty values included: a property's `label`, `description`, `group`, `fieldType`, `formField`, `options` (no `as`), `hidden`, `displayOrder`, the display fields, `calculationFormula` and lifecycle but `preventDestroy`; a group's `label`; a pipeline's `label` and `displayOrder`; a stage's `label` and metadata field. Else `E_OVERRIDE_DEFINITION`. `pull` writes these fields into the override.
|
|
83
|
+
A target's override `definition` (targets.md) replaces each field it states there, whole, and owns it, empty values included: a property's `label`, `description`, `group`, `fieldType`, `formField`, `options` (no `as`), `hidden`, `displayOrder`, the display fields, `calculationFormula` and lifecycle but `preventDestroy`; a group's `label`; a pipeline's `label` and `displayOrder`; a stage's `label` and metadata field; an association label's `label` and `inverseLabel` (a plain association takes none, and `label` alone leaves the other side as the file has it). Else `E_OVERRIDE_DEFINITION`. `pull` writes these fields into the override.
|
|
83
84
|
|
|
84
85
|
## Mode: addon and takeover
|
|
85
86
|
|
|
86
87
|
`mode: 'addon' | 'takeover'` at the top level, under `objects.<object>`, under `targets.<target>`, or under `targets.<target>.objects.<object>`. The most specific wins, in that order from the last, and the default is `addon`: Kalup manages only what config names. A target `mode` that differs from an object's `mode` the target says nothing more about is `W_MODE_SHADOWED`.
|
|
87
88
|
|
|
88
|
-
Under `takeover`, `plan` archives every custom property and group in the object's pull scope that config lacks and `hubspot/removed.ts` does not name (a group only once every property in it goes, and after them), and removes enum options only the portal holds. Never a HubSpot-defined or calculated property, a kind Kalup does not write, anything an object file lists, a name `exclude` covers, a property a custom object schema names,
|
|
89
|
+
Under `takeover`, `plan` archives every custom property and group in the object's pull scope that config lacks and `hubspot/removed.ts` does not name (a group only once every property in it goes, and after them), and removes enum options only the portal holds. Never a HubSpot-defined or calculated property, a kind Kalup does not write, anything an object file lists, a name `exclude` covers, a property a custom object schema names, a pipeline or stage, or an association. Every takeover removal is destructive: it needs `allowDestroy: true` on the target and a person at a terminal, and `--yes` and `--approve` never cover it. Without `allowDestroy` it is blocked, reason `policy`; after an incomplete read, reason `scope`.
|
|
89
90
|
|
|
90
91
|
## Removed resources
|
|
91
92
|
|
|
@@ -98,7 +99,7 @@ export default defineRemoved({
|
|
|
98
99
|
})
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
`destroy` deletes the resource, only on a target with `allowDestroy: true` (default false). `release` stops managing it, leaving it there. Keys are property, group, pipeline or
|
|
102
|
+
`destroy` deletes the resource, only on a target with `allowDestroy: true` (default false). `release` stops managing it, leaving it there. Keys are custom object, property, group, pipeline, stage or association addresses (`E_TOMBSTONE_ADDRESS`) that config no longer defines (`E_TOMBSTONE_CONFLICT`). A pipeline's tombstone covers its stages, and a custom object's everything on it, its associations on either side included. HubSpot keeps no archive of a pipeline, a stage or an association: a delete is permanent.
|
|
102
103
|
|
|
103
104
|
## Pipelines
|
|
104
105
|
|
|
@@ -128,6 +129,31 @@ export const RenewalsPipeline = definePipeline('deals', {
|
|
|
128
129
|
|
|
129
130
|
A target's override takes a pipeline's or stage's ID on that portal as `name`, `skip` (a skipped pipeline takes its stages), and a `definition` with a pipeline's `label` and `displayOrder` or a stage's `label` and metadata field.
|
|
130
131
|
|
|
132
|
+
## Associations
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// hubspot/associations.ts
|
|
136
|
+
import { defineAssociations } from '@kalup/core'
|
|
137
|
+
|
|
138
|
+
export const Associations = defineAssociations({
|
|
139
|
+
// The person who signed the contract.
|
|
140
|
+
signer: { from: 'deals', to: 'contacts', name: 'deal_signer', label: 'Signer', inverseLabel: 'Signed deal' },
|
|
141
|
+
colleague: { from: 'companies', to: 'contacts', name: 'colleague', label: 'Colleague' },
|
|
142
|
+
visitCompany: { from: 'visit', to: 'companies', name: 'visit_to_company' },
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- Each entry is one association of a pair of objects. `from` and `to` are object keys under `objects`, both (`E_ASSOCIATION_FIELD`); a pair of one object with itself is not managed in this release.
|
|
147
|
+
- `name` is HubSpot's internal name: unique in the portal, set on create and never changed, so it is the address: `association:deals/contacts/deal_signer`. A label made in the HubSpot UI is named after its text in lower case with spaces as underscores. A name or object holding whitespace or `/` is `E_ASSOCIATION_NAME`; a name used twice is `E_DUPLICATE_KEY`.
|
|
148
|
+
- `label` is what records of `from` show, and `inverseLabel` what records of `to` show. Leave `inverseLabel` out and HubSpot shows the label on both sides; `pull` leaves it out when the two are equal. `inverseLabel` needs a `label` (`E_ASSOCIATION_FIELD`). A label is never empty, and two entries of a pair may not show one text from the same side, ignoring case (`E_DUPLICATE_LABEL`).
|
|
149
|
+
- An entry with no `label` is the plain association of the pair. A pair with a custom object has one HubSpot lets you manage, at most one per pair; between two standard objects HubSpot defines it, so an entry for it is `E_ASSOCIATION_FIELD`. Apply creates a pair's plain association before its labels. A label created on a pair with no plain association makes one too, under a name HubSpot picks: the plan says so.
|
|
150
|
+
- The name is the identity, not the direction: rewriting an entry from the other side is the same association, and state follows it. A tombstone on the other direction of an association config holds is `E_TOMBSTONE_CONFLICT`.
|
|
151
|
+
- The keys are free. In the app, `Associations.signer.name` is typed `'deal_signer'`, and `AssociationName<typeof Associations>` is the union of the names.
|
|
152
|
+
- Deleting a label is destructive: records lose that association. HubSpot refuses to delete a plain association while a label of its pair remains, so plan blocks it until the labels go.
|
|
153
|
+
- Kalup does not manage association limits, the most records one record may have under a label, in this release.
|
|
154
|
+
|
|
155
|
+
A target's override takes an association label's `label` and `inverseLabel` as a `definition`, and `skip`.
|
|
156
|
+
|
|
131
157
|
## Canonical form
|
|
132
158
|
|
|
133
|
-
`kalup fmt` validates, then rewrites `kalup.config.ts`, `hubspot/removed.ts`, every object and pipeline file and the barrel: groups and properties sorted by internal name, stages in the order written, tombstones by address, fields in a fixed order, options in display order, quotes as biome writes them, 120 columns. It keeps every value you wrote, `description: ''`, `options: []`, `false` and an empty `lifecycle` included: a present field is owned. `fmt --check` lists the files it would change and exits 2 when there are any. Old files go to `.kalup/history/<timestamp>/` first; the last 20 runs are kept.
|
|
159
|
+
`kalup fmt` validates, then rewrites `kalup.config.ts`, `hubspot/removed.ts`, every object and pipeline file, the associations file and the barrel: groups and properties sorted by internal name, stages in the order written, association entries by name, tombstones by address, fields in a fixed order, options in display order, quotes as biome writes them, 120 columns. It keeps every value you wrote, `description: ''`, `options: []`, `false` and an empty `lifecycle` included: a present field is owned. `fmt --check` lists the files it would change and exits 2 when there are any. Old files go to `.kalup/history/<timestamp>/` first; the last 20 runs are kept.
|
package/docs/dictionary.md
CHANGED
|
@@ -11,7 +11,7 @@ This page is the reference. For the walk-through with examples, see [kalup docs]
|
|
|
11
11
|
|
|
12
12
|
1. `# <project> data dictionary` and a line naming the source: the config files, or the target, portal ID and `observedAt` of the snapshot.
|
|
13
13
|
2. `## Coverage`. For a snapshot: whether the read was complete, the objects not read with the missing scope, objects absent from the portal, what `skip` overrides left out, unsupported properties (Kalup does not write them), schemas without a label, `name` overrides, config properties in a group no address can hold, counts out of scope and shadowed, custom objects config does not name, objects whose pipelines were not read, and the fields Kalup does not capture. Reference properties record only their options.
|
|
14
|
-
3. One `## <object>` section per object key, sorted: a custom object's labels, display property and property lists; a groups table (internal name, label); a properties table (internal name, label, type, field type, group, managed or reference, description); one options table per enumeration (value, label, hidden, description), in display order;
|
|
14
|
+
3. One `## <object>` section per object key, sorted: a custom object's labels, description, display property and property lists; a groups table (internal name, label); a properties table (internal name, label, type, field type, group, managed or reference, description); one options table per enumeration (value, label, hidden, description), in display order; per pipeline, sorted by ID, a `### Pipeline <label> (<id>)` heading, its display order, and a stages table in stage order (stage ID, label, and its probability or state; config adds the key); and an associations table of the associations from the object, sorted (internal name, the other object, label and inverse label; config adds the key). Config adds the key, codec, required and alias columns. Unsupported properties appear only under Coverage.
|
|
15
15
|
4. Config with `definition` overrides: `## Per-target overrides`, one row per address, field and target with its value, sorted.
|
|
16
16
|
|
|
17
17
|
## Escaping
|
|
@@ -20,7 +20,7 @@ Every string from a file or a portal is shown as text: newlines become spaces, c
|
|
|
20
20
|
|
|
21
21
|
## Deterministic
|
|
22
22
|
|
|
23
|
-
The same source gives the same bytes: objects, groups, properties and
|
|
23
|
+
The same source gives the same bytes: objects, groups, properties, pipelines and associations sorted, options and stages in display order, and no timestamp but a snapshot's own `observedAt`. Commit the dictionary and check it in CI:
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
26
|
npx --no-install kalup docs --out DATA-DICTIONARY.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# E_ASSOCIATION_FIELD
|
|
2
|
+
|
|
3
|
+
An entry of `associations.ts` breaks a rule HubSpot keeps for association labels. Exit 3.
|
|
4
|
+
|
|
5
|
+
## When
|
|
6
|
+
|
|
7
|
+
Both objects of an entry must be under `objects` in `kalup.config.ts`, so plan can read the pair. A label holds text: leave `label` out for the plain association of a pair. A pair has one plain association, and between two standard objects HubSpot defines it, so the files cannot. A label is unique per pair and direction (live runs, 2026-10-01 and 2026-10-05); two the same are `E_DUPLICATE_LABEL`. A pair of one object with itself is not managed in this release, and an inverseLabel needs a label.
|
|
8
|
+
|
|
9
|
+
## Fix
|
|
10
|
+
|
|
11
|
+
Change or remove the entry the message names.
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
crew: { from: 'companies', to: 'contacts', name: 'crew_plain' },
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
hubspot/associations.ts:6: E_ASSOCIATION_FIELD: association:companies/contacts/crew_plain has no label, and HubSpot defines the plain association between companies and contacts (fix: give it a label, or remove it: the plain association between two standard objects is always there) (docs: errors/E_ASSOCIATION_FIELD.md)
|
|
21
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# E_ASSOCIATION_NAME
|
|
2
|
+
|
|
3
|
+
An entry of `associations.ts` names an object or an internal name holding whitespace or a slash. Exit 3.
|
|
4
|
+
|
|
5
|
+
## When
|
|
6
|
+
|
|
7
|
+
An association is addressed as `association:<from>/<to>/<name>`, so from, to and name must each be non-empty and hold no whitespace or slash. The name is what HubSpot stores for both directions of the label, unique in the portal and never changed.
|
|
8
|
+
|
|
9
|
+
## Fix
|
|
10
|
+
|
|
11
|
+
Use the object keys from `objects`, and an internal name of letters, digits and underscores, such as the label in lower case with underscores: `charter_signer`.
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
hubspot/associations.ts:6: E_ASSOCIATION_NAME: from, to and name must each be non-empty and hold no whitespace or slash, so an address can hold them (fix: use object keys and an internal name without spaces or slashes) (docs: errors/E_ASSOCIATION_NAME.md)
|
|
17
|
+
```
|
|
@@ -4,14 +4,14 @@ A blueprint needs a custom object config does not define. Exit 1. Nothing was wr
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
A blueprint's resources and its `requires` list name objects. A standard object (contacts, companies, deals, tickets and the rest) is always there, and `kalup add` adds it to `objects` in `kalup.config.ts` when missing. A custom object has to
|
|
7
|
+
A blueprint's resources and its `requires` list name objects. A standard object (contacts, companies, deals, tickets and the rest) is always there, and `kalup add` adds it to `objects` in `kalup.config.ts` when missing. A custom object has to be in config first: a blueprint carries no custom object schema, so its properties would have nowhere to go.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Add the key under `objects` in `kalup.config.ts`. Define the object with `defineCustomObject` in its object file, or, when HubSpot has it already, run `kalup pull` to write that file. Then run the command again.
|
|
12
12
|
|
|
13
13
|
## Example
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
E_BLUEPRINT_REQUIRES: the blueprint needs the custom object vineyard, which config does not define. Nothing was written. (fix: add vineyard: {} under objects in kalup.config.ts and run kalup pull to write its object file
|
|
16
|
+
E_BLUEPRINT_REQUIRES: the blueprint needs the custom object vineyard, which config does not define. Nothing was written. (fix: add vineyard: {} under objects in kalup.config.ts and define it with defineCustomObject, or run kalup pull to write its object file when HubSpot has it) (docs: errors/E_BLUEPRINT_REQUIRES.md)
|
|
17
17
|
```
|
|
@@ -4,7 +4,7 @@ A saved plan deletes something config does not ask to delete. Exit 1. Nothing wa
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
A delete needs a `destroy` tombstone that `kalup rm` wrote, or takeover to ask for it (the mode of the object on the target is takeover, the address is in the pull scope, neither `exclude`, a `skip` override nor a tombstone names it, and the step carries the `takeover` label), and an address gone from config. Before approval, `kalup apply` reads `hubspot/removed.ts` and the object files as data, never running them, and refuses a delete step whose address has neither, is still in config, or sets `lifecycle.preventDestroy`; the message says why takeover does not archive it. It also refuses a delete of a portal resource that another address in config names through a name override on the target. The tombstone was removed after planning, the resource came back into config or into `exclude`, or the plan file was edited.
|
|
7
|
+
A delete needs a `destroy` tombstone that `kalup rm` wrote, or takeover to ask for it (the mode of the object on the target is takeover, the address is in the pull scope, neither `exclude`, a `skip` override nor a tombstone names it, and the step carries the `takeover` label), and an address gone from config. Before approval, `kalup apply` reads `hubspot/removed.ts` and the object files as data, never running them, and refuses a delete step whose address has neither, is still in config, or sets `lifecycle.preventDestroy`; the message says why takeover does not archive it. A step labelled `takeover` needs takeover to ask for it whatever `hubspot/removed.ts` says, so takeover never deletes a custom object, a pipeline or a stage. It also refuses a delete of a portal resource that another address in config names through a name override on the target, and a custom object archive while config still holds anything on the object, since the archive takes it along. The tombstone was removed after planning, the resource came back into config or into `exclude`, or the plan file was edited.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
The file named on the command line is missing, is not JSON, or does not match the `plan/1` schema. The message names the first place that fails. Apply also refuses a file whose steps contradict themselves: a change that writes a value the step's `desired` values do not hold. `kalup plan` never writes such a file.
|
|
7
|
+
The file named on the command line is missing, is not JSON, or does not match the `plan/1` schema. The message names the first place that fails. Apply also refuses a file whose steps contradict themselves: a change that writes a value the step's `desired` values do not hold, or a custom object archive whose `expect` does not count the properties, groups and pipelines it takes along. `kalup plan` never writes such a file.
|
|
8
|
+
|
|
9
|
+
A step of a resource type this version does not handle: a later version of Kalup made the plan. The message names the step and that version. Apply the plan with it, or a later one.
|
|
8
10
|
|
|
9
11
|
## Fix
|
|
10
12
|
|
|
@@ -4,7 +4,7 @@ A step in the plan does not match what Kalup derives from state and the portal.
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Under the portal lock, `kalup apply` reads state and the portal again and derives each step's risk, labels and blocked status as `plan` does. It refuses when a step states a lower risk than derived, leaves out a derived label (`reverts-ui-edit`, `overwrites-portal`, `takeover`), or would be blocked: an update of what state does not own, a delete of what state does not own that takeover does not archive, an adopt of what it does, a delete or takeover option removal the target does not allow, a takeover archive of what HubSpot defines, of a property in a group a `skip` override covers or one a custom object schema names, of a group that held no property, or whose `expect` leaves out a field the base holds (so an edit made in HubSpot after the review would not stop it), a custom object schema change, or a group delete while properties still name the group. A plan `kalup plan` saved matches, unless config changed since, such as a `skip` override added; otherwise the file was edited.
|
|
7
|
+
Under the portal lock, `kalup apply` reads state and the portal again and derives each step's risk, labels and blocked status as `plan` does. It refuses when a step states a lower risk than derived, leaves out a derived label (`reverts-ui-edit`, `overwrites-portal`, `takeover`), or would be blocked: an update of what state does not own, a delete of what state does not own that takeover does not archive, a delete labelled `takeover` of a custom object, a pipeline, a stage, something on an object whose mode is not takeover, or something `hubspot/removed.ts` names or covers, an adopt of what it does, a delete or takeover option removal the target does not allow, a takeover archive of what HubSpot defines, of a property in a group a `skip` override covers or one a custom object schema names, of a group that held no property, or whose `expect` leaves out a field the base holds (so an edit made in HubSpot after the review would not stop it) or, for a custom object archive, a count of what it takes along, a custom object schema change, or a group delete while properties still name the group. A plan `kalup plan` saved matches, unless config changed since, such as a `skip` override added; otherwise the file was edited.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
`preventDestroy: true` in a property's `lifecycle` says the property must never be deleted through Kalup. `kalup rm <address>` writes a `destroy` tombstone, which a later plan turns into a delete, so rm refuses it before it changes any file.
|
|
7
|
+
`preventDestroy: true` in a property's `lifecycle` says the property must never be deleted through Kalup. `kalup rm <address>` writes a `destroy` tombstone, which a later plan turns into a delete, so rm refuses it before it changes any file. A custom object's archive takes every group, property and pipeline on it along, so `kalup rm object:<name>` refuses while any of them sets `preventDestroy`, and names them. After `kalup rm object:<name> --release` they have left config, so rm will not turn that release into a destroy: remove the release tombstone, pull the object back, then remove it again.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ A key in `hubspot/removed.ts`, or the address given to `kalup rm`, is not the ad
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
Each key in `hubspot/removed.ts` is an address, such as `property:companies/legacy_score`: the type, a colon, the object, a slash and the name. A key with no object, such as `property:legacy_score`, names nothing and is refused. A stage address names its pipeline as well: `stage:deals/renewals/won`. This version removes properties, property groups, pipelines and stages
|
|
7
|
+
Each key in `hubspot/removed.ts` is an address, such as `property:companies/legacy_score`: the type, a colon, the object, a slash and the name. A key with no object, such as `property:legacy_score`, names nothing and is refused. A stage address names its pipeline as well: `stage:deals/renewals/won`. This version removes custom objects, properties, property groups, pipelines and stages, so a key of another type, such as `list:renewals`, is refused as well, and so is `object:<name>` for a standard object, which HubSpot defines, or for a key that is not under `objects` in `kalup.config.ts`.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ An address is in `hubspot/removed.ts` and still defined in config. Exit 3.
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
A tombstone takes a resource out of config: `destroy` deletes it in the portal, `release` stops managing it and leaves it there. Config may not define the same address at the same time, not even as a reference without `label`, `group` and `fieldType`. This usually means the entry was added to `hubspot/removed.ts` by hand and the property or group was left in its object file.
|
|
7
|
+
A tombstone takes a resource out of config: `destroy` deletes it in the portal, `release` stops managing it and leaves it there. Config may not define the same address at the same time, not even as a reference without `label`, `group` and `fieldType`. This usually means the entry was added to `hubspot/removed.ts` by hand and the property or group was left in its object file. A custom object tombstone takes everything on the object along, so config may not hold any group, property, pipeline or stage on that object either; `kalup rm object:<name>` takes them out with it.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@ A key under `objects` is neither a standard object nor a custom object in the po
|
|
|
4
4
|
|
|
5
5
|
## When
|
|
6
6
|
|
|
7
|
-
A key that is not a standard object name (`contacts`, `companies`, `deals`, `line_items` and the rest, plural) is read as a custom object. None of the portal's custom objects has that name.
|
|
7
|
+
A key that is not a standard object name (`contacts`, `companies`, `deals`, `line_items` and the rest, plural) is read as a custom object. None of the portal's custom objects has that name, and config defines none either. A key a `defineCustomObject` in config backs is never this code: `pull` reports that object missing in portal and leaves its file as it is, `plan` creates it, and `compare` finds it on the config side only. A key whose object `hubspot/removed.ts` names is left out by `pull` too, whether or not HubSpot still holds it.
|
|
8
8
|
|
|
9
9
|
## Fix
|
|
10
10
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# W_OBJECT_FIELD
|
|
2
|
+
|
|
3
|
+
A warning from validate: a custom object in config has a name, a label or secondary display properties HubSpot refuses. Exit stays 0.
|
|
4
|
+
|
|
5
|
+
## When
|
|
6
|
+
|
|
7
|
+
A custom object name starts with a letter and holds only letters, digits and underscores, at most 50 characters, and its singular and plural labels hold at most 50 characters each. HubSpot refuses anything else on create (live runs, 2026-10-05). The name is the first argument of `defineCustomObject` and is permanent once HubSpot creates the object; the labels can change.
|
|
8
|
+
|
|
9
|
+
`secondaryDisplayProperties` holds at most two properties, each once: HubSpot refuses a third (live runs, 2026-10-01).
|
|
10
|
+
|
|
11
|
+
An object HubSpot holds already can break these rules, so validate only warns. `plan` blocks a create, or an update, that would send such a value, with reason `unsupported`.
|
|
12
|
+
|
|
13
|
+
## Fix
|
|
14
|
+
|
|
15
|
+
Choose a name HubSpot takes, such as `orchard_visit`, or shorten the label. For an object HubSpot holds already, the name in config is the portal's: keep it. List at most two secondary display properties, each once.
|
|
16
|
+
|
|
17
|
+
## Example
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export const Visit = defineCustomObject('orchard-visit', {
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
hubspot/objects/orchard_visit.ts:3: W_OBJECT_FIELD: 'orchard-visit' is not a custom object name HubSpot takes: a letter, then letters, digits and underscores, at most 50 characters (fix: choose another name; HubSpot never changes a custom object name once it creates the object) (docs: errors/W_OBJECT_FIELD.md)
|
|
25
|
+
```
|