Skip to content

Commit d6bbeae

Browse files
committed
Merge origin/main into claude/project-thread-g33c7k for a conflict
The merge queue removed the PR on a conflict. The one conflict is a count: the metadata-forms positive control in object-lifecycle-panel-echo- decisions.test.ts (686 on main after #26548, 688 here with this branch's two registry types). Both comments are kept, main's first. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
2 parents f35f411 + 1685a06 commit d6bbeae

119 files changed

Lines changed: 5185 additions & 555 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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: inside one bulk create, a child rule that reads a parent summary counts the earlier rows of the same batch its rules admitted, so a batch is judged as its rows would be one by one (#25579)
6+
7+
Clause-②: no (narrowing) — a bulk create (`insert(array)`, `insertMany`, and the doors on them: `createManyData`, `insertManyData`, the import's batched creates) refuses a row whose rule reads a parent `summary` that the earlier admitted rows of the same batch move past the rule's limit, which it admitted before. Nothing is widened.
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 or type changes: an existing validation rule is now enforced on the rows of one bulk create the way it is already enforced one write at a time. The refusal is the rule's own VALIDATION_FAILED, worded by its author, so there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the bulk insert (`@objectstack/objectql`), served by every bulk create door.
13+
14+
- **Before.** A bulk insert read the related rows its rules read through a reference once, before any row of the batch was written, and recomputed parent summaries once, after. So "each supplier has at most one primary contact" (a `count` summary filtered on `{ is_primary: true }`, and the child rule `record.is_primary == true && record.supplier.primary_contact_count > 0`) refused a second primary contact written on its own, but admitted two primary contacts for one supplier in one import: both rows read `0`.
15+
- **Now.** Each row's rules read the parent summary as it stands after the earlier rows of the same batch that the rules admitted. The batch's admitted rows are accumulated in memory per summary and per parent; ⛔ nothing is re-read from the database per row. In the card's import, one contact is created and the other row reports the rule's error. A multi-row insert preview (`validate` with an array, mode `insert`) answers the same way.
16+
- **Unchanged.** Which rows land and how a refusal is reported stay each door's own: `insertMany` / `insertManyData` (the import) report the refused row and write the rest; `insert(array)` / `createManyData` refuse the whole batch. A row the rules refuse is not counted. A single write reads the stored summary as before. The summary written after the batch is the same recompute as before.
17+
- **Which summaries accumulate.** `count`, and `sum` / `min` / `max` over number values, with the summary's `filter` evaluated on the new row. An `avg` summary, a `sum` / `min` / `max` that meets a value that is not a number, and a summary whose `filter` reads a dotted path through another reference keep reading the stored value, as before.
18+
- **Not covered.** Concurrent writes: two writes in flight at the same time each read the summary they started from (批量等于逐条,并发不保证).
19+
- **Who sees it.** An app whose child rule reads a parent summary and that bulk-creates children: a batch that broke the rule was admitted whole before, and now its offending rows are refused.
20+
21+
**The remedy.** None needed for a rule that meant what it says. A batch that must land whole has to satisfy the rule row by row, in order.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/platform-objects': patch
5+
---
6+
7+
feat(spec): a report declares the period it covers (`dateRange`) and the export of its table (`exportOptions`)
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: added
11+
12+
`ReportSchema` gains two optional top-level keys. A report that declares neither parses and renders exactly as before.
13+
14+
- **`dateRange: { dimension?, defaultRange, allowCustomRange }`.** A date window over one dimension of the report's dataset. `defaultRange` and `allowCustomRange` are the dashboard's own declaration, verbatim: the same preset table (`DATE_RANGE_DEFAULT_RANGES`), the same defaults (`this_month`, `true`) and the same rename hints. `dimension` names a dataset **dimension** that declares `type: 'date'`, not a raw object field; omitted, the dataset's one `type: 'date'` dimension is used. The window is a filter, not a bucket. The renderer sends it as `selection.timeDimensions: [{ dimension, dateRange }]` with no `granularity`, which the analytics service already accepts. `dateFilter` / `timeRange` are renamed onto `dateRange`, the dashboard's own spellings.
15+
- **`exportOptions: { formats?, includeHeaders?, fileNamePrefix? }`.** Three members of the list export block, under its key name. `formats` is `csv` / `xlsx` only (`json` and `pdf` are refused with a report-side message). Export is on by default (both formats) and `formats: []` turns it off. `maxRecords` and `streaming` are list-only and are refused with that reason. There is no report permission key: the control is gated on the dataset base object's `ObjectPermission.allowExport`, the axis the list toolbar uses. `allowExport` written on a report is answered with that pointer.
16+
- **Refused on a `joined` report**, on the container and on a block. The blocks may bind different datasets, and the report draws one table per block.
17+
- **No subscription.** `subscription` / `schedule` on a report are answered: there is no scheduled delivery of a report.
18+
19+
`@objectstack/lint` adds the dataset-aware half as a reference-integrity rule, `validateReportDateRange`. It runs on `os validate`, `os lint` and `os build`, and at the report publish door. Four `error` verdicts, each with an `os explain` entry: `report-date-range-dimension-unknown`, `report-date-range-dimension-not-date`, `report-date-range-dimension-ambiguous` (dimension omitted, two or more date dimensions) and `report-date-range-dimension-none` (dimension omitted, no date dimension). The liveness author-warning lint now walks `reports`, and both keys warn (`liveness-planned-property`) until the Console report page reads them. The report editor form gains a collapsed "Period & export" section, translated in `@objectstack/platform-objects` for zh-CN, ja-JP and es-ES.
20+
21+
Nothing renders either key yet. The Console report page's period chip and export control are the renderer's half, and until it lands an author declaring either key gets a planned-property warning that says so.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/console': patch
4+
---
5+
6+
fix(app-shell): the approval step line draws the route the request's run took, not the flow graph's first branch
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The approval step line used to draw the request's `flow_steps`. The approvals read builds that list by walking the flow from its start and taking the `approve` edge, else the `true` edge, else the first edge at every node. So a request that a decision routed down another branch showed rungs it never reaches. A contract routed from the manager to the finance-only rung read "manager · legal + finance (joint) · executive · general manager" while it waited on the finance controller.
12+
13+
What changes:
14+
15+
- On a business record's approvals panel, the line now shows the steps the pending request's flow run has passed, oldest first, then the pending step. Each step comes from a request the run actually opened: same flow run, another node, approved, and opened before the pending one. A step approved in several rounds is shown once. Nothing ahead of the pending step is shown, because no request row says which branch the run takes next.
16+
- The approvals list serves a request only to its participants, so a step the viewer had no part in is left out rather than guessed. With fewer than two steps, no line is drawn.
17+
- On the approval request page, the decision panel no longer draws a step line. Its row proves only its own step, which the page header already names. The line returns there once the approvals read serves the route the run takes.
18+
19+
What to do: nothing. Nothing a flow, page or object declares changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): flow screen date, date-time and boolean fields use the platform controls
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A screen flow's input screen was the last form surface still drawing the browser's native date input, so a `date` field showed the machine's format (`mm/dd/yyyy` under a Chinese console), a `datetime` field was a bare text box, and a `boolean` field was a lone checkbox on the line under its label, which read like an empty radio button.
11+
12+
What changes:
13+
14+
- A `date` or `datetime` screen field is now the record form's date picker: a text box in the display locale's format, a calendar popover, a clear button and the today / tomorrow shortcuts. The value it hands the flow is the stored shape: `YYYY-MM-DD` for a date and an ISO instant for a date-time, and `''` when cleared, as the native date input wrote.
15+
- A `boolean` (or `checkbox`) screen field is now the inline row the action-parameter dialog draws: the checkbox with its label beside it, and the help text under the row.
16+
17+
What to do: nothing. No flow metadata changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/core': patch
3+
---
4+
5+
fix(core): a declared action's confirm dialog is titled with the action's label, and a danger action's confirm button is destructive
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Before, clicking an action that declares `confirmText` always opened a dialog titled 「确认操作」 / "Confirm Action" with a neutral 「继续」 / "Continue" button: the action's own label and its `variant: 'danger'` never reached the dialog. `ActionRunner` now hands its `ConfirmationHandler` the existing `options` bag:
11+
12+
- `title` and `confirmText` are the action's `label`, already localized by the surface that dispatches it.
13+
- `destructive: true` when the action declares `variant: 'danger'`, which paints the console's confirm button in the destructive style, as the record Delete already does.
14+
- The cancel button keeps the host's own copy. An action with neither a label nor the danger variant is asked with the message alone, as before.
15+
16+
What to do: nothing. No key is added; the action already declares its label and variant. A custom `ConfirmationHandler` that ignores its second argument behaves as before.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-print': patch
3+
---
4+
5+
feat(service-print): a record prints on any print page of its object, not only on the default record page
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
`printRecordToFile` (and `POST /api/v1/print/records`) used to refuse every page that was not the object's default record page (`VALIDATION_ERROR` / 400), so an app that printed an object had to turn that object's everyday record page into the printed document. A record now prints on any record page of its object that declares `print`:
11+
12+
- **The default record page** (`isDefault: true`) renders on the console's record route, `CONSOLE/apps/APP/OBJECT/record/ID`, as before.
13+
- **Any other print page** renders on the console's named record-page route, `CONSOLE/apps/APP/OBJECT/record/ID/page/PAGE`, which draws that page and never falls back to the default one. An object keeps its working record page and adds its documents (an invoice, a quote, a delivery note) beside it.
14+
15+
Two refusals remain, both `VALIDATION_ERROR` / 400 before anything is minted or rendered: the page is not a record page of the object, and the page declares no `print`. "A record page of the object" is the console's own reading (`type: 'record'` and that `object`), so the server prints exactly the pages the console's named route draws. An unknown page name is still `RESOURCE_NOT_FOUND` / 404. No metadata key is added or changed: the request already names its page.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/console': patch
4+
---
5+
6+
fix(console,i18n): the OAuth consent screen explains the `metadata:write` scope and never shows an unknown scope as its bare spelling
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
- **`@object-ui/console`.** The consent screen at `/oauth/consent` names every MCP scope in a sentence. The sentences come from `@objectstack/spec`'s `MCP_OAUTH_SCOPE_CONSENT_COPY`, the copy the spec declares beside each scope, through a switch that is exhaustive over `McpOauthScope`: a scope added to the spec without a case here fails to compile. `metadata:write` used to appear as that raw string beside three plain sentences. It now reads "Build and change this app's metadata as drafts, within your own permissions — a person publishes them". A scope the page cannot name renders as "Unrecognised permission: SCOPE" instead of the bare scope string, which could read as a description of the grant.
12+
- **`@object-ui/i18n`.** Every locale pack gains `oauth.consent.scope.metadataWrite` and `oauth.consent.scope.unrecognised` (the latter takes a `{{scope}}` placeholder). `TranslationKeys` widens by these two keys.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/lint': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(lint): `security-controlled-by-parent-owner-policy-without-opt-in` prescribes `ownerMayEdit` only for a policy keyed on `owner_id` (#26591)
7+
8+
Clause-②: no (lint advice is not a contract face; no accepted metadata shape moves)
9+
Metadata-protocol: none
10+
11+
`sharing.controlledByParent.ownerMayEdit: true` admits one caller past a `controlled_by_parent` detail's master gate: the user in the stored row's `owner_id`, on a detail that carries that column. The lint arm used to prescribe the opt-in for any `update` / `all` policy of the form `FIELD == current_user.id`. So an app whose accountable person is its own user field (`owner == current_user.id`) declared the opt-in, lint went quiet, and that user was still refused with `403 requires edit access to its master record`.
12+
13+
- The opt-in is still prescribed, as before, for a policy keyed on `owner_id` on a detail that carries it and has not opted in, and the arm stays silent once it has.
14+
- A policy keyed only on another user field is now flagged whether or not the detail opted in. The verdict says the opt-in admits only the stored `owner_id`. The hint never names the opt-in: it says to key the accountable person on `owner_id`, or drop the policy.
15+
- On a detail with no `owner_id` (`ownership: 'business_unit' | 'org' | 'none'` and no `owner_id` field of its own), the opt-in names nobody. A policy keyed on `owner_id` there gets the same treatment, and the hint says to give the detail an `owner_id` column.
16+
- The runtime gate is unchanged. The rule's `os explain` entry covers both cases. The `ownerMayEdit` evidence line in `@objectstack/spec`'s liveness ledger now describes the narrowed arm.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: the filter save and validate doors refuse an `addDays` offset whose `{ $field }` is a dotted path, in the words SQL push-down already refuses it with on every read
6+
7+
An `addDays` offset reads its days from a column of the same record: `{ $field: 'due_on', addDays: { $field: 'grace_days' } }`, or `{ $now: true, addDays: { $field: 'review_sla_days' } }`. A dotted offset such as `addDays: { $field: 'contract_type.review_sla_days' }` passed `FieldReferenceSchema`, `NowAnchorSchema`, `FilterConditionSchema`, the dataset `filter` and measure `filter`, and `os validate`, and then failed every SQL read with `INVALID_FILTER` / 400. The one executor that walks the path, the memory evaluator, reads a lookup's id there on a stored row and counts the offset as zero days. Now every authoring door refuses it, naming the path: `the addDays offset "contract_type.review_sla_days" is a dotted path, and SQL push-down compiles same-table column references only (no relation traversal, no alias-qualified columns).`
8+
9+
This retires the earlier reading that the dotted offset validates and is answered in memory (the #14104 ruling-record correction 5550346132, and the spec pin that accepted `addDays: { $field: 'duty.grace_days' }`). Ruling A on #25808 bounds the offset to a same-record column.
10+
11+
Clause-②: no (narrowing) — the save and validate doors refuse a dotted `addDays` `$field` that every SQL read already refused. Nothing is widened.
12+
13+
Metadata-protocol: narrowed (a filter, a dataset filter or a dataset measure filter may no longer carry an `addDays` offset whose `$field` is a dotted path)
14+
15+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed. The dotted offset never compiled on SQL push-down and, on a stored row, read as zero days in memory, so there is no mechanical rewrite that keeps what the author meant: the related value has to be copied onto a numeric column of the row, which is an application change the author makes. -->
16+
17+
**BREAKING** accept-set narrowing on the `addDays` offset (`@objectstack/spec`).
18+
19+
- **Before.** `FieldReferenceSchema`, `NowAnchorSchema`, the ordering slots of `FieldOperatorsSchema`, `FilterConditionSchema` (data filters, dataset `filter`, dataset measure `filter`) and `os validate` accepted `addDays: { $field: 'a.b' }`. SQL reads answered `INVALID_FILTER` / 400; a dataset measure filter answered it with the reason withheld.
20+
- **Now.** Each of those doors refuses it. `parseFilterAST` and the engine's filter doors (`where`, `aggregations[i].filter`, `having`) refuse it with `INVALID_FILTER` / 400 and the path named, so a dataset measure filter that never met the save door now gets the reason too.
21+
- **Unchanged.** A same-record offset column, a literal offset, and the reference's own `$field` (a dotted base reference keeps its asymmetry: resolved in memory, refused by SQL push-down).
22+
- **Who sees it.** An author who read the offset through a relation. Every SQL deployment already refused that filter on read.
23+
24+
**The remedy.** Copy the related value onto a numeric column of the record (on create and whenever the relation changes) and name that column: `addDays: { $field: 'review_sla_days' }`.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/auth': patch
3+
---
4+
5+
fix(auth): sign-out follows the provider-logout redirect that better-auth answers, so signing out of a hosted environment also ends the cloud session
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
On an environment signed in through an identity provider (ObjectOS Cloud, or any OIDC provider wired through `oidcProviders`), 退出登录 ended only the environment session. better-auth's `POST /sign-out` already answers `{ success: true, url, redirect: true }` for such a user, with `url` pointing at the provider's end-session endpoint, but the Console ignored it. The provider session survived, and 「使用 ObjectOS Cloud 继续」 signed the next person on a shared browser straight back in as the user who had just signed out, with no password.
11+
12+
- `createAuthClient().signOut()` now sends `callbackURL` = the SPA's own sign-in page (`<basename>/login`, so `/_console/login` on a hosted environment). better-auth passes it to the provider as `post_logout_redirect_uri`.
13+
- When the answer carries `redirect: true` with an absolute `http:` or `https:` URL, the client clears the bearer token and then sends the window there with `window.location.assign`. Any other URL is ignored.
14+
- `AuthProvider.signOut()` still resets its state and purges the tab's cached metadata before the page unloads. The header's 退出登录, the user menu and the remediation overlay's sign-out link all go through it.
15+
16+
What to do: nothing. A deployment without a provider gets `{ success: true }` from better-auth, and sign-out keeps its in-app route to the sign-in page. To end the provider session as well, register the environment's sign-in page (`<origin>/_console/login`) as a post-logout redirect URI with the provider.

0 commit comments

Comments
 (0)