Skip to content

Commit eff5fc6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20321-rls-tags-retire
# Conflicts: # packages/spec/src/conversions/registry.ts # packages/spec/src/migrations/registry.ts
2 parents da5d401 + eee0974 commit eff5fc6

49 files changed

Lines changed: 2654 additions & 601 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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
A sharing rule whose `condition` compares a field with a `json` or `multiple` field is refused when it is authored, at `os validate` / `os build` / `os lint`, instead of being seeded and then granting nothing (#19886).
6+
7+
**BREAKING** — an accept-set narrowing, shipped by `@objectstack/lint` as `minor` under the repo's launch-window convention for accept-set narrowings. The hand-migration prescription is already registered under protocol major 18 as `cel-predicate-one-value-comparand-refused`, whose surface names `sharingRules[].condition` and this class.
8+
9+
Clause-②: no (narrowing)
10+
11+
`record.status != record.tags`, with `tags` a `json` field or a `multiple` lookup, lowers to a legal filter shape, because the CEL lowering sees the condition's text and not the object's field types, so the seeder seeds the rule. Measured before this change: 72 conditions (`==`, `!=`, `!(==)`, `>`, `>=`, `<`, `<=`; a `json`, `address`, `multiselect`, `multiple` lookup and `multiple` user field; both operand orders; plus a list-against-list and a compound spelling) were all accepted by the real `os validate`. The runtime refused every one of them, measured through the real plugin-sharing on driver-sql and driver-sqlite-wasm: the rule was seeded into `sys_sharing_rule`, every criteria query it ran answered `INVALID_FILTER` / 400, `SharingRuleService` read that as matching no record, and no `sys_record_share` grant was written, at boot or on a later insert or update. The recipient read nothing. The only signal was one WARN line per rule in the server log.
12+
13+
What changes:
14+
15+
- `@objectstack/lint`: `validateSharingRuleEnforceability` reports `sharing-rule-unlowerable-condition` for a condition that lowers but compares two fields (`==`, `!=`, `>`, `>=`, `<`, `<=`, on either side, under `!` too) where either column is DECLARED to hold a list or an object. It uses the same classification as the row-level-security rule's arm for this class (`listHoldingComparisons`, now exported from `validate-rls-predicate-enforceability.ts`), which reads the spec's value-shape classes, the same two driver-sql refuses such a comparison by: a structured JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`), or a multi-valued field (`multiselect`, `checkboxes`, `tags`, or `select` / `radio` / `lookup` / `user` / `file` / `image` with `multiple: true`). The finding names each comparison and the declaration behind it, and states the run-time consequence. It keeps the unlowerable id because the fix is the same rewrite of the condition, and the literal spelling of the same class (`record.status == ['a', 'b']`) is already reported under that id.
16+
- Inactive rules are judged too, as the rule already does for every condition: the seeder seeds them regardless of `active`.
17+
18+
Not changed: a field compared with a single-valued field (`record.status != record.owner_name`, `record.amount > record.budget`), a `json` or `multiple` field compared with a literal or tested against `null`, and any column the stack does not declare (an anchor object from another package, an object with no field map, an undeclared name), which the rule does not judge. No row-level-security verdict changes. The rule still runs only at the CLI doors; the metadata save door for a `sharing_rule` does not run it, as before.
19+
20+
No shipped condition moves: the 3 declared sharing-rule conditions in this repository's packages and examples compare a field with a literal, the cloud repository declares none, and the real `os validate` over `app-crm`, `app-multi-package`, `app-showcase` and `app-todo` reports no new `sharing-rule-*` finding.
21+
22+
**What to change.** A field compared with a `json` or `multiple` field has no row-filter form: compare with a single-valued column, or with a literal ("one of these values" is `record.status in ['open', 'pending']`), or keep the value the rule keys on in a single-valued field and compare with that.
23+
24+
<!-- adr-0087: not-required (already-registered cel-predicate-one-value-comparand-refused) The entry's surface already names sharingRules[].condition and this exact class ("a field compared with another field (==, !=, or an ordering operator) where either column holds a list or an object on the record, as a json column or a multiple lookup does"), and its replacement carries the prescription ("A field compared with a json or multiple field has no pushdown form: compare with a single-valued column"). This change moves where that registered class is refused on a sharing rule, from a silent zero-share at run time to authoring time; it adds no class and no prescription the entry does not already hold, and it rewrites no stored metadata. -->
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
**BREAKING** — retire `currencyConfig.precision`: a currency's decimal places are its currency's (#19992).
7+
8+
`currencyConfig.precision` was declared, validated against ISO 4217, and baked to `2`
9+
into parse output — and **no renderer or runtime ever read it**. objectui's
10+
`CurrencyField` derives an amount's decimal places from the currency's ISO 4217
11+
minor unit (2 for USD, 0 for JPY, 3 for KWD) and never looked at the key, so an
12+
author who wrote `precision: 4` saw the same two decimals as everyone else. Its
13+
only reader was its own contradiction check. ADR-0049 enforce-or-remove; triage
14+
direction REMOVE under ruling 乙 on #19910 — 「a currency's decimal places are the
15+
currency's, not a setting」.
16+
17+
Clause-②: no
18+
19+
## FROM → TO
20+
21+
| you wrote (17.4 and earlier) | write instead |
22+
| --- | --- |
23+
| `currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }` | `currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' }` |
24+
| `currencyConfig.decimals` / `currencyConfig.scale` (always refused, with a suggestion to write `precision`) | nothing — delete the key; the refusal now says why instead of suggesting `precision` |
25+
| a field whose amounts need a different number of decimals | a different currency: the width is the currency's minor unit and is declared nowhere |
26+
27+
**The one-line fix:** delete `precision` from every `currencyConfig`. ⛔ Do not move
28+
the number to the field-level `precision`: that key is the amount's TOTAL digit count
29+
(a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places, and it is
30+
unchanged by this release.
31+
32+
`os migrate meta --from 17` lists the mechanical edits for existing sources; apply
33+
them by hand.
34+
35+
## The retirement kit
36+
37+
- **`CurrencyConfigSchema.precision`** — removed from the shape. The schema is a
38+
`strictObject`, so the route is strict deletion plus a `guidance` entry: an
39+
authored key is refused as `unrecognized_keys` at `currencyConfig`, and the message
40+
carries the prescription (``currencyConfig.precision` was removed in
41+
@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever
42+
read it: …``). `tsc` refuses a literal in a typed position too — the key is off
43+
`CurrencyConfig`'s input type.
44+
- **The `decimals` / `scale` aliases** — gone with their target. Each is now answered
45+
with the same reason (`` `currencyConfig.scale` is not a currency configuration key,
46+
and nothing replaces it: … ``) and no rename suggestion.
47+
- **The ISO 4217 contradiction check** (the `.superRefine`) and **the
48+
default-materializing `.overwrite()`** — both existed only for this key and are
49+
removed. `CurrencyConfigSchema.parse({})` now returns exactly
50+
`{ currencyMode: 'dynamic', defaultCurrency: 'CNY' }`; `CurrencyConfigParsed` no
51+
longer declares `precision`. The internal helpers `currencyPrecisionContradiction`
52+
and `currencyFractionDigits` (never exported from a public entry) are removed; the
53+
CLDR table they read stays, because the `iso_4217_currency` value domain reads its
54+
key set.
55+
- **The field designer form** — the field-level `precision` row's help text read
56+
"Decimal places (e.g., 2 for $10.50)", the one reading the contract refuses. It now
57+
reads "Total digits", matching the key's describe and the object designer's row;
58+
the zh-CN / ja-JP / es-ES translations follow (`@objectstack/platform-objects`).
59+
- **Registry** — `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision`;
60+
the protocol-18 step gains the D2 conversion `currency-config-precision-removed` and
61+
its D3 entry `currency-config-precision-retired`, which states the two judgments the
62+
strip cannot make: a width declared where the old check never looked (a `dynamic`
63+
field, or a code with no known ISO 4217 minor unit) never applied, and code of your
64+
own that read the served key must derive the width from the field's currency.
65+
66+
## What an operator with STORED metadata sees
67+
68+
Nearly every stored currency field carries this key without anyone having written it:
69+
the old `.overwrite()` baked `precision: 2` into parse output, so `sys_metadata`
70+
object rows and built artifacts hold it. Nothing breaks at read: the conversion
71+
`currency-config-precision-removed` is retired from the load path but replayed by the
72+
stored-row and artifact seams, which strip the key from every field's
73+
`currencyConfig` on objects and object extensions and serve the row canonical. The
74+
strip is lossless — the key never had an effect — and the field-level `precision` is
75+
never touched. `os migrate meta --stored --apply` rewrites the stored rows so the
76+
per-row notice stops.
77+
78+
<!-- adr-0087: registered currency-config-precision-removed -->
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/rest": patch
3+
"@objectstack/runtime": patch
4+
---
5+
6+
**Pending metadata drafts are now served only to a caller with an authoring capability — the check `GET /api/v1/meta/_drafts` already made.** A pending draft is unpublished authoring work. Until now, every door that reads one, other than `/meta/_drafts`, served it to any signed-in caller who could open the item. The draft access that the `previewDrafts` / `state` request declarations, ADR-0106 D4 and ADR-0037 described as admin-gated upstream is now gated.
7+
8+
Clause-②: no
9+
10+
- **The doors:** `GET /api/v1/meta/:type/:name?state=draft` and `?preview=draft`, `GET /api/v1/meta/:type?preview=draft`, and `POST /api/v1/analytics/dataset/query` with `previewDrafts: true` or `?preview=draft` on `RestServer`, plus the runtime dispatcher's `/meta` item and list `?preview=draft`.
11+
- **Who may read drafts:** a system context, or a caller holding `studio.access`, `setup.access` or `manage_metadata`. This is the same predicate `/meta/_drafts` asks, not a second rule.
12+
- **Everyone else gets the read as if the draft switch were absent.** They receive the published version, pruned for them as the plain read prunes it. For a name that has nothing published, they receive that door's own absence answer: `404` on the item read, `404 NOT_FOUND` for a dataset by name, and the item simply missing from a list. The answer is byte-identical to the plain read, so it does not reveal whether a draft exists. For example, `?state=draft` on an app with no pending draft answers such a caller with the published app, not `404 NO_DRAFT`. A dataset preview run by such a caller uses live rows, never a pending seed draft's rows.
13+
- **Unchanged:** callers with an authoring capability read exactly what they read before. Whoever may save an app reads its `?state=draft` whole, and everyone else pruned per caller. `/meta/_drafts` still answers `403` to a caller without the capability, because it lists drafts and has no published answer to fall back to. `/diff` and `/history` are not changed by this release.

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,21 @@ code lives in `currencyConfig`, never in the value.
152152
| `precision` | `number` | — | Total digits |
153153
| `min` | `number` | — | Minimum value |
154154
| `max` | `number` | — | Maximum value |
155-
| `currencyConfig.precision` | `number` | `2` | Decimal precision for currency |
156155
| `currencyConfig.currencyMode` | `'dynamic' \| 'fixed'` | `'dynamic'` | Whether the column is pinned to `defaultCurrency` (`fixed`) or displays in the tenant default currency, the `localization.currency` setting (`dynamic`); the value is a bare number either way |
157156
| `currencyConfig.defaultCurrency` | `string` | `'CNY'` | The column's currency code under `fixed`; not the displayed currency under `dynamic` |
158157

159158
```typescript
160-
{ name: 'price', label: 'Price', type: 'currency', currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' } }
159+
{ name: 'price', label: 'Price', type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } }
161160
```
162161

163162
A currency amount's decimal places are its currency's ISO 4217 minor unit
164-
(2 for USD, 0 for JPY, 3 for KWD), not a field setting, so a `currency` field
165-
takes no `scale`: publish-time validation refuses the key. The field-level
166-
`precision` is the amount's total digit count — a DECIMAL(18,2) amount declares
167-
`precision: 18` — and is never compared with the currency.
168-
169-
A declared `currencyConfig.precision` must agree with the currency's ISO 4217
170-
fraction digits when the currency is statically known — that is, when
171-
`currencyConfig` is in `fixed` mode. Publish-time validation rejects the
172-
contradiction, naming both numbers ("currency JPY has 0 fraction digits;
173-
`precision: 2` contradicts it"): the yen has no minor unit to show two digits
174-
of, and `precision: 2` on a KWD field would silently drop the third fils digit
175-
that exists. Omit `currencyConfig.precision` to let renderers derive the width
176-
from the currency itself. The rule is
177-
deliberately partial: a field in `dynamic` currencyMode has no single currency
178-
to check against, and currency codes outside CLDR `currencyData`
179-
(cryptocurrency or custom business codes) are not checked. The rule judges
180-
only *authored* precision — an untouched `currencyConfig` (whose `precision`
181-
defaults to `2`) is never rejected, whatever its currency.
163+
(2 for USD, 0 for JPY, 3 for KWD), not a field setting, and they are declared
164+
nowhere. A `currency` field takes no `scale`, and `currencyConfig` has no
165+
decimal-places key: publish-time validation refuses `scale` on the field and
166+
`currencyConfig.precision` (removed in `@objectstack/spec` 17.5.0 — no renderer
167+
or runtime ever read it), each with a prescription to delete the key. The
168+
field-level `precision` is the amount's total digit count — a DECIMAL(18,2)
169+
amount declares `precision: 18` — and is never compared with the currency.
182170

183171
### `percent`
184172
Percentage value (0–100 or decimal).

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ discount: Field.percent({ label: 'Discount', scale: 2, min: 0, max: 1 }),
7474
price: Field.currency({
7575
label: 'Price',
7676
currencyConfig: {
77-
precision: 2,
7877
currencyMode: 'fixed', // 'fixed' pins the column to defaultCurrency;
7978
// 'dynamic' (default) shows the tenant default
8079
defaultCurrency: 'USD', // the column's currency under 'fixed'

‎content/docs/data-modeling/validation-rules.mdx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,10 @@ These properties apply to **all** field types and are validated by the base `Fie
152152
|:---|:---|:---|:---|
153153
| `min` | `number` | — | Minimum monetary value |
154154
| `max` | `number` | — | Maximum monetary value |
155-
| `currencyConfig.precision` | `number` | `2` | Decimal places (0–10) |
156-
| `currencyConfig.currencyMode` | `enum` | `dynamic` | `fixed` pins the field to `defaultCurrency`, and an authored `currencyConfig.precision` must agree with that currency's ISO 4217 fraction digits (the field-level `precision`, total digits, is not compared); `dynamic` displays the tenant default currency (the `localization.currency` setting). Not a per-record choice |
155+
| `currencyConfig.currencyMode` | `enum` | `dynamic` | `fixed` pins the field to `defaultCurrency`; `dynamic` displays the tenant default currency (the `localization.currency` setting). Not a per-record choice |
157156
| `currencyConfig.defaultCurrency` | `string` | `CNY` | 3-character currency code (ISO 4217 or crypto): the field's currency under `fixed`; not the displayed currency under `dynamic` |
158157

159-
**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The currency **code** is never stored per record: it is field configuration, carried by `currencyConfig` above. Precision defaults to 2 decimal places.
158+
**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The currency **code** is never stored per record: it is field configuration, carried by `currencyConfig` above. Decimal places are the currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD) and are declared nowhere — `currencyConfig` has no decimal-places key (`currencyConfig.precision` is refused, with a prescription to delete it), and the field-level `precision` is the total digit count, never compared with the currency.
160159

161160
### `percent`
162161

@@ -530,7 +529,7 @@ section above). See the
530529
| `html` | — | Sanitized, `maxLength` |
531530
| `richtext` | — | Sanitized, `maxLength` |
532531
| `number` | — | `min`, `max`, `precision`, `scale` |
533-
| `currency` | — | `currencyConfig` (precision, mode, code) |
532+
| `currency` | — | `currencyConfig` (mode, code) |
534533
| `percent` | — | `min`, `max`, stored as decimal |
535534
| `date` | — | ISO 8601 date |
536535
| `datetime` | — | ISO 8601 datetime, UTC |

‎content/docs/getting-started/common-patterns.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default defineStack({
5151
// Person references target the platform user object `sys_user`
5252
owner: { label: 'Owner', type: 'lookup', reference: 'sys_user' },
5353
due_date: { label: 'Due Date', type: 'date' },
54-
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' } },
54+
budget: { label: 'Budget', type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } },
5555
}
5656
}
5757
]
@@ -80,7 +80,7 @@ export default defineStack({
8080
// Person references target the platform user object `sys_user`
8181
owner: { label: 'Owner', type: 'lookup', reference: 'sys_user' },
8282
due_date: { label: 'Due Date', type: 'date' },
83-
budget: { label: 'Budget', type: 'currency', currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' } },
83+
budget: { label: 'Budget', type: 'currency', currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } },
8484
},
8585
},
8686
},

‎content/docs/protocol/objectql/types.mdx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,6 @@ annual_revenue:
293293
type: currency
294294
label: Annual Revenue
295295
currencyConfig:
296-
precision: 2
297296
currencyMode: fixed
298297
defaultCurrency: USD # the field's currency under fixed
299298
```

0 commit comments

Comments
 (0)