Skip to content

Commit 91d44bd

Browse files
committed
Merge origin/main into claude/project-thread-o8o2xv (#25916)
Resolve the scripts/sdui-manifest.record.json conflict with #25977 by regenerating the record with node scripts/gen-sdui-manifest-node.mjs. Claude-Session: https://claude.ai/code/session_01C6T942afhqAP5w9XBmfTDC Co-authored-by: Claude <noreply@anthropic.com>
2 parents 3d2fd1d + a4eae06 commit 91d44bd

85 files changed

Lines changed: 5264 additions & 710 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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: an object's own actions are stored as their `ActionSchema` parse, so `requiresFeature` takes effect on a raw manifest's object actions
6+
7+
Clause-②: no (narrowing: an object's own action that fails `ActionSchema` is refused at the runtime registration door, as #25649 did for standalone actions)
8+
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: `ActionSchema` and `ObjectSchema` are byte-unchanged, and no spec key, authorable spelling, stored row or stored shape is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no old-to-new mapping to record. What changes is one runtime door converging on the existing contract: the schema registry now stores a code package's object with its own actions as their parse and refuses an object whose action does not parse. Every body it refuses was already off-contract, and refused by the authoring doors (`defineStack`, `os validate`, the metadata save door), before this change. The remedy is a correction of that body against the issues the refusal lists, not a conversion between two contract versions. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
The schema registry now parses an object's own `actions` through `ActionSchema` when a code package registers the object (`SchemaRegistry.registerObject`), and stores the parsed actions. A standalone `action` item has been stored this way already; both now go through one parse. Before this, an object that reached the registry without a parse kept its actions as authored. Examples are a raw plugin manifest, a `strict: false` stack, and a programmatic `registerApp`. Such an object was served with its actions unparsed: a declared `requiresFeature` (or `requiresMembershipReach`) was never lowered into `visible`, so the gate did nothing.
14+
15+
What the parse does to such an action, as with any parsed action:
16+
17+
- it lowers `requiresFeature` / `requiresMembershipReach` into `visible`;
18+
- it fills schema defaults;
19+
- it wraps a CEL string shorthand in `{ dialect, source }`.
20+
21+
An object a strict `defineStack` or `ObjectSchema.create` already parsed is stored unchanged. A registration with no package, and a tenant-authored body (the metadata save door's write-through, the boot rehydration of stored rows), is not judged here.
22+
23+
**BREAKING** for a package that registers an object whose own action `ActionSchema` does not accept.
24+
25+
- **Before.** The registry stored such an object as written, and served the action with any declared gate inert.
26+
- **Now.** The object's registration is refused with `INVALID_METADATA` (422), and nothing of it is stored. The error names the action, its object and its package, and `issues[]` gives the path (`actions.N.KEY`) and message of each failure.
27+
- **Fix.** Correct the action as `issues[]` describes. An object that passes `os validate`, or a strict `defineStack`, is unaffected.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
'@objectstack/lint': minor
5+
'@objectstack/platform-objects': minor
6+
'@object-ui/app-shell': minor
7+
---
8+
9+
feat(spec,analytics,lint,platform-objects,app-shell): a dataset measure can aggregate the time between two datetime fields of each record
10+
11+
Clause-②: yes (widening)
12+
Metadata-protocol: added
13+
14+
- **`@objectstack/spec`**: `DatasetMeasureSchema` gains an optional `duration: { from, to, unit }`, in place of `field`. `from` and `to` name two `datetime` fields of the dataset's own object by their bare names, `unit` is `seconds`, `minutes`, `hours` or `days`, and the measure's `aggregate` is `avg`, `min`, `max` or `sum` of `to` minus `from`. A relationship path, an expression or the row wildcard in either column, the same column twice, another unit, a counting aggregate, or a `field`, `derived` or `currency` beside it is refused at parse. New exports: `DatasetMeasureDurationSchema`, `DatasetDurationUnit`, `DATASET_DURATION_UNIT_SECONDS`, `DATASET_DURATION_AGGREGATES`, `DATASET_DURATION_FIELD_TYPE` and the `DatasetMeasureDuration` / `DatasetDurationUnit` types.
15+
- **`@objectstack/service-analytics`**: the dataset compile refuses a duration column declared anything but `datetime` (`400 DATASET_INVALID`, naming the column and its type). The native-SQL strategy computes the duration as one statement on SQLite, PostgreSQL and MySQL, in double precision on each; SQLite and PostgreSQL are tested live to answer the same number: a record missing either instant is left out, a `to` before its `from` counts as negative, and over a group with no such record `sum` answers `0` and the others `null`. Measure filters, derived measures, ordering and the draft preview serve it. A query the native strategy does not serve (a time dimension bucketed by `granularity`, a field-to-field filter, an object served through engine middleware or an external datasource, a non-SQL datasource) is refused with `400 INVALID_FIELD` naming the measure. Its two columns pass the field-level read gate like any measure's field. The temporal half of the aggregate × field-type refusal now names the duration measure instead of a stored "days open" number.
16+
- **`@objectstack/lint`**: `os validate` refuses a duration column not declared `datetime` with the new gating rule `measure-duration-field-type-refused` (at `datasets[N].measures[M].duration.from` / `.to`), with an `os explain` entry. `dataset-field-unknown` now also reports a duration column the object does not declare.
17+
- **`@objectstack/platform-objects`**: the dataset form's new **Duration** row is translated in every shipped locale.
18+
- **`@object-ui/app-shell`**: changing a dataset's base object in the console's dataset inspector now clears a duration measure along with the other measures that name the old object's fields, instead of keeping it as if it were a plain row count.
19+
20+
What to do: nothing. A dataset without `duration` parses, compiles and answers exactly as before.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
fix(plugin-audit): an activity entry for an update that moves no `trackHistory` field names the fields that changed
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The record Discussion feed wrote `Updated <object> "<label>"` for every update that fired no milestone and changed no `trackHistory` field, so a record a roll-up, flow or seed updates repeatedly showed a run of identical entries with nothing to tell them apart. That entry now names the changed fields by label, in declaration order: `Updated Campaign "CPG-0001": Number Sent, Responses, and Leads`. Values are still rendered only for `trackHistory` fields; an untracked field's value never reaches the text.
11+
12+
At most five fields are named and the rest counted (`…, and 3 more`), and fewer are named when the text would exceed the `maxLength` declared on `sys_activity.summary`. Each named or counted field is recorded as a source of the summary, so a reader not served one of those fields is served the entry without its summary, as for a tracked-change entry. Two message keys are added to the plugin's en, zh-CN, ja-JP and es-ES bundles, `messages.activityUpdatedFields` and `messages.activityMoreFields`. A locale bundle that defines `messages.activityUpdated` but not `messages.activityUpdatedFields` keeps its field-less sentence.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): three comments describe the shared-chunk bundle layout, not the per-entry one
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The module doc block in `@objectstack/spec`'s root entry, the `@__PURE__` note on the conversions table, and the Package API assembled-entry doc block (the generated reference page of the same name) still said each entry is a self-contained, single-file bundle. The build is one module graph: a module two entries reach ships once, in a shared chunk, and an entry links every chunk its graph touches. The sentences now say that. The doc text rides in the shipped declaration files, which is why this is a patch entry; no export, key, value or behaviour changes.
11+
12+
What to do: nothing.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/runtime': patch
3+
---
4+
5+
fix(runtime)!: the flow trigger door refuses a `params` that is not a JSON object, instead of running the flow with an empty bag
6+
7+
`POST /api/v1/automation/:name/trigger`, its legacy spelling `POST /api/v1/automation/trigger/:name` (the route `client.automation.trigger()` calls), and every declared `type: 'flow'` API endpoint build the flow's context the same way. That build read `params` only when it was a truthy object. A string such as `{"params":"oops"}` became an empty params bag, and the flow ran with no inputs: it failed later with a misleading `400 FLOW_FAILED` "Unknown variable", or succeeded doing the wrong thing. An array was spread into an index-keyed bag (`{ "0": 1, "1": 2 }`). A falsy value (`null`, `0`, `false`, `""`) was read as an absent `params`, so the body's other top-level keys were forwarded as params instead.
8+
9+
These doors now answer `400 VALIDATION_FAILED` with one `fields[]` entry, `{ field: 'params', code: 'invalid_type' }`, whose message names the type received. The refusal comes before the flow is dispatched, so no run starts and no run row is written. It is the refusal the resume door already gives a non-object `inputs`, `variables` or `output`, and both doors now ask the question through one check, so their wording is the same.
10+
11+
Unchanged: an object `params` (an empty one included) builds the same bag as before, a body with no `params` key still forwards its other top-level keys as params, and an empty body still runs with an empty bag. An unknown flow still answers `404`, and a caller the flow refuses still answers `403`, before the body is judged.
12+
13+
Clause-②: no (narrowing) — the trigger doors refuse request shapes they used to dispatch: a `params` that is a string, number, boolean, array or `null`. Nothing is widened.
14+
15+
Metadata-protocol: none (an HTTP request body's handling at the trigger doors; nothing a metadata author writes changes)
16+
17+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition, stored row or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is how the flow trigger doors handle a request body whose `params` is not a JSON object: it is refused instead of dropped. The remedy is a request edit (send `params` as an object, or leave it out), never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
18+
19+
**BREAKING** for a caller that sent `params` as something other than an object and relied on the flow running anyway, shipped as `patch` because the metadata protocol does not move.
20+
21+
- **Before.** A non-object `params` was dropped (or, for `null` and the other falsy values, replaced by the body's other keys), and the flow was dispatched.
22+
- **Now.** The request is refused with `400 VALIDATION_FAILED` naming `params`, and nothing runs.
23+
- **Fix.** Send `params` as an object of flow input names to values, for example `{"params":{"ownerId":"u1"}}`, or leave the key out.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): a grouped grid orders a level's groups by its `grouping.fields[].orderBy`, the label still from `field`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A view grouped by `period_label` with `orderBy: 'period_start', order: 'desc'` now lists one header per label in descending `period_start` order, where it used to list the labels as text (`Nov 2026` before `Oct 2026` before `Q1 2027`).
11+
12+
- **Server-grouped grid.** The grid forwards the level's `orderBy` into the group header query, so the query carries the `min` (asc) / `max` (desc) column the spec compiles for it, and sorts the header rows by the column `listViewGroupOrderColumn` names. An `orderBy` field the user may not read is not forwarded: that level orders by its own key.
13+
- **Rows handed in whole.** Each group's order value is the `min` (asc) or `max` (desc) of `orderBy` over its rows, so a group sits where its first row would if the rows were sorted by `orderBy`.
14+
- **Both paths.** A group with no `orderBy` value sorts last in both directions; equal values fall back to the existing key comparison (declared option order, then label) in the same direction. A level without `orderBy`, or with `orderBy` naming the grouping field itself, orders exactly as before.

0 commit comments

Comments
 (0)