Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/30944-skills-field-patches-example-hotcrm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/skills': patch
---

docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patches options only on a field that declares its own

Clause-②: no
Metadata-protocol: none

The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`). The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes.
10 changes: 10 additions & 0 deletions .changeset/30944-spec-field-patches-example-hotcrm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': patch
---

docs(spec): the `fieldPatches` description's example no longer patches an option of a picklist-bound field

Clause-②: no
Metadata-protocol: none

`ObjectExtensionSchema.fieldPatches`'s description and the `ObjectExtensionSchema` TSDoc taught `industry: { label: 'Sector', options: { technology: { label: 'Tech' } } }`. HotCRM's `crm_account.industry` takes its options from a shared picklist, so the install door and `os validate` refuse that patch (`picklist-option`). The description now says an option of a picklist-bound field is never patchable and only a field declaring its own `options` takes an option patch, and its example sets `industry`'s label and help text and patches `type`'s `customer` option. The generated `references/data/object.mdx` follows. Description text only: no schema, key or behaviour changes.
5 changes: 4 additions & 1 deletion content/docs/data-modeling/object-extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ export const AccountWordingExtension = defineObjectExtension({
industry: {
label: 'Sector',
inlineHelpText: 'The market the account sells into.',
options: { technology: { label: 'Tech' } }, // addressed by the stored value
// no `options` here: `industry` takes its options from a shared picklist
},
type: {
options: { customer: { label: 'Client' } }, // addressed by the stored value
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ const result = ApiMethod.parse(data);
| :--- | :--- | :--- | :--- |
| **extend** | `string` | ✅ | Target object name (FQN) to extend |
| **fields** | `Record<string, { name?: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; description?: string; … }>` | optional | Fields to add/override, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`). |
| **fieldPatches** | `Record<string, { label?: string; description?: string; inlineHelpText?: string; placeholder?: string; … }>` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into', options: { technology: { label: 'Tech' } } } }`. |
| **fieldPatches** | `Record<string, { label?: string; description?: string; inlineHelpText?: string; placeholder?: string; … }>` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never patchable: its labels are the picklist's, so only a field declaring its own `options` takes an option patch. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into' }, type: { options: { customer: { label: 'Client' } } } }`. |
| **label** | `string` | optional | Override label for the extended object |
| **pluralLabel** | `string` | optional | Override plural label for the extended object |
| **description** | `string` | optional | Override description for the extended object |
Expand Down
10 changes: 7 additions & 3 deletions packages/spec/src/data/object.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5112,8 +5112,10 @@ function fieldPatchesDescription(): string {
'(`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant\'s own text keeps ' +
'winning, and between two extensions the higher `priority` wins. A patch naming a field or option value ' +
'the target does not have is refused at install and by `os validate`; a field the same extension ' +
'replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: \'Sector\', ' +
'inlineHelpText: \'The market the account sells into\', options: { technology: { label: \'Tech\' } } } }`.'
'replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never ' +
'patchable: its labels are the picklist\'s, so only a field declaring its own `options` takes an option ' +
'patch. Example: `fieldPatches: { industry: { label: \'Sector\', inlineHelpText: \'The market the ' +
'account sells into\' }, type: { options: { customer: { label: \'Client\' } } } }`.'
);
}

Expand All @@ -5128,7 +5130,9 @@ function fieldPatchesDescription(): string {
* fields: { sales_stage: Field.select([...]) },
* // A base field's display properties only (ADR-0146 D10), applied by the
* // registry merge (`mergeObjectDefinitions`) — see `fieldPatches`.
* fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } },
* // Where `industry` is picklist-bound, as on HotCRM's `crm_account`, its options are not
* // patchable (its display keys are); an option patch goes on a field declaring its own options.
* fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } },
* }]
* ```
*/
Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,15 +361,15 @@ values and rules stay the owner's. A key the tenant edited keeps the tenant's
text; between two extensions the higher `priority` wins. A patch naming a field
or option value the target lacks is refused at install and by `os validate`, and
a `picklist`-bound field's options are never patchable. Example:
`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } } })`.
`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } })`.

**An object an installed package ships** (ADR-0146 D10) is extended from the
base's paired `.custom` package (`hotcrm.custom`; **objectstack-platform**
§ Customizing an installed package), never replaced: it takes no `overrides`
declaration. Every field the pair adds ends in `__c` (`region__c`). The suffix is
reserved to the pair, so a `__c` field in any other package, or an unsuffixed
field in a `.custom` extension, is refused at save, publish and install and by
`os validate` / `os build`; a base field changes only through `fieldPatches`.
`os validate` / `os build`.
Studio's **Add field** on a base object writes the same extension, as an
`object_extension` item of the pair.

Expand Down
Loading