Skip to content

Commit f5a486b

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20478-dispatcher-meta-layers
2 parents 148af0a + 1c1b8c8 commit f5a486b

40 files changed

Lines changed: 2057 additions & 714 deletions
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/platform-objects": patch
4+
---
5+
6+
Clause-②: no
7+
8+
Four more live structured keys are authorable in the metadata forms: `activityMilestones`, `publicSharing` and `userActions` on the object form, and `inlineColumns` on the field form. Each was **declared** by its schema, graded `live` by the liveness ledger, and offered by **no** form in `METADATA_FORM_REGISTRY`, so an author's only door was the Source tab. Each is now a row whose sub-rows are declared by hand rather than derived from the schema:
9+
10+
- `activityMilestones` (object form, Advanced, beside `validations`) — a `type: 'repeater'` over the milestone's four keys: `field` (`widget: 'text'`, required), `value` and `summary` (text, required) and `type` (text). `field` pins its widget because the console turns a string sub-row named `field` into a field picker whose catalogue an object draft never fills.
11+
- `publicSharing` (object form, Advanced, after `requiredPermissions`) — a `type: 'composite'` over all six keys of the share-link policy: `enabled` (switch), `allowedAudiences` and `allowedPermissions` (`widget: 'multiselect'` over their enum members), `maxExpiryDays` (number, at least 1), `redactFields` (`widget: 'string-tags'`) and `eligibility` (`type: 'code'`, `language: 'expression'`).
12+
- `userActions` (object form, Advanced, under `managedBy`) — a `type: 'composite'` over the five affordance keys. `create`, `import`, `edit` and `delete` are each a boolean **or** a `{ enabled, visibleWhen, disabledWhen }` object, so they take `widget: 'json'`: the console renders a switch for a new entry or a stored boolean, and the object's own keys for a stored object, and never writes one arm over the other. `exportCsv` is a switch.
13+
- `inlineColumns` (field form, Configuration, beside `inlineTitle`, shown on `master_detail` fields) — a `type: 'repeater'` over a **curated subset** of the twenty keys an inline grid column accepts: `name` (required), `label`, `width` and `defaultHidden`. The metadata-form reconciliation ledger records the nested `subset` row and names what is left to source and why: `type` opts a column out of hydration from the child field, the type-specific keys cannot be gated on a type the column takes from the child field at render, and the rules are copies of the child field's own.
14+
15+
The help text states what the runtime does with each value, read from its consumer, and claims a refusal only where one exists. A misspelt `publicSharing.redactFields` entry is refused at publish and by `os validate`. `activityMilestones[].field`, a `{token}` in its `summary`, and `inlineColumns[].name` are judged by no authoring door, and their help texts say so and name what happens instead: the milestone never fires, the token renders empty, the column renders as plain text.
16+
17+
The two new repeaters' row schemas also carry a JSON Schema `title` on every property, as every repeater row schema must: the four keys of an `activityMilestones` entry, and all twenty keys of `InlineGridColumnSchema`. Each title is a `.meta({ title })` call and nothing more.
18+
19+
⛔ **No schema accept set moves and no export changes.** `METADATA_FORM_REGISTRY` is declared as an opaque `Readonly<Record<string, FormView>>`, so row contents were never part of the declared surface. What changes is the **form payload** `getMetaTypes()` serves (its rows, and the titles above in its JSON Schema) and the translation keys `os i18n extract` walks, hence the regenerated `platform-objects` metadata-form bundles. Their 46 new leaves are authored in `zh-CN`, `ja-JP` and `es-ES` rather than left as extractor fills.
20+
21+
⛔ **The gate that would notice a missing row is NOT landed here.** The reconciliation gate's top-level `zodOnly` direction stays unwired; this change lands offers and one nested ledger row only.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/objectql": minor
4+
"@objectstack/metadata-protocol": patch
5+
---
6+
7+
A grouped or aggregated query now honours `search`: the groups and every aggregated number are computed over the searched rows, exactly the rows the same query without `groupBy` / `aggregations` returns.
8+
9+
Clause-②: yes (widening) — `EngineAggregateOptionsSchema` gains two OPTIONAL keys, `search` and `searchFields`, so the accept set of the aggregate options grows. Nothing previously admitted is refused, no key is renamed or retired, and no producer is required to write them.
10+
11+
`QuerySchema.search` (ADR-0061) is declared on the query beside `groupBy` and `aggregations`, with no carve-out. Until now, `POST /data/:object/query` accepted a body such as `{ groupBy: ["business_unit"], aggregations: [{ function: "count", alias: "count" }], search: "harbour" }` and answered it with the UNSEARCHED groups — no error and no warning — while the same body without `groupBy` / `aggregations` returned only the searched rows. A grouped list view under a toolbar search would therefore show group headers that ignore what the user typed.
12+
13+
- **`@objectstack/spec`** — `EngineAggregateOptionsSchema` declares `search` (the bare string, or the structured `FullTextSearchSchema` form) and `searchFields`, identically to `EngineQueryOptionsSchema`. A parse used to strip them.
14+
- **`@objectstack/objectql`** — `engine.aggregate()` (and `ctx.api.object(name).aggregate()`) accepts the two keys it used to refuse as unknown options, and expands them through the same ADR-0061 expansion `find()` uses: the same server-resolved searchable fields, the same `searchFields` narrowing, AND-ed with `where` before the security middlewares run. There is one expander, not two. It applies on both aggregate paths, native `driver.aggregate()` and the in-memory lowering. A key the verb still does not execute, such as `$search`, is refused as before.
15+
- **`@objectstack/metadata-protocol`** — `findData`'s grouped branch passes `search` / `searchFields` to `engine.aggregate()`. `searchFields` is validated on that branch exactly as on the flat one: a column search cannot scan is `400 INVALID_FIELD`.
16+
17+
Nothing to migrate. A caller that worked around the gap, for example by grouping a page of searched rows on the client, can send the grouped query with its `search` instead.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Reword the `CurrencyConfigSchema.precision` clause in two major-18 D3 migration entries (`os migrate meta`) — the key it describes as "unchanged" is retired in this same protocol major (#20379).
6+
7+
`18.field-scale-precision-integer-refused.ts` and `18.ui-form-field-precision-scale-integer-refused.ts` each carried a sentence distinguishing `CurrencyConfigSchema.precision` from the field/row-level `scale`/`precision` keys those entries retire, saying the currency key is a different surface and is unchanged. `currency-config-precision-removed` (D3 `currency-config-precision-retired`) retires that same key in this same major, so the sentence became false the moment that conversion landed. Reworded only that clause in each entry to say the key was retired in this same protocol major by `currency-config-precision-removed`.
8+
9+
The form-field entry's other clause was also wrong on its own terms: it named a "gantt `scale` enum" that `GanttConfigSchema` does not declare (its granularity key is `viewMode`; `strictObject` refuses `scale` there). Corrected it to name the surface that actually carries an enum-valued `scale` — `TimelineConfigSchema.scale`, still unchanged — and to say plainly that the gantt view has no `scale` key.
10+
11+
Clause-②: no
12+
13+
No schema, key, conversion or verdict changes — text only. `packages/spec/src/migrations/registry.ts` regenerated with `gen:migration-registry` so the printed `os migrate meta` text matches.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the zh-CN platform-object bundle no longer ships English labels, options and help as copies of the source (#20462)
6+
7+
Clause-②: no
8+
9+
A zh-CN console showed English on Setup surfaces: the Invite user dialog listed
10+
the membership roles as 所有者 / 管理员 / Delegated Admin / 成员, and a team record
11+
read MEMBER COUNT. The keys were present in `zh-CN.objects.generated.ts`, but
12+
their values were byte copies of the English source that `os i18n extract
13+
--fill=default` seeds.
14+
15+
- 320 of the 362 zh-CN string leaves that equalled their `en` source are now
16+
translated: labels, plural labels, select options, help, descriptions and
17+
empty states. `delegated_admin` reads 受托管理员 on both
18+
`sys_member` and `sys_invitation`, the word the console already uses for that
19+
role.
20+
- The other 42 stay English by design, and each is recorded with its reason in
21+
`objects-zh-cn-echo-decisions.test.ts`: sign-in provider brands (Google, GitHub,
22+
…), the bare `ID` initialism, protocol names (JWKS, IdP SSO URL, Message-ID,
23+
UI / API), the OAuth credential names in the create-application result dialog,
24+
and placeholders that show a value the admin types.
25+
26+
`zh-CN.source-hashes.generated.ts` was regenerated by `pnpm i18n:extract`: it
27+
records a leaf only while the leaf is a copy of its source, so it now lists
28+
exactly those 42. `ja-JP` and `es-ES` are unchanged.

‎content/docs/references/data/data-engine.mdx‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ Options for DataEngine.aggregate operations
152152
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | |
153153
| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
154154
| **timezone** | `string` | optional | |
155+
| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | |
156+
| **searchFields** | `string[]` | optional | |
155157
| **filter** | `Record<string, any> \| any` | optional | Data Engine query filter conditions |
156158

157159

@@ -708,6 +710,8 @@ This schema accepts one of the following structures:
708710
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | |
709711
| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
710712
| **timezone** | `string` | optional | |
713+
| **search** | `string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }` | optional | |
714+
| **searchFields** | `string[]` | optional | |
711715
| **filter** | `Record<string, any> \| any` | optional | Data Engine query filter conditions |
712716

713717
---
@@ -898,6 +902,8 @@ QueryAST-aligned options for DataEngine.aggregate operations
898902
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; filter?: any }[]` | optional | |
899903
| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
900904
| **timezone** | `string` | optional | |
905+
| **search** | `string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }` | optional | |
906+
| **searchFields** | `string[]` | optional | |
901907

902908
### Nested Shape: `EngineAggregateOptions.context`
903909

@@ -954,6 +960,19 @@ QueryAST-aligned options for DataEngine.aggregate operations
954960
| **distinct** | `never` | optional | [REMOVED] `query.aggregations[].distinct` was removed in @objectstack/spec 17 (ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while `driver-sql`, `driver-turso`, `driver-mongodb`, `driver-memory` and the service-analytics SQL builder all ignored it — so `{ function: 'sum', field: 'amount', distinct: true }` answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the `count_distinct` aggregation function, which every SQL face compiles to `COUNT(DISTINCT field)` and the in-memory fallback computes identically. `SUM(DISTINCT …)` / `AVG(DISTINCT …)` get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. |
955961
| **filter** | `any` | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate: lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. |
956962

963+
### Nested Shape: `EngineAggregateOptions.search`
964+
965+
| Property | Type | Required | Description |
966+
| :--- | :--- | :--- | :--- |
967+
| **query** | `string` | ✅ | Search query text |
968+
| **fields** | `string[]` | optional | Fields to search in (if not specified, searches all text fields) |
969+
| **fuzzy** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only `query` + `fields`; no executor receives this flag. |
970+
| **operator** | `Enum<'and' \| 'or'>` | optional (default: `"or"`) | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
971+
| **boost** | `Record<string, number>` | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
972+
| **minScore** | `number` | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
973+
| **language** | `string` | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
974+
| **highlight** | `boolean` | optional (default: `false`) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
975+
957976

958977
---
959978

‎packages/lint/src/validate-predicate-path-refs.test.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,16 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
605605
// `field :: accept | currencyConfig | dependsOn | lookupColumns |
606606
// lookupFilters | relatedListColumns`, plus `action :: patch` and
607607
// `action :: bodyExtra`. The other eight rows carry no predicate.
608-
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(81);
608+
// It is 82 today, an ADDITION of ONE: #19332 (flight G2b) gave four live
609+
// structured keys a form row each, and one of them carries a meaningfulness
610+
// gate — the field form's `inlineColumns` repeater, read only on a
611+
// `master_detail` field, like its `inlineTitle` / `inlineAmountField`
612+
// siblings. Measured, not inferred: the shipped corpus was differenced
613+
// against the merge base `e956924e` by `<form>::<field>::<source>`,
614+
// 81 → 82, `field :: inlineColumns :: data.type == 'master_detail'` added
615+
// and NONE removed. The other three rows and all their sub-rows carry no
616+
// predicate.
617+
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(82);
609618

