Skip to content

Commit 3d35f8f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-14503-package-routes-single-implementation
2 parents ba6b831 + ed7243d commit 3d35f8f

290 files changed

Lines changed: 26705 additions & 1849 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `AGGREGATE_FIELD_TYPE_COMPATIBILITY` accepts `boolean` / `toggle` for `sum` / `avg` / `min` / `max` — ruling #11152 (booleans aggregate as numbers on every backend) stands over batch #59's blanket default (#16685)
6+
7+
The aggregate × field-type table declared by `@objectstack/spec/data` gains the boolean class (`boolean`, `toggle`) on its four arithmetic / order rows. Two maintainer rulings collided on that class: decision batch #59 (2026-09-06) said "every other pair: refused" without ever naming booleans, while ruling #11152 (2026-08-28) pins that booleans aggregate as NUMBERS on every backend with no per-aggregate exception — `sum(flag)=3`, `avg(flag)=0.5`, `min(flag)=0`, `max(flag)=1`, enrolled on six backends by the spec's own `AGGREGATION_CASES`, and implemented by `driver-sql`'s Postgres cast (#11635). The director ruling of decision batch #80 (2026-09-08, #16685, maintainer verbatim 「其他同意」, option A) holds that the specific ruling stands over the blanket default: the four rows carry both boolean members, and nothing else moves — `AGGREGATION_CASES` and the driver cast are untouched.
8+
9+
| Aggregate | Accepted field types |
10+
|---|---|
11+
| `count`, `count_distinct` | every `FieldType` |
12+
| `sum` | `number`, `currency`, `rating`, `slider`, `progress`, `summary`, **`boolean`, `toggle`** |
13+
| `avg` | the numeric class (`percent` included), **`boolean`, `toggle`** |
14+
| `min`, `max` | the numeric class, `date`, `datetime`, `time`, **`boolean`, `toggle`** |
15+
| every other pair | refused |
16+
17+
Why it matters: `avg(flag)` is the win-rate / SLA-violation-rate shape (#11065) — the reason the conformance table exists. A compatibility table refusing it would refuse a pair every backend is REQUIRED to answer, and the two consumer legs that execute this table (the compile-time refusal in the dataset compiler, #16099, and the authoring-time lint rule) would have turned a supported measure into an authoring error.
18+
19+
**Additive, `minor`.** No export is added, removed or renamed — the exported declarations are unchanged (the table's declared type is the same; only its value gains members), the new `BOOLEAN_AGGREGATE_FIELD_TYPES` constant is module-private — absent from `dist/*.d.ts` and from the bundles' export lists — and `api-surface/` and `export-origins/` are untouched — and the accept set only WIDENS: every pair accepted before is still accepted. It rides the same release as the table's own changeset, so the version outcome is the same either way; `minor` is declared because widening a published accept set is the same class of change the table's introduction was (decision batch #35 puts additive widening at `minor`), and because the two consumer legs are the change's real audience: a measure over a boolean field compiles and lints clean.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): declare the aggregate × field-type compatibility matrix a dataset measure is judged against — `AGGREGATE_FIELD_TYPE_COMPATIBILITY` and `isAggregateCompatibleWithFieldType` (#16353, spec half of #16099)
6+
7+
A dataset measure pairs an `aggregate` with a `field`, and nothing between author and driver correlated the two: `avg` over a `Field.datetime` compiled to `AVG(col)` and reached the backend, where one SQL family averages the column's storage form and another rejects the call — one metadata document, two answers. Which pairs are accepted is a contract, so it is now declared once in `@objectstack/spec/data`:
8+
9+
| Aggregate | Accepted field types |
10+
|---|---|
11+
| `count`, `count_distinct` | every `FieldType` |
12+
| `sum` | `number`, `currency`, `rating`, `slider`, `progress`, `summary` — the numeric class EXCEPT `percent` (a rate does not add; `isIncoherentAggregate` already says so) |
13+
| `avg` | the numeric class, `percent` included |
14+
| `min`, `max` | the numeric class plus `date`, `datetime`, `time` — both return a value of the field's own type |
15+
| every other pair | refused |
16+
17+
The ruling (director, decision batch #59, 2026-09-06) named its buckets by category; the table resolves them against the real `FieldType` membership through the `field-value.zod` semantic classes: "numeric" is `NUMERIC_VALUE_TYPES` (`integer` is a driver-internal column alias, not a `FieldType` — the integer-valued authorable members are `rating` / `slider` / `progress`); "temporal" is the three temporal classes, `time` included because its stored form is a dialect question exactly like `date` / `datetime` (native TIME on Postgres and MySQL, canonical `HH:MM:SS[.fff]` TEXT on SQLite), the canonical form orders chronologically on every dialect, and `AnalyticsResult.fields[].type` already describes `min` / `max` over it as temporal (#15768). `formula` is refused for arithmetic aggregates whatever its declared `returnType`: it is virtual in SQL storage, no column exists to aggregate.
18+
19+
**Booleans** (`boolean`, `toggle`) are NOT in the "every other pair" default: maintainer ruling #11152 has every backend answer them as numbers (`sum(flag)=3`, `avg(flag)=0.5`, `min(flag)=0`, `max(flag)=1`, pinned in the spec's `AGGREGATION_CASES`; `driver-sql` casts the aggregand on Postgres to make it hold), and the director ruling of decision batch #80 (2026-09-08, #16685) holds that specific ruling over batch #59's blanket default — the four arithmetic / order rows carry both boolean members (see that changeset). One refused row overrides an existing opinion and is recorded as such, not presented as agreement: **the string classes** are refused for `min` / `max` here, while `service-analytics` (#15768) already types `min` / `max` over them as a supported `'string'` result; the refusal is defensible (string order is collation-dependent) but it overrides that opinion.
20+
21+
**The narrowing, stated plainly.** Every pair outside the table — `avg` × `datetime`, `sum` × `text`, `min` × `json`, `sum` × `percent`, and so on — is an authoring shape `DatasetMeasureSchema` accepts today and will be REFUSED once the two consumer legs land: the compile-time refusal in the dataset compiler (#16099) and the authoring-time lint rule (its devx sub-card). A measure whose pair is refused is fixed by changing the aggregate to one the field's type supports (`min` / `max` for a temporal field; `avg` for a `percent`; `count` for anything), never by widening the table.
22+
23+
**Not breaking in this release, `minor` on purpose.** This changeset ships a table and a predicate that nothing yet enforces: `DatasetMeasureSchema` accepts byte-for-byte what it accepted before, no export is removed or narrowed, and no runtime path reads the table yet. It is an additive widening of the published surface — two new exports in `dist/*.d.ts` — which the maintainer ruling of 2026-09-04 (decision batch #35) puts at `minor`. The refusal itself arrives with the consumer legs, whose changesets carry the breaking declaration, its migration prescription and the ADR-0087 disposition; this one names the narrowing so an upgrading author can read the contract before it is executed.
24+
25+
`isIncoherentAggregate` is unchanged and stays the semantic opinion beside this table. The two diverge on exactly one pair: `count_distinct` × `percent` is flagged there and accepted here (the ruling reads `count_distinct` as "any type"). That divergence is pinned in the table's test and reported on #16353 rather than resolved silently.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/runtime": minor
4+
---
5+
6+
feat(spec)!: `timeDimensions[].dateRange`'s string arm closes to the date-range preset vocabulary; any other string is refused with `400 ANALYTICS_DATE_RANGE_UNRECOGNIZED` (#16041)
7+
8+
<!-- adr-0087: registered analytics-time-dimension-date-range-vocabulary-closed -->
9+
10+
**BREAKING** — an accept-set narrowing on a published analytics contract.
11+
`AnalyticsQuerySchema.timeDimensions[].dateRange` (and with it the
12+
`POST /analytics/query` / `/analytics/sql` bodies, `AnalyticsQueryRequestSchema`,
13+
and the `AnalyticsQuery` type every driver and `AnalyticsService.query` caller is
14+
typed against) used to accept ANY string. It now accepts exactly the thirteen
15+
dashboard date-range preset names, derived from `data/date-range-presets.ts`
16+
(`z.enum(DATE_RANGE_PRESETS)` — the vocabulary's single source of truth since
17+
#4614, so the two cannot drift), or the unchanged `[start, end]` array arm.
18+
Shipped as `minor` under the repo's launch-window convention for breaking
19+
changes; the hand-migration prescription is registered under protocol major 18.
20+
Maintainer ruling on #16041 (2026-09-06, decision batch #57, option A —
21+
contract first, 「同意」): 「本项目以协议为基准。所以开发应该对其协议,协议有问题应该立卡修改协议」.
22+
23+
## What was wrong
24+
25+
The arm was a bare `z.string()` whose only documented example — `"Last 7 days"`,
26+
in the schema's own comment — was a value no driver could parse. `driver-memory`
27+
recognised exactly `today` and a case-sensitive `last N <unit>` and fell every
28+
other string through to a `[range, range]` pseudo-window that (measured through
29+
mingo, 2026-09-05) matched **every `Date`-typed row**, 2099 included, because a
30+
`Date` compares above a `String` under BSON cross-type ordering. The SQL
31+
strategies read the same bare string as a single ISO day. A dashboard asking for
32+
one week silently got all of history on one backend and one day on the other,
33+
at HTTP 200 on both.
34+
35+
## What it does now
36+
37+
- The string arm is `AnalyticsDateRangePresetSchema = z.enum(DATE_RANGE_PRESETS)`
38+
(`today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`,
39+
`this_quarter`, `last_quarter`, `this_year`, `last_year`, `last_7_days`,
40+
`last_30_days`, `last_90_days`); the schema example is corrected to
41+
`'last_7_days'`.
42+
- Any other value raises ONE prescriptive issue at `timeDimensions.N.dateRange`
43+
(`analyticsDateRangeRefusalMessage`: the value, the vocabulary, the array
44+
spelling for an explicit window). `@objectstack/spec/data` exports the
45+
structural predicate `isAnalyticsDateRangeRefusalIssue` for doors.
46+
- `POST /analytics/query` and `/analytics/sql` answer the ADR-0112 envelope
47+
**`400 ANALYTICS_DATE_RANGE_UNRECOGNIZED`** — a new `ERROR_CODE_LEDGER` member
48+
registered under `@objectstack/runtime` — and the analytics service is never
49+
reached. A body wrong in more places than the `dateRange` stays the generic
50+
`400 VALIDATION_FAILED` + `details.fields[]`.
51+
52+
## FROM → TO
53+
54+
| you wrote | write instead |
55+
|:--|:--|
56+
| `dateRange: 'Last 7 days'` / `'last 7 days'` | `dateRange: 'last_7_days'` |
57+
| `dateRange: 'Last 30 days'` / `'last 30 days'` | `dateRange: 'last_30_days'` |
58+
| `dateRange: 'last 3 months'` | `dateRange: 'last_90_days'`, or an explicit `['{90_days_ago}', '{today}']` |
59+
| `dateRange: '2026-01-20'` (the SQL single-day dialect) | `dateRange: ['2026-01-20', '2026-01-20']` |
60+
| `dateRange: 'This week'` | `dateRange: 'this_week'` |
61+
| `dateRange: ['2026-01-01', '2026-01-31']` | unchanged |
62+
63+
Measured in this repository at the ruling: three authored `'Last 7 days'`, all
64+
in `packages/spec` tests (re-spelled here), and no published dashboard authors
65+
the string arm at all — the shipped console lowers presets to the array arm
66+
before querying. The drivers' own refusal of a non-conforming value that reaches
67+
them in-process (past the schema) is the sibling card #16322, blocked by this
68+
one; the fenced `service-analytics` fixture that authors the retired bare-ISO
69+
spelling is that card's to re-triage.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
`@objectstack/plugin-auth` pins the `better-auth` family to an exact `1.7.2`, so a fresh install of a published `@objectstack/*` release loads the auth plugin again — and with it creates the system tables and seeds the admin.
6+
7+
Published 17.1.0, 17.2.0 and 17.3.0 declared `"@better-auth/core": "^1.7.2"` and imported `createLocalAccountIssuer` / `createOAuthAccountIssuer` from `@better-auth/core/db`. `@better-auth/core@1.7.3` — a **patch** — deleted both names, and the `account.issuer` column behind them, because upstream rolled the issuer-scoped account identity back to opt-in (better-auth/better-auth#10909). A static ESM named import of a missing export is a link-time `SyntaxError`, so the plugin could not load at all. Every symptom followed from that one failure and every one of them was quiet: the scaffolded project's CLI printed the `SyntaxError` as a scrollable oclif warning and carried on, the server printed `✓ Server is ready` on the broken boot, `sys_user` / `sys_organization` / `sys_permission_set` / `sys_position` were never created, the seeded admin sign-in never answered, and the Console's sign-in form answered `Auth request failed with status 404`.
8+
9+
**This is a stopgap, deliberately, and it is labelled as one.** Upstream removed the export on purpose; adopting 1.7.3 means dropping `sys_account.issuer` — a required column with a unique `(issuer, accountId)` index — from the platform object, retiring the boot-time backfill that stamps it, and migrating every existing deployment. That is its own change with its own decision to make; this one restores a working install today.
10+
11+
All five members `plugin-auth` declares move together (`better-auth`, `@better-auth/core`, `@better-auth/oauth-provider`, `@better-auth/scim`, `@better-auth/sso`), because they are only correct as one line: `@better-auth/core@1.7.2` and `@better-auth/kysely-adapter@1.7.3` are mutually incompatible in both directions. `better-auth@1.7.2` declares its own siblings exactly, so pinning those five resolves all twelve family members to 1.7.2 — measured on a fresh `npm install` with no lockfile.
12+
13+
The workspace `overrides` move to the same exact target in step, so the version this repository tests is the version a consumer resolves. In-repo resolutions are unchanged: the lockfile already held 1.7.2 for all eleven overridden members.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
docs(cli): give the two `plugin` artifacts their own nouns, and rewrite "Which scaffolder?" as a two-question decision (#16484, #15531)
6+
7+
`plugin` names two different artifacts in this CLI, and neither the help text
8+
nor the docs said which one a reader was about to get:
9+
10+
- `os init <name> -t plugin` scaffolds a **metadata package** — declarative
11+
objects another stack loads, built by `objectstack compile`, emitted
12+
`private: true`.
13+
- `os create plugin <name>` scaffolds a **kernel code plugin** — TypeScript
14+
implementing the kernel `Plugin` contract, built by `tsc`, publishable as
15+
`@objectstack/plugin-<name>`.
16+
17+
Someone who wanted a "plugin skeleton" and reached for the nearer of the two got
18+
the wrong artifact, with no failure anywhere to tell them so — the metadata
19+
package has no `Plugin` to implement, and the kernel code plugin has no
20+
declarative objects to compile.
21+
22+
**No flag and no subcommand is renamed.** `-t plugin` and `os create plugin` are
23+
published surface and are spelled exactly as before; renaming them is a separate
24+
decision, not this change. What moved is the NOUN each surface uses for the
25+
artifact, so the two shapes stop sharing one word:
26+
27+
- `os init --help` now reads `Template: app, plugin (a metadata package), empty`,
28+
and the `plugin` template describes itself as
29+
`Metadata package: declarative objects another stack loads` rather than
30+
`Reusable plugin with objects`.
31+
- `os create`'s `plugin` template describes itself as a **kernel code** plugin.
32+
33+
The "Which scaffolder?" guidance in `content/docs/deployment/cli.mdx` is now a
34+
two-question decision — *metadata or kernel code?* then *a new project, or an
35+
addition to a directory you already have?* — landing on exactly one of the four
36+
entry points, each with the reason to pick it: `npm create objectstack@latest`
37+
(equivalently `npx create-objectstack`), `os init`, `os init <name> -t plugin`,
38+
and `os create plugin <name>`. `os create example` is deliberately absent: it was
39+
retired in #16483.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/client": patch
3+
---
4+
5+
The client SDK reads the CRUD data prefix off the discovery document instead of restating `/data` as a literal, so a deployment that sets a non-default `crud.dataPrefix` is reachable through the scoped surface.
6+
7+
`crud.dataPrefix` moves two things together: REST mounts every CRUD route under `${basePath}${crud.dataPrefix}`, and the discovery handler advertises the same value as `routes.data = ${realBase}${crud.dataPrefix}`. The SDK is the third surface describing those same paths, and its scoped half was not reading the value — it wrote `/data` into all seventeen of its data methods. On a deployment that moved the prefix, that half called paths the server does not mount, while the unscoped half of the *same* SDK called the right ones: the unscoped methods build `${baseUrl}${getRoute('data')}` and `routes.data` already carries the prefix. One SDK disagreed with itself about where the data routes are.
8+
9+
- **`_dataPrefix()` recovers the prefix from the advertised routes.** `routes.data` is one string carrying two unknowns (`{realBase}{dataPrefix}`) and no discovery key carries either half alone, so the split is recovered in two steps. A value that already ends with the conventional `/data` *is* the default prefix — taken first, which is what makes the change unable to regress any deployment that works today: every later rule can only run in the branch where the previous single-literal code was already wrong. Otherwise `routes.metadata` supplies the missing equation, being `{realBase}{metadata.prefix}` over the same base, so the two advertised routes share exactly `realBase` plus whatever their prefixes share; cutting that common run back to its last `/` lands on the boundary. This also covers a document served from the environment-scoped mount, where both routes carry the same `/environments/{id}` segment.
10+
- **It declines rather than guess.** Where the document does not determine the split — no advertised routes, no `routes.metadata`, or a derived prefix of `/` or empty — the derivation returns the conventional `/data`, which is byte-identical to the previous behaviour. This follows the rule the neighbouring `_apiBase()` already sets in this file, and it is why an unconnected client is unaffected.
11+
- **`_apiBase()` strips the advertised prefix instead of the literal `/data`.** It previously declined whenever the prefix was non-default, because the only suffix it knew how to strip was `/data`. It now strips whatever `_dataPrefix()` read, so the base and the prefix are derived by one rule and cannot disagree. On every default-prefix deployment the result is unchanged.
12+
13+
No new client option and no new configuration: the value is read from the server that already publishes it. A client that never calls `connect()` builds exactly the URLs it built before.

0 commit comments

Comments
 (0)