Skip to content

Commit df37e65

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16745-cloud-provided-package-version
2 parents 8d6f1d9 + bccf311 commit df37e65

8 files changed

Lines changed: 1416 additions & 27 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
fix(cli): `os generate migration` emits the field-level unique index the driver creates (#16317)
6+
7+
## What was wrong
8+
9+
Both migration formats emitted the table and none of the object's declared
10+
uniqueness. Measured on live PostgreSQL 16.13 — one object driven through all
11+
three producers into three schemas, `pg_indexes` read back per schema:
12+
13+
```ts
14+
{ name: 'probe', fields: { keyed_unique: { type: 'text', unique: true, maxLength: 100 } } }
15+
```
16+
17+
| producer | before | after |
18+
|:--|:--|:--|
19+
| `driver-sql` via `initObjects` | `probe_pkey`, `uniq_probe_keyed_unique` | unchanged |
20+
| `--format sql` | `probe_pkey` | `probe_pkey`, **`uniq_probe_keyed_unique`** |
21+
| `--format ts` | `probe_pkey` | `probe_pkey`, **`uniq_probe_keyed_unique`** |
22+
23+
Two rows with the same `keyed_unique` value were refused by the platform's table
24+
(`23505 ... violates unique constraint "uniq_probe_keyed_unique"`) and accepted
25+
by both generated ones, with nothing reporting it: a scaffold that creates the
26+
table for an object silently dropped a uniqueness guarantee the object declares.
27+
After the change the duplicate is refused by all three, each naming the same
28+
constraint.
29+
30+
The key set was not missing — it was already computed here to size the keyed
31+
text family's columns; only the index it implies was never emitted.
32+
33+
## What it does now
34+
35+
- **`--format sql`** emits an inline `CONSTRAINT "<name>" UNIQUE (<columns>)`.
36+
That is what knex's `table.unique(columns, { indexName })` — the driver's own
37+
call — compiles to on PostgreSQL, so a generated table and a platform-created
38+
one agree in `pg_constraint` as well as in `pg_indexes`; and it stays inside
39+
the statement's `IF NOT EXISTS`, which a following `ALTER TABLE ... ADD
40+
CONSTRAINT` has no spelling for.
41+
- **`--format ts`** emits that knex call itself, `indexName` included — which is
42+
what makes the driver recognise the constraint as already present on its first
43+
boot against a generated table, instead of adding a second one under its own
44+
name and then reporting the generated one as an orphan to drop.
45+
- Names come from a transcription of `driver-sql`'s `buildIndexName`, pinned
46+
against the driver's own export (a CLI production module may not statically
47+
value-import a driver package).
48+
49+
## What it deliberately still does not emit — and now says so
50+
51+
Both formats print a `NOT EMITTED:` line naming the index, its key parts and the
52+
reason, instead of dropping it silently:
53+
54+
- the **organization-scoped composite** (`unique: true` / `'organization'` on an
55+
object with an organization column), whose key part is
56+
`COALESCE(<organization column>, '__global__')`. Emitting the bare composite
57+
instead would be worse than emitting nothing: under SQL's NULL-distinct
58+
`UNIQUE` it constrains no row that has no organization, which on a
59+
single-tenant deployment is every row.
60+
- an index over a column no field materialises (a virtual `formula` field) —
61+
the same skip the driver performs, where the driver logs a warning.
62+
63+
Object-level `indexes[]` remains unemitted by both formats; it is normalized by
64+
a different driver-side rule and is not covered by this change.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
fix(client): `oauth.applications.register` declares only the members `/oauth2/create-client` accepts — `name`, `scopes` and `metadata` are removed (#15447)
6+
7+
**BREAKING** — three members leave a published request type. A caller who sets one compiles today and gets a type error after this release. That is the point: the route never honoured any of them, so what the compiler now refuses is code that was already having its value thrown away.
8+
9+
## What a caller passing these members should do instead
10+
11+
| you were passing | pass instead | why |
12+
|---|---|---|
13+
| `name: 'My App'` | `client_name: 'My App'` | same `string`, and `client_name` is the member the route reads |
14+
| `scopes: ['openid', 'profile']` | `scope: ['openid', 'profile'].join(' ')` | ⚠️ **not** a rename — `scope` is one space-delimited string; posting an array is refused with `400 [body.scope] Invalid input: expected string, received array` |
15+
| `metadata: { tenant: 'acme' }` | nothing — delete the member | no door this SDK can reach accepts it (see below) |
16+
17+
## ⚠️ These were the vendor's RECORD vocabulary, not typos
18+
19+
`client_name` writes the DB column literally named **`name`**; `scope` writes the DB column literally named **`scopes`**, as a JSON array. The removed members were the *column* names offered next to the *wire* names in the same declared type — an author picking the adjacent one of two got a success receipt and no value. Treating them as misspellings would be the wrong reading of what they were; the prescription above is still the wire member either way.
20+
21+
## Why they had to go rather than be honoured here
22+
23+
`POST /api/v1/auth/oauth2/create-client` is mounted verbatim from `@better-auth/oauth-provider@1.7.2`. Its body schema declares 21 members and sets no `catchall`, so it is zod's default **strip**: an unknown key is dropped, not refused, and the caller gets **HTTP 201 and a client that quietly does not have the value**. Driven end to end against a real `betterAuth` + `oauthProvider` over a real ObjectQL engine on a real socket, through this client: each of the three came back absent from the response, absent from `oauth.applications.get`, absent from `oauth.applications.list`, and `null` in the `sys_oauth_application` row.
24+
25+
A second, independent barrier stands behind that strip — the handler funnels the parsed remainder into the opaque-metadata envelope, and all three names sit in `OPAQUE_METADATA_RESERVED_FIELDS` — so no amount of loosening on the SDK side could ever have made them arrive. `metadata` in particular is honoured only by `PATCH /admin/oauth2/update-client`, which is `SERVER_ONLY` and therefore not an HTTP route at all: over the wire it answers 404 with a zero-byte body.
26+
27+
Nothing else on the method moves. The two members the route does honour, `client_name` and `scope`, are declared exactly as before and still reach the server byte for byte; the method's return type, its URL and its request-building step are unchanged.
28+
29+
Graded `minor` rather than `patch` because a published package's public surface moves, per the maintainer's ruling of 2026-09-04 (decision batch #35) that such a change takes at least `minor`; the banner above carries the breaking-ness the level cannot.
30+
31+
<!-- adr-0087: not-required (no-migration-prescription) Claimed on a POSITIVE argument rather than on the detector finding nothing. Stated plainly: the table above IS a prescription, and it is addressed to a TYPESCRIPT CONSUMER at their own call site, delivered by the compiler — the audience ADR-0087's D8 addendum says the ledger explicitly does not serve. Nothing authorable moves: no spec key, no Zod schema, no object definition, no config field and no stored representation changes spelling or shape, so `objectstack migrate meta` has nothing to visit, `spec-changes.json` has nothing to project and the upgrade guide has no row to gain. Minting a ledger id here would put a prescription in the one ledger this gate keeps true that none of its three consumers can project. The other four categories are closed on facts: `@objectstack/client` publishes to npm (not `unpublished`); no id pre-dates the base (not `already-registered`); no named symbol is a non-metadata runtime interface whose members moved (not `runtime-interface-only`); and `type-surface-only` fails its predicate 4 (`narrowed-from-erased`), because the request type was concretely declared at the merge base rather than `any` — this narrowing removes members from a concrete type instead of replacing an erased one. ⚠️ Residual declared rather than hidden: the vocabulary still has no category for a source-author prescription the ledger must not carry, which is D8's blind spot reached from a third direction; raised for the maintainer in the PR report rather than resolved by dropping the BREAKING banner. -->

0 commit comments

Comments
 (0)