610619
const findings = validatePredicatePathRefs(corrupted);
611620
expect(findings).toHaveLength(predicates);
@@ -701,7 +710,10 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
701710
// `data.type == 'currency'`, `action :: patch` on
702711
// `data.operation == 'update'`, `action :: bodyExtra` on
703712
// `data.type == 'api'`); the other five are `in`-list gates.
704-
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(56);
713+
// It is 57 today: #19332 G2b's one new predicate, `field :: inlineColumns`
714+
// on `data.type == 'master_detail'`, compares against a single-quoted
715+
// literal.
716+
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(57);
705717

706718
const rhsFindings = validatePredicatePathRefs(corrupted)
707719
.filter((f) => f.rule === PREDICATE_RHS_PATH_SHAPED);

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11067,6 +11067,16 @@ export class ObjectStackProtocolImplementation implements
1106711067
// was finding 1: the one wire path to aggregate() lost the
1106811068
// clause before any executor could ever see it.
1106911069
having: options.having,
11070+
// ADR-0061 `search` is declared on the query beside `groupBy` /
11071+
// `aggregations` with no carve-out, and the flat branch below
11072+
// hands it to `engine.find`. Leaving it out of THIS bag answered
11073+
// a grouped query under a search with the UNSEARCHED groups —
11074+
// no error, no warning. The engine expands it through the same
11075+
// expander `find` uses, so the header numbers are the grouping
11076+
// of exactly the rows the flat query returns. `searchFields` was
11077+
// validated above on both branches (#4254 gate).
11078+
search: options.search,
11079+
searchFields: options.searchFields,
1107011080
context: options.context,
1107111081
} as any);
1107211082
// Apply limit client-side (EngineAggregateOptions doesn't carry limit).

0 commit comments

Comments
 (0)