Skip to content

Commit 1519ed6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20914-release-sum-row
2 parents a949ee8 + b3d7a70 commit 1519ed6

43 files changed

Lines changed: 4297 additions & 206 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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
---
4+
5+
fix(plugin-audit)!: an engine read of `sys_activity` returns only the rows whose parent record the caller can read, the same way an engine read of `sys_comment` is narrowed
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a narrowing of what a READ returns, on one platform object, decided at request time: an activity row is served to a caller exactly when the record it names (object_name, record_id) is one that caller can read. No authorable key, spelling, value domain, export or stored metadata shape moves: the sys_activity object definition is byte-identical, every query shape parses as before, the package barrel exports nothing new and nothing less, and no stored row is rewritten. There is therefore nothing for an author to convert and nothing for `objectstack migrate meta` to reach. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a read-visibility rule and this diff adds none (not registered / already-registered); and the change is runtime behaviour, not a published TypeScript declaration (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING**: this narrows what an engine read of `sys_activity` returns to a caller that is not system context. It ships as `minor` under the repo's launch-window convention for narrowings.
12+
13+
**What changes.** `AuditPlugin` now mounts a read gate on `sys_activity`, beside the one `sys_comment` already has. It is an engine middleware, so it narrows what passes through the engine: `find`, `findOne`, `count` and `aggregate`, which on the generic data doors are the list, its `total`, the by-id read and both query shapes. There a row is returned only when the caller can read the record it is about. That answer is the one the comment gate asks: the caller's own engine read of the parent record, so the parent object's sharing, RLS and object-level permissions decide. Parent reads are batched, one per parent object.
14+
15+
**Rows that are left out**, failing closed exactly as the comment gate does for its threads:
16+
17+
- a row about a record the caller cannot read;
18+
- a row about a record that no longer exists;
19+
- a row that names no record, or names an object the engine does not know;
20+
- a row that names `sys_activity` itself.
21+
22+
**Unchanged.** A caller who can read every record (an admin) keeps every row about a record that exists. System-context reads, including the audit writer's own, are not narrowed. The object, its fields and what the CRUD mirror writes are unchanged, and nothing stored is rewritten.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/core': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
fix(core,objectql)!: a relative-date placeholder that resolves outside its field's years is refused `INVALID_FILTER` / 400, naming the placeholder and the year it resolved to, instead of reaching the driver and answering the wrong rows
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) a refusal of a VALUE at the engine's filter-resolution stage: a relative-date placeholder (a date macro such as {8000_years_from_now}) whose resolved day or instant falls outside its column's years, refused INVALID_FILTER / 400 on where, a per-aggregation filter and having. No authorable key, spelling, export or stored metadata shape moves: every filter, view, dataset and query shape parses as before, the date-macro vocabulary is unchanged, and @objectstack/core and @objectstack/objectql export nothing new and nothing less (resolveFilterToken and resolveFilterTokens keep their signatures). The resolver's spelling of a day outside 0001..9999 changes, and that day was never a value any reader read as the day it names. The other categories are closed on facts: both packages publish (not unpublished); no ADR-0087 id covers a value range or a resolved value and this diff adds none (not registered / already-registered); and the change is runtime behaviour, not a declaration (not runtime-interface-only / type-surface-only). -->
11+
12+
**BREAKING**: this narrows what the engine answers for a filter carrying a relative-date placeholder. A date macro is resolved after the temporal-comparand door, which steps around a placeholder, so the year range that door asks of a literal never saw the value one resolved to. It does now, through the same function, core's `isOutsideTemporalYearRange`, by the column's kind: a `date` takes the years 0001 to 9999 and a `datetime` 1000 to 9999. It ships as `minor` under the launch-window convention for accept-set narrowings. No export or published type changes.
13+
14+
**What is refused now.** A date macro whose resolved value falls outside its column's years, on a declared `date` or `datetime` field (or, on a `time` field, one that resolves to an instant whose UTC year has no four-digit spelling), at `where` (on `find`, `findOne`, `count`, `aggregate`, a multi-row `update` and `delete`), at a per-aggregation `filter`, at `having` (by the aggregated column's kind), and through `judgeFilter`. On REST that is `POST /api/v1/data/:object/query` and every other door that reads through the engine. Measured before this on InMemoryDriver and SqlDriver on SQLite, over a `datetime` field with a row in 2026 and a row in 1500:
15+
16+
- `$gt {8000_years_from_now}` answered both rows, and the right answer was none;
17+
- `$lt {2027_years_ago}` answered the 1500 row, because the resolver spelled year -1 as `-1-10-01` and that text was read as a day in 2001, and the right answer was none;
18+
- `$lt {1977_years_ago}` resolved to year 49, below the `datetime` floor of 1000, which now applies to a resolved placeholder as it does to a literal;
19+
- on a `time` field, `$gt {8000_years_from_now}` answered every row: the `time` rule keeps no time of day from an instant whose UTC year has no four-digit spelling, so it compared as text. Such a placeholder is refused now in the words a literal of that instant gets.
20+
21+
**What an author sees.** The refusal names the field, the placeholder as written, its position, the value it resolved to and that value's year, in the temporal-comparand door's words for the year class: `filter on 'opened_at' compares a declared datetime field against "{8000_years_from_now}" at where.opened_at.$gt, a relative-date placeholder that resolved to "+010026-10-01" (the year 10026), an instant whose UTC year falls outside the years 1000 to 9999 …`. It ends by asking for a placeholder whose offset lands inside those years.
22+
23+
**The resolver's spelling** (`@objectstack/core`). A date macro that lands on a day outside 0001..9999 now resolves to that day in the expanded-year form of ECMAScript's date time string format, `+010026-10-01` or `-000001-10-01` (year 0 is `0000-10-01`). It used to take the storage rule's unpadded spelling, `10026-10-01` or `-1-10-01`, which `Date.parse` reads through the host's legacy parser in the host's zone, so a day in year -1 read as one in 2001 and could not be judged. Every consumer of `resolveFilterToken` and `resolveFilterTokens` sees the new spelling for such a day only. A day inside 0001..9999 and a sub-day placeholder's instant are spelled as before.
24+
25+
**Unchanged.** A placeholder that resolves inside its column's years answers as before; a `date` keeps the years 0001 to 0999, which a `datetime` refuses, and a `time` field reads the time of day of any instant with a four-digit year, year 0 included. A placeholder on a column with no temporal kind (text, number) and a context placeholder such as `{current_user_id}` are not judged by this range. Every literal comparand answers as before.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: a dashboard widget with no dimension declares two or more measures only on a type that renders them — `pie` / `donut` / `funnel` / `scatter` / `radar` / `treemap` / `sankey` are refused at `values` (#20958; objectui#8894 ruling D's principle)
6+
7+
Clause-②: yes (narrowing) — the accept set NARROWS (that is the change), and the published surface GAINS two exports: the one constant the rule reads, `DASHBOARD_WIDGET_MULTI_MEASURE_TYPES`, and the check itself, `checkDashboardWidgetDimensionlessMeasureArity`, exported so objectui's `.shape` mirror can chain it.
8+
9+
<!-- adr-0087: registered dashboard-widget-dimensionless-multi-measure-refused -->
10+
11+
**BREAKING** accept-set narrowing at `dashboard.widgets[].values`, shipped as
12+
`minor` under this repo's launch-window convention for breaking changes
13+
(`check-changeset-no-major` refuses `major` while the window is open, so
14+
breaking-ness is carried by this banner and by the ADR-0087 disposition above,
15+
never by the bump level). The prescription is registered under protocol major 18
16+
as `dashboard-widget-dimensionless-multi-measure-refused`.
17+
18+
**What was wrong.** Outside the metric family, `DashboardWidgetSchema.values`
19+
(`z.array(z.string()).min(1)`) had no upper bound. Measured on this tree before
20+
the change: `{ type: 'pie', dataset: 'sales', values: ['a', 'b'] }` with no
21+
`dimensions` parsed through `DashboardWidgetSchema`, and so did `donut`,
22+
`funnel`, `scatter`, `radar`, `treemap` and `sankey` — while `bogusProp` on the
23+
same widget was refused by name, the lit control. After it, the same body is
24+
refused at `defineStack`, at `os validate` (which loads through `defineStack`),
25+
and on the metadata save path (`422 INVALID_METADATA`, active and draft). With
26+
nothing to split by, those seven types draw `values[0]`: every measure after it
27+
is queried and dropped on the floor by the renderer. The maintainer's ruling D
28+
(「协议不正确的应该先修改协议」) fixes the protocol where it admits measures a
29+
widget type cannot render; the metric-family narrowing was its first
30+
application, and this is the same principle on the chart types.
31+
32+
### Write instead
33+
34+
| wrote | write instead |
35+
|---|---|
36+
| `{ id: 'mix', type: 'pie', dataset: 'sales', values: ['amount_sum', 'count'] }` (no `dimensions`) | `{ id: 'mix', type: 'table', dataset: 'sales', values: ['amount_sum', 'count'] }` — a row of measures |
37+
| the same, wanting a chart | `type: 'bar'` (or `column` / `horizontal-bar`) — one bar per measure |
38+
| the same, wanting the pie | `{ id: 'mix', type: 'pie', …, values: ['amount_sum'] }` **and** `{ id: 'mix_count', type: 'pie', …, values: ['count'] }` — one widget per measure, each with its own `id` (and `layout`, if you pin positions) |
39+
40+
No conversion does this for you: whether a dimensionless two-measure pie meant a
41+
table, a bar chart or two pies is an authoring choice. The refusal lands at
42+
`widgets[N].values` as ONE `custom` issue naming the widget's `id`, the number
43+
of measures and the authored `type`, and it lists the types that do render
44+
several measures on a dimensionless widget, read from
45+
`DASHBOARD_WIDGET_MULTI_MEASURE_TYPES` (`table`, `pivot`, `bar`, `column`,
46+
`horizontal-bar`, `line`, `area`, `combo`). That constant is the one list — the
47+
check, the refusal text and the `values` doc string read it, and objectui's
48+
mirror is to import it rather than restate it. A type that later gains a
49+
declared multi-measure rendering joins it with no migration.
50+
51+
**Nothing else moves.** The seven types WITH a dimension, and with one measure,
52+
parse exactly as before; every type in the multi-measure set keeps accepting
53+
any number of measures with no dimension; the metric family's refusal is
54+
unchanged and still ONE issue (this check steps aside for `metric` / `kpi` /
55+
`gauge` / `solid-gauge` / `bullet` and for a typeless widget, which resolves to
56+
`metric`); an empty `values` keeps its `too_small`; a `type` outside
57+
`ChartTypeSchema` reports the type refusal alone. Census at the branch point
58+
(`05be35259`), every tracked `.ts` / `.tsx` / `.js` / `.json` / `.md` / `.mdx`:
59+
23 widget literals on the seven types, every one with one dimension and one
60+
measure, and 0 dimensionless multi-measure widgets on them; the same scan over
61+
an objectui checkout (`1263e40`) reads 0 as well.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
fix(plugin-security)!: a field whose masking rule applies is served masked to a caller who resolves no permission set, and that caller may not filter, sort, group or aggregate on it (#20995)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) Nothing an author wrote is rewritten or changes meaning: every maskingRule already declared that it applies to every non-system caller who does not hold all of the field's requiredPermissions, and this change makes the runtime honour that declaration for the one caller class it skipped. No stored metadata, schema key or published type moves. -->
10+
11+
**BREAKING for callers who resolve no permission set.**
12+
13+
**What changed.** A field that declares `maskingRule` is masked for every
14+
non-system caller unless the caller holds all of the field's
15+
`requiredPermissions`. A caller who carries a principal but resolves no
16+
permission set holds no capability, so the rule applies to it, but the runtime
17+
served that caller the stored value and let it filter, sort, group and
18+
aggregate on the field. That caller is now served the masked value. A filter,
19+
sort key, group key or aggregate that names the field is refused with
20+
`403 PERMISSION_DENIED`, as it already was for any other masked caller. A write
21+
that sends the masked placeholder back is refused with `400 VALIDATION_ERROR`, so
22+
a client that saves the record it was served cannot overwrite the stored value
23+
with its mask.
24+
25+
The published field answers agree with what is served.
26+
`ISecurityService.getQueryableFields` no longer lists such a field for this
27+
caller, so a door that compiles its own query refuses it the same way.
28+
`getReadableFields` still lists it, because a masked field is a served column.
29+
30+
**Who this reaches.** A caller who resolves no permission set but carries a
31+
position, a named permission set or a user id. A caller with none of the three
32+
is handed through untouched, as before, and the field projections say so. A
33+
system context is unaffected.
34+
35+
**One more refusal, by the same rule.** If the object's security posture cannot
36+
be read, this caller's request is now refused, as every other caller's already
37+
is. The masking rules come from that posture, so they cannot be known without
38+
it.
39+
40+
**What to do.** Nothing, unless such a caller needs the stored value. A field's
41+
`requiredPermissions` are the gate that lifts its mask, so give the caller a
42+
permission set that holds all of them, or drop the `maskingRule`. A query that
43+
must sort or search on the field needs the same.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `ObjectSchema.fields` refuses `constructor` and `prototype` at the offending key (`fields.constructor`, `fields.prototype`), not at the `fields` slot
6+
7+
Clause-②: no
8+
9+
A field map carrying a key named `constructor` or `prototype` is refused, as before. The
10+
refusal used to be reported at the path `fields`, which names no field, so a form reading the
11+
structured issues of a refused save could not point at the field that caused it. It is now
12+
reported at that key, like the `__proto__` refusal (`fields.__proto__`) and the key grammar's
13+
`invalid_key` refusal (`fields.Bad Name`) already are. A document carrying both names gets two
14+
issues, one at each key, where it used to get one at the slot.
15+
16+
Nothing else moves. The same keys are refused and the same documents are accepted. The issue
17+
code (`custom`) and the message are unchanged. The published JSON Schema states the same ban:
18+
it is now written as one `propertyNames` clause per name inside `allOf` instead of one clause
19+
naming both, which accepts and refuses exactly the same documents.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): the layered read of a flow name a managed package ships reports the package's flow as the effective layer, as the by-name read and the flow list do (#21002)
6+
7+
Clause-②: no
8+
9+
`flow` is in ADR-0126's Regime C: a managed package's flow is sealed, and there is no overlay read path for it. The flow list, `GET /api/v1/meta/flow`, and the by-name read, `GET /api/v1/meta/flow/:name`, already serve the package's flow for a name a managed package ships (#20913, #20946). The layered read, `GET /api/v1/meta/flow/:name/layers`, did not: for such a name it reported a stored flow of that name as the effective layer, while its lock and provenance flags named the package. So the layered read and the other two read doors answered two different flows for one name.
10+
11+
The layered read now decides the effective layer with the same check the other two doors use. For a name a managed package ships, the effective layer is the package's flow, with or without a package scope, whatever the stored flow's own package binding or markings say. The stored flow is still reported, as a separate layer of its own scope that does not take effect. The deprecated layers flag on the by-name read answers the same.
12+
13+
The stored flow is not deleted, rewritten or refused. Flow names no managed package ships, organization-scoped rows and every other metadata type are read as before.
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+
fix(spec): an object's embedded `listViews` are served in the reader's language, from the `_views` keys `os i18n extract` already writes
6+
7+
Clause-②: no
8+
9+
- `GET /api/v1/meta/object` and `GET /api/v1/meta/object/:name` now translate each view in an object's `listViews`: its `label`, its `description`, and the copy of its `bulkActionDefs` (label, confirm prompt, confirm button, and each param's label, help and placeholder). They read the keys `os i18n extract` writes for those views, `objects.<object>._views.<view>.*`, where `<view>` is the view's key under `listViews`. For example, `sys_account`'s `mine` view is now served as `我的链接` to a `zh-CN` reader. It used to be served as the authored `My Links`.
10+
- A view with no translation for the requested locale keeps its authored text. An object with no `_views` entries is served unchanged.
11+
- A string that was changed after the package shipped still wins over the packaged translation, the same rule a served view document and a dashboard follow. The string is compared with the same view in the packaged object. If it differs, the changed string is served in every locale. A view the packaged object does not declare keeps all of its own strings.
12+
- `translateObject` (and `translateMetadataDocument('object', …)`) in `@objectstack/spec/system` does the translating, so any caller of those functions gets the same result.

0 commit comments

Comments
 (0)