Skip to content

Commit a78dee0

Browse files
committed
Merge origin/main into claude/project-thread-yg03y1
2 parents 34d4562 + 35f2055 commit a78dee0

64 files changed

Lines changed: 3704 additions & 314 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/cli': patch
4+
'@objectstack/metadata-protocol': patch
5+
---
6+
7+
feat(objectql,cli,metadata-protocol): backfill the currency code of rows stored before an amount field was switched to `currencyMode: 'record'`, and acknowledge the switch at the metadata save
8+
9+
Clause-②: yes (widening) — a new `@objectstack/objectql` export (`backfillRecordCurrencyCodes` and its report helpers), a new command `os migrate record-currency-codes`, and a new finding `currency_mode_to_record` registered in the existing 409 `destructive_change` face, lifted by its existing `force`
10+
Metadata-protocol: none
11+
12+
An existing `dynamic` or `fixed` amount switched to `record` names a code field, and the rows already stored hold an amount beside a blank code. Such a row is not refused for resting, but until it holds a code it displays as a plain number, falls into the `null` group of a grouped aggregate, and sits outside every currency-paired filter.
13+
14+
- **The backfill.** `os migrate record-currency-codes --object <object> --field <amount> --code <ISO 4217>` writes the code you give into every row whose amount is not blank and whose code is blank. ⛔ The code is never derived: not from a former `defaultCurrency`, not from the organization's `localization.currency`. A row that holds a code is never touched; a row with a blank amount is left alone. Dry run by default; `--apply` writes (with `--yes` when not interactive). The report counts the rows written, the rows skipped because they hold a code, and the rows skipped because their amount is blank. Running it again finds nothing, so a clean second report is the verification. A code that is not an ISO 4217 code, or a field that is not a `record`-mode currency field, is refused before any row is read (`INVALID_FIELD`, 400). Each row is written through the engine's update, so the write is judged and audited like any other. No `sys_migration` flag is recorded.
15+
- **The engine function.** `backfillRecordCurrencyCodes(engine, logger, { object, field, code, apply })` from `@objectstack/objectql`, with `formatRecordCurrencyBackfillReport` and `recordCurrencyBackfillComplete`; `isRecordCurrencyBlank` is the one blank predicate the write guard and the backfill share.
16+
- **The save.** Saving an object (REST `PUT /meta`, Studio, a package publish) whose stored amount field moves to `currencyMode: 'record'` from any other mode now answers the existing 409 `DESTRUCTIVE_CHANGE` with the finding `currency_mode_to_record`, whose message names the three consequences and the command. Re-submit with `force` to save. It reads no row, and fires only where a previous version exists: a code-declared package registered at boot is not refused.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
fix(spec): the permission-set form asks for the label first, and requires it
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The Identity section of `permissionForm` (the form `/meta/types` serves for the `permission` kind) listed `name`, the API name, before an optional `label`, so an administrator creating a permission set typed a machine name first and could save a set with no human label, while the Console's create form fills `name` from the label. The section now lists `label` first and required, then `name`, whose help says it is derived from the label when the set is created.
12+
13+
`required` on a form row is a layout flag the renderer reads: `PermissionSetSchema` is unchanged and still accepts a set with no label, and nothing changes in how `name` is derived.
14+
15+
`@objectstack/platform-objects` carries the regenerated metadata-form bundles: the `permission` form's `label` row now precedes `name` in every locale, the English `name` help is the new source text, and the zh-CN, ja-JP and es-ES `name` help is translated to match.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: the flow-node expression ledger roots a path in a node's sibling block, and declares a `connector_action` node's `connectorConfig.input` a value slot
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: registered connector-action-input-value-slot-judged -->
11+
12+
**BREAKING** accept-set narrowing of `connectorConfig.input` on `connector_action` flow nodes, served at `AutomationEngine.registerFlow` and `objectstack validate` (see the `@objectstack/service-automation` and `@objectstack/lint` entries). This package carries the ledger both doors read.
13+
14+
- **The ledger.** `FLOW_NODE_EXPRESSION_PATHS` could address a node's `config` only, so the one value map the executor reads from a sibling block — `connectorConfig.input`, evaluated before dispatch since the executor half landed — was declared nowhere and judged by no door. An entry may now name `block: 'connectorConfig'` (absent means `config`); `FLOW_NODE_SIBLING_EXPRESSION_BLOCKS` lists the blocks a path can be rooted in with the node type that reads each, and `FlowNodeExpressionBlock` / `FlowNodeSiblingExpressionBlock` / `FlowNodeSiblingBlocks` type them. The new entry is `connector_action` · `connectorConfig` · `input.*` · `value`.
15+
- **The resolvers.** `resolveFlowNodeExpressions`, `resolveFlowNodeValueSlots` and `flowNodeValueTemplateRefusals` take an optional third argument, the node's sibling blocks (pass the node itself), and each result names the `block` its `path` is inside. A caller that passes none is answered for `config` only, exactly as before; every `config`-rooted result reads `block: 'config'`.
16+
- **The declaration.** `FlowNodeSchema.connectorConfig.input`'s map value carries `.meta({ xExpression: 'value' })`, and `getFlowNodeSiblingBlockJsonSchemas()` hands the node's blocks other than `config` to the reconciliation ratchet as its third channel. The value stays `z.unknown()`: `FlowSchema.parse` does not judge the map, as it judges no value slot of a node's open `config`.
17+
- **Unchanged.** Every `config`-rooted entry resolves byte-identically. The `FlowSchema` example comment no longer says `connectorConfig.input` is handed to the action as written.
18+
19+
**The remedy.** Write a computed input value as a CEL value envelope, `{ dialect: 'cel', source: 'record.id' }`, and a constant as the literal it is; a string that carried a `{…}` token takes the CEL spelling its refusal names (`'{rec.id}'` → `{ dialect: 'cel', source: 'rec.id' }`).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
---
4+
5+
fix(service-automation)!: `registerFlow` refuses a malformed envelope and a `{…}` token in a `connector_action` node's `connectorConfig.input`
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: registered connector-action-input-value-slot-judged -->
11+
12+
**BREAKING** accept-set narrowing at `AutomationEngine.registerFlow`.
13+
14+
- **Before.** The executor evaluates `connectorConfig.input` as a value-slot map, but registration judged value slots through a ledger that could address `config` only. A malformed CEL envelope (`{ dialect: 'cel', source: 'rec.' }`), a template envelope (`{ dialect: 'template', … }`) and a `{…}` token string (`'{rec.id}'`) all registered there, while the same three under a `subflow`'s `config.input` were refused. The malformed envelope failed the step at run time; the token string was handed to the action as its literal text.
15+
- **Now.** The value-envelope and value-slot template checks read the ledger's `connectorConfig` entry, so all three are refused at registration, each located at `connectorConfig.input.<key>` with the same refusal a `subflow` input value draws. A flow row in `sys_metadata` carrying one is skipped at boot with a `warn` naming it, its trigger not armed.
16+
- **Unchanged.** Literals and valid CEL envelopes register and dispatch as before; an envelope nested inside a literal is data; `input` stays optional.
17+
18+
**The remedy.** Write a computed value as `{ dialect: 'cel', source: '…' }` and a constant as a literal; follow the CEL spelling each refusal names.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/lint': minor
3+
---
4+
5+
fix(lint)!: `objectstack validate` judges a `connector_action` node's `connectorConfig.input` as a value slot
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: registered connector-action-input-value-slot-judged -->
11+
12+
**BREAKING** accept-set narrowing at `validateStackExpressions` (`objectstack validate`), and so at the runtime authoring gate that runs the same pass on a flow write.
13+
14+
- **Before.** A malformed CEL envelope, a template envelope and a `{…}` token string in `connectorConfig.input` passed clean; the same three under a `subflow`'s `config.input` were `error`s.
15+
- **Now.** Each is an `error` at `node 'ID' (connector_action) connector_action input value at connectorConfig.input.KEY`, with the message a `subflow` input value draws. A valid envelope is also held to the value slots' other checks, the unbound-root judgment included, as every value slot is.
16+
- **Unchanged.** Literals and valid CEL envelopes pass.
17+
18+
**The remedy.** Write a computed value as `{ dialect: 'cel', source: '…' }` and a constant as a literal; follow the CEL spelling each refusal names.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation)!: a connector pull whose input breaks the read action's declared `inputSchema` is refused before the action is called, with `VALIDATION_ERROR` naming each parameter (#28040)
6+
7+
Clause-②: yes (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export, type or error code is removed or renamed: `MappingSchema.connectorSource` and `ConnectorActionSchema.inputSchema` keep their shapes, and the refusal reuses the existing `VALIDATION_ERROR` code the pull door already answers its other input faults with. What narrows is the run-time dispatch of the connector pull: a call the action's own declared schema says is wrong is no longer sent upstream. Nothing stored is rewritten or re-judged at rest, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
11+
12+
**BREAKING** (an accept-set narrowing at run time), shipped as `patch`: nothing a metadata author writes changes shape (`Metadata-protocol: none`), so the level follows the kind, and the banner and the ADR-0087 disposition stay.
13+
14+
**FROM.** `pullConnectorSource` (the door a `job`'s `pull` runs) built the action's input from `connectorSource.input` plus the watermark parameter and called the connector's handler with it unchecked. The check the `connector_action` flow node gained in #27995 did not cover this door, so a pull whose input missed a parameter the action declares `required` still reached the remote API; for an OpenAPI `getPet`-style action the path placeholder was sent literally.
15+
16+
**TO.** The input the pull is about to send, with the watermark parameter added, is judged against the action's `inputSchema` by the same check the `connector_action` node runs (one shared helper, so the two doors judge alike: `required`, `type` with OpenAPI 3.0 `nullable`, descent through `properties` and `items`; keywords outside that subset never refuse). A failing input throws `ConnectorPullError` with `code: 'VALIDATION_ERROR'`, `status: 400` and the new `reason: 'input_schema_mismatch'`, naming each problem by its dotted path (`missing required 'query.since'`, `'query.limit' must be integer, got string`); a text value where a number or boolean is declared adds a hint to write it as that JSON type (no CEL runs on this door, so there is no cast to suggest). The action is not called and nothing is written, so the job run is recorded `failed` and its `retryPolicy` applies. An action that declares no `inputSchema` pulls whatever it is given, as before. The `connector_action` node's behaviour and messages are unchanged.
17+
18+
**TypeScript.** `ConnectorPullRefusalReason` gains `'input_schema_mismatch'`, and `ConnectorPullRegistry` now requires `getConnectorDescriptors()` (where the action's `inputSchema` is read). The automation engine already has it; a hand-built registry passed to `pullConnectorSource` adds it.
19+
20+
**Who sees it.** A `mapping` whose `connectorSource.input` omits a parameter the read action declares `required`, or sends one of a type the action does not declare: its pull now fails before the call instead of the remote answering it. A required parameter that only the watermark supplies is absent on the first pull (nothing is stored yet), so that pull is refused too.
21+
22+
**Fix.** Make `connectorSource.input` satisfy the action's `inputSchema`, which `GET /api/v1/automation/connectors` lists: supply every required parameter, and write each one as the declared type (`{ query: { limit: 50 } }`, not `{ query: { limit: '50' } }`). The watermark parameter is sent as the value stored in its target field, so the action must declare that parameter with the stored field's type (a text or datetime field ⇒ `string`, the ISO form a pull sends for a date), and must not declare it `required` unless `connectorSource.input` also gives it a first value.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): a refused `state_machine` move is named by the states' option labels, not their raw values
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The built-in `invalid_transition` sentence filled its `{{from}}` / `{{to}}` from the raw option values, so a user refused a status change read `Invalid transition for Status: presented → draft`. It now reads `Invalid transition for Status: Presented → Draft` — each state named the way the form's dropdown shows it, in the caller's locale:
11+
12+
- the field's own option translation (`objects.<object>.fields.<field>.options.<value>`), then, for a picklist-bound field, the shared list's (`picklists.<name>.options.<value>`), then the option's declared `label`, then the raw value for an option with no label;
13+
- asked through the same translation hook the field label resolves through, so a `zh-CN` caller reads `状态不允许从 已提交 变更为 草稿`.
14+
15+
What a client acts on is unchanged: `code` stays `invalid_transition` and `constraint: { from, to }` keeps the raw option values. An author-written rule `message` is handed over exactly as before — it gains no `{{from}}` / `{{to}}` holes. The `invalid_initial_state` sentence is unchanged.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec)!: `EngineQueryOptionsSchema.searchFields` and its aggregate and count siblings refuse an empty list, as `FullTextSearchSchema.fields` does (#28059)
6+
7+
Clause-②: no (narrowing) — the engine-side declarations of the search-field override refuse `searchFields: []`, which they admitted before. Nothing is widened and no key is added, removed or renamed.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no metadata type or stored shape moves: these are in-process engine option declarations no metadata type embeds, never stored, and the value they stop admitting is the one the Data API doors and the shared FullTextSearchSchema.fields already refuse. A caller names at least one field or leaves the key out; there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing of published engine option declarations (`@objectstack/spec/data`), read by codegen, the reference docs and any caller that parses an engine option bag with them.
13+
14+
- **Before.** `EngineQueryOptionsSchema.searchFields`, `EngineAggregateOptionsSchema.searchFields` and `EngineCountOptionsSchema.searchFields` admitted `[]`, while the structured `search.fields` on the same schemas (it reuses `FullTextSearchSchema`) refused it. One schema said two things about one value.
15+
- **Now.** All three take the shared search-field array arm: a list must hold at least one name, refused with the same sentence (`Name at least one field, or leave the key out to search the default search fields`) at `searchFields`. A list with a blank member is still admitted, as the Data API doors admit it and then refuse the blank name as an unknown field.
16+
- **Unchanged.** The Data API doors already refused an empty override, and in the same release the in-process engine refuses it too (#27732): `engine.find` / `findOne` / `count` / `aggregate` called directly with `searchFields: []` or `search.fields: []` answer `400 VALIDATION_ERROR` with the same sentence. The engine does not parse its options with these schemas, so the declarations now state the refusal it enforces. The TypeScript types are unchanged (`string[] | undefined`).
17+
- **Who sees it.** Code that parses an engine option bag with one of these schemas and sent `searchFields: []`, or a generated client that trusted the wider declaration.
18+
19+
**Supersedes, in the same release.** Two earlier entries no longer hold for these declarations: the `#27683` entry's "Unchanged" bullet saying the engine-side declarations still admit `[]`, and the `#27484` entry's "Not yet narrowed" bullet, whose remaining member `EngineQueryOptionsSchema.searchFields` (with its aggregate and count siblings) refuses `[]` now. A string `''` was never part of these array declarations; it is the transport spelling `$searchFields`, narrowed by `#27683`.
20+
21+
**The remedy.** Name at least one field in `searchFields`, or leave it out to search the object's default search fields.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql): every item a composed stack declares only on its top level, beside package bodies that do not declare it, registers in the engine at boot the way a package body's item of that type does
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
The engine's `manifest` service registered a multi-package stack's residual (top-level items no package body declares, ADR-0130 D4) objects, connectors and flows under the stack's own `manifest.id`, and no other type. A residual permission set, position, capability, sharing rule, queue, assignment rule, app, view, page, dashboard, report, action, hook, agent, tool, skill, API endpoint, webhook, email template, doc and every other collection a package body registers was held by the metadata service alone, so a reader that asks the engine registry answered as if it were not declared. Among those readers is the metadata save door's managed-content check: a residual top-level security rule was not sealed against an environment save the way the same rule declared in a package body is; it now is.
11+
12+
Each such item now registers under the stack's manifest id and version, through the judges a body's item of that type passes: a view container is filed under its object and expanded into its views after the divergent-`name` judge, a `views:` entry that is not a container is refused as a body's is (422 `INVALID_METADATA`), a permission set whose `packageId` names another package is refused (ADR-0086 D3), an action is parsed against its schema, and the boot's later checks (the ownership-queue service check for queues and assignment rules, the bound-action target audit) now cover residual items too, so a residual queue on a composition without an ownership-queue service fails the boot with the prescription a body's does. Picklists stay metadata-only, as the residual objects rule already states. A top-level item a package body declares is still registered once, as that body's; stacks with no residual item, and single-package stacks, are unchanged.

0 commit comments

Comments
 (0)