Skip to content

Commit 360efc9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20646-spec-registries-subpath
# Conflicts: # packages/cli/src/commands/migrate/meta.ts
2 parents 8558334 + 3b47a69 commit 360efc9

34 files changed

Lines changed: 2077 additions & 204 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-analytics': minor
4+
---
5+
6+
An authored analytics cube's measure `format` and time-dimension `granularities` now take effect on the analytics query doors, the way a compiled dataset's always have (#20282).
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: registered analytics-cube-single-granularity-default-enforced -->
11+
12+
**BREAKING**: this narrows what `POST /api/v1/analytics/query` and `POST /api/v1/analytics/sql` answer for one class of request. When an authored cube's time dimension declares exactly one granularity, a query that groups by that dimension without stating a granularity is now bucketed at the declared one. The raw-SQL path declines every bucketed query, so such a query now runs on the engine aggregate path, which answers `400 INVALID_FIELD` for every member it cannot evaluate: a custom-SQL measure (a measure of type `number`, `string` or `boolean` whose `sql` is an expression); and, on a cube whose members resolve through its `joins`, a measure or a `where` field over a joined object, a `timeDimensions` entry over a joined object (bucketed or a `dateRange` window, so grouping by a one-granularity time dimension over a joined object is refused too), a dimension that traverses more than one relationship, and an `avg` or `count_distinct` measure beside any dimension over a joined object. The raw-SQL path answers every one of these, with one group per distinct timestamp; each is now refused, exactly as it already was when the caller stated that granularity by hand. On a host that overrides `queryCapabilities` to offer raw SQL with no engine aggregate bridge (the plugin's default wires both), no strategy remains for a bucketed query, so every newly bucketed query, a plain `count` included, now answers "No strategy can handle query" instead of grouping raw timestamps. The remedy: run such a query without grouping by that dimension, or, if the dimension is not meant to have one default bucket, declare the granularities it offers as a list of two or more (or omit the key); on a raw-SQL-only host, add the engine aggregate bridge. It ships as `minor` under the launch-window convention; the widening half is two authored keys taking effect.
13+
14+
Until this change both keys were read on the compiled-dataset path only. One cube shape has three producers — cubes authored with `defineCube()` / `defineStack({ analyticsCubes })`, cubes the dataset compiler mints, and cubes inferred for an ad-hoc query — and only a compiled dataset's cube reached the two readers:
15+
16+
- **`measures.<metric>.format`** reached a caller as `fields[].format` only because the dataset door copies it from the DATASET measure. An authored cube has no dataset, so `POST /api/v1/analytics/query` described its measure columns with `name` and `type` alone. Now every measure column a query names carries the `format` its cube measure declares, whichever strategy answered, and a column that declares none carries no `format` key at all. `GET /api/v1/analytics/meta` is unchanged: its projection stays `name`, `type` and `title`, and a client reads `format` off the query result's `fields[]`, as the Data API page already says. The value is relayed verbatim; the vocabulary `fields[].format` documents is a numeral pattern such as `"$0,0.00"` or `"0.0%"`.
17+
- **`dimensions.<dimension>.granularities`** was the default bucket only for a compiled dataset, which the dataset executor filled in before querying. An authored cube's time dimension grouped raw timestamps whatever it declared. Now `query()` and the `generateSql()` dry run read it the same way, through the one rule both paths share: a single-entry list is the dimension's default bucket for a query that groups by it; a granularity the query states always wins, and one the list does not name is not refused (the dataset path compares against no list either); a list of two or more states no default; and a `timeDimensions` entry that carries only a `dateRange` for a dimension the query does not group stays a filter.
18+
19+
What to expect after upgrading:
20+
21+
- **A cube measure that declares `format`** now carries it on `POST /api/v1/analytics/query` results. A client that formats amounts from `fields[].format` starts formatting that column.
22+
- **A cube time dimension that declares one granularity** (`granularities: ['month']`) is now bucketed by it when a query groups by it without stating one: one row per month where there was one row per timestamp. Name another granularity in the query's `timeDimensions` to bucket differently.
23+
- **A cube time dimension that declares several, or none**, behaves exactly as before.
24+
- **Compiled datasets** (`POST /api/v1/analytics/dataset/query`) answer exactly as before: the value read off their cube is the one the dataset door already used.
25+
26+
In `@objectstack/spec`, `MetricSchema.format` and `DimensionSchema.granularities` now carry descriptions that state what the analytics service does with them (the metric's example values move from the names "currency" / "percent" to numeral patterns, the vocabulary the `fields[].format` slot documents), and the liveness ledger rows `analytics_cube.measures.format` and `analytics_cube.dimensions.granularities` move from `dead` to `live`, citing the new readers.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
refactor(spec): protocol 18's conversions are authored as identifier-sorted entries with an explicit application order — no value changes (#20574)
6+
7+
Nothing a consumer reads changes. `CONVERSIONS_BY_MAJOR[18]` (46 conversions, same
8+
order), `ALL_CONVERSIONS` (113, same order) and `MIGRATIONS_BY_MAJOR[18].conversionIds`
9+
are value-identical to the previous release, so the loader and the migration chain
10+
apply the same conversions in the same sequence.
11+
12+
What changed is how the list is written, so two major-18 retirements can be in flight
13+
at once without conflicting in `packages/spec/src/conversions/registry.ts`:
14+
15+
- `CONVERSIONS_BY_MAJOR[18]` is read off `MAJOR_18_CONVERSIONS`: one
16+
`{ conversion, order }` entry per conversion, kept sorted by the conversion's
17+
identifier and applied by ascending `order` (ties by the conversion's `id`). A
18+
retirement adds ONE entry where its identifier sorts — never at the end — with
19+
`order` one more than the highest present.
20+
- A new conversion is defined directly above the definition of the entry that follows
21+
it in that list, not at the end of the definitions.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
The `http` node's designer form says an outbound credential never goes in the node's `url` or `headers`, and where it goes instead (#20590)
6+
7+
Clause-②: no
8+
9+
The `http` action descriptor's `configSchema` is what the flow designer's palette and property form read (`GET /api/v1/automation/actions`). It described `url` as "Target URL" and `headers` as "Request headers", with no word about credentials. Both values are stored in the flow definition, and a flow definition is served to every member who can read flows; of the node's config, only `signingSecret` is withheld. The two field descriptions now say this, and name where the credential goes instead:
10+
11+
- a credential in a header: a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret;
12+
- a key in the query string: a declarative `rest` connector with `api-key` auth, whose `paramName` names the parameter and whose `auth.credentialRef` names the secret;
13+
- a webhook whose path is the secret: a token-authenticated connector instead, such as the `slack` connector with its bot token. No `credentialRef` variant carries a secret in the url path.
14+
15+
Description text only. No config key is added or removed, nothing more is withheld on read, and there is nothing to migrate.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate meta --from N` prints the schema verdict and the refusals first, then the applied mechanical edits, then the manual changes
6+
7+
Clause-②: no
8+
9+
`os migrate meta --from N` prints every semantic entry of every protocol major the
10+
chain crosses, whatever the stack uses: a `--from 17` run prints 242 manual changes.
11+
Those used to come before the schema verdict, which was the last line of the report
12+
and said "resolve the manual changes above". The refusals that keep the migrated
13+
stack from parsing were not listed at all. The only refusal list was the one printed
14+
while the config loaded, and that list names the stack as authored, including the
15+
keys the chain goes on to convert.
16+
17+
The human-readable report now prints three groups, each opened by one header line
18+
that counts it:
19+
20+
1. the verdict: `Migrated stack is schema-valid`, or
21+
`Migrated stack does not yet pass schema validation — N refusals left after the chain`
22+
followed by one `✗ path: message` line per refusal of the migrated stack;
23+
2. `Applied N mechanical change(s):`;
24+
3. `N manual change(s) require your judgment:`.
25+
26+
No manual change is dropped, merged or reworded. Every applied edit and every
27+
manual change prints byte for byte as before, in the same order within its group.
28+
The data-migration advice is still the last thing printed. A range that holds no
29+
migration step also leads with the verdict, which now lists the source's refusals,
30+
and the note naming the range to use follows it.
31+
32+
`--json` is unchanged: same keys, same values, same array order. The exit code is
33+
unchanged too: a run whose migrated stack does not parse still exits 0.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/plugin-webhooks': patch
3+
'@objectstack/plugin-audit': patch
4+
'@objectstack/plugin-security': patch
5+
---
6+
7+
fix(plugin-webhooks,plugin-audit,plugin-security): the ja-JP, es-ES and zh-CN object help, descriptions and labels that contradicted their current English source are re-translated (#20653)
8+
9+
Clause-②: no
10+
11+
A translated object leaf that a translator wrote by hand is kept as written
12+
when its English source changes later, so some leaves went on saying what the
13+
old source said. On a ja-JP, es-ES or zh-CN console the `sys_webhook` record
14+
page told an admin that `definition_json` carries the full headers / auth /
15+
retry / payload configuration, where the English help says credentials are not
16+
stored there: the signing secret and the custom headers live in the encrypted
17+
`signing_secret` and `headers_secret` fields.
18+
19+
Eighteen leaves (six paths, in all three locales) whose meaning contradicted
20+
the current English now match it:
21+
22+
- `@objectstack/plugin-webhooks`: the `sys_webhook.definition_json` help (no
23+
credentials in the JSON) and the `sys_webhook` description (dispatched by the
24+
webhook auto-enqueuer onto the shared HTTP outbox, not executed by an HTTP
25+
connector plugin; declared through `defineStack({ webhooks })` too);
26+
- `@objectstack/plugin-audit`: the `sys_activity.environment_id` label and help
27+
(Environment, not Project), and the `sys_audit_log.user_id` label (User: the
28+
object's separate `actor` field is the actor);
29+
- `@objectstack/plugin-security`: the `sys_position` description (positions
30+
distribute capability, not definitions for RBAC access control).
31+
32+
Leaves whose English source only gained detail, was reworded, or was
33+
title-cased (the `@objectstack/plugin-approvals` status and action options)
34+
are unchanged. Values only: no key is added or removed, and no provenance
35+
table changes.

‎content/docs/references/data/analytics.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Type: `[string, string]`
148148
| **description** | `string` | optional | |
149149
| **type** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'number' \| 'string' \| 'boolean'>` | ✅ | |
150150
| **sql** | `string` | ✅ | SQL expression or field reference |
151-
| **format** | `string` | optional | |
151+
| **format** | `string` | optional | Display format for this measure's result column: a numeral pattern such as "$0,0.00" or "0.0%". Relayed verbatim as fields[].format on POST /analytics/query results; not published by GET /analytics/meta. |
152152

153153
### Nested Shape: `Cube.dimensions[string]`
154154

@@ -159,7 +159,7 @@ Type: `[string, string]`
159159
| **description** | `string` | optional | |
160160
| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'time' \| 'geo'>` | ✅ | |
161161
| **sql** | `string` | ✅ | SQL expression or column reference |
162-
| **granularities** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>[]` | optional | |
162+
| **granularities** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>[]` | optional | For a time dimension. A single interval is its default bucket: a query that groups by this dimension without stating a granularity is bucketed at it. Two or more intervals state no default. A granularity the query states always wins, listed or not. |
163163

164164
### Nested Shape: `Cube.joins[string]`
165165

@@ -199,7 +199,7 @@ Type: `[string, string]`
199199
| **description** | `string` | optional | |
200200
| **type** | `Enum<'string' \| 'number' \| 'boolean' \| 'time' \| 'geo'>` | ✅ | |
201201
| **sql** | `string` | ✅ | SQL expression or column reference |
202-
| **granularities** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>[]` | optional | |
202+
| **granularities** | `Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>[]` | optional | For a time dimension. A single interval is its default bucket: a query that groups by this dimension without stating a granularity is bucketed at it. Two or more intervals state no default. A granularity the query states always wins, listed or not. |
203203

204204

205205
---
@@ -228,7 +228,7 @@ Type: `[string, string]`
228228
| **description** | `string` | optional | |
229229
| **type** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'number' \| 'string' \| 'boolean'>` | ✅ | |
230230
| **sql** | `string` | ✅ | SQL expression or field reference |
231-
| **format** | `string` | optional | |
231+
| **format** | `string` | optional | Display format for this measure's result column: a numeral pattern such as "$0,0.00" or "0.0%". Relayed verbatim as fields[].format on POST /analytics/query results; not published by GET /analytics/meta. |
232232

233233

234234
---

‎examples/app-showcase/src/automation/flows/index.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,17 +1014,27 @@ export const FanOutNotifyFlow = defineFlow({
10141014
edges: [],
10151015
},
10161016
{
1017-
// Slack is a CONNECTOR, not a notify channel (#4343): post through
1018-
// an incoming webhook, or a `connector_action` with the Slack
1019-
// connector. The retired `script` + `actionType: 'slack'` shape
1020-
// logged a line and delivered nothing.
1017+
// Slack is a CONNECTOR, not a notify channel (#4343). The retired
1018+
// `script` + `actionType: 'slack'` shape logged a line and
1019+
// delivered nothing. A real post goes through a connector, as
1020+
// `TaskCompletedSlackFlow` above does. This branch stays an `http`
1021+
// node because it is the showcase's `parallel` + `http` fixture
1022+
// (docs/qa/platform-checklist/areas/automation.json).
10211023
name: 'Post to Slack',
10221024
nodes: [
10231025
{
10241026
id: 'slack_post',
10251027
type: 'http',
10261028
label: 'Slack Notify',
10271029
config: {
1030+
// A placeholder, and it must stay one. This url is served with
1031+
// the flow definition to every member who can read flows, and
1032+
// an incoming-webhook url's path IS its secret, so a real one
1033+
// never goes here. A credentialed call goes through a
1034+
// connector whose credential lives outside the definition:
1035+
// a declarative instance's `auth.credentialRef`
1036+
// (src/system/connectors/index.ts), or the `slack` connector
1037+
// `TaskCompletedSlackFlow` uses.
10281038
url: 'https://hooks.slack.com/services/T000/B000/XXXX',
10291039
method: 'POST',
10301040
body: { channel: '#tasks', text: 'Task done: {record.title}' },

0 commit comments

Comments
 (0)