You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 7f0e0e6
Browse filesBrowse the repository at this point in the historyBrowse files
Merge origin/main (#25299) into claude/project-thread-y1fvs4
Keeps both refused-shape describes in refused-shapes.test.ts. The
has() argument detector reads the stack alone and ignores the context
parameter #25279 added.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jc59n5NWaMp1khDwL7Fici
fix(spec)!: the published list-query schemas declare the page window the Data API enforces — a non-negative integer — instead of any number
7
+
8
+
Clause-②: no (narrowing)
9
+
Metadata-protocol: none
10
+
11
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no metadata type, authorable metadata key, object definition or stored shape is added, removed, renamed or re-shaped. What narrows is two request schemas a Data API caller is validated against (`FindDataRequestSchema`, through `QueryWithTransportSchema` and `QueryTransportParamsSchema`, and `HttpFindQueryParamsSchema`), and every value they now refuse was already refused by the list doors since the page-window normalizer landed. There is nothing for `objectstack migrate meta` to rewrite; the remedy is in the request (send a whole count, or leave the parameter out). -->
12
+
13
+
**BREAKING** accept-set narrowing on two published request schemas. It changes what a schema-validated Data API request may carry, not what a metadata author can write, so it ships as `patch` with this banner.
14
+
15
+
-**Before.**`FindDataRequestSchema` (the `POST /api/v1/data/:object/query` body) declared `limit` / `offset` / `top` as any number and `$top` / `$skip` / `skip` as any number or string, and `HttpFindQueryParamsSchema` (the `GET /api/v1/data/:object` querystring) declared `top` / `skip` as `z.coerce.number()`. A client generated from them, or a request validated against them, was told `-1`, `1.5` or a blank `?top=` was valid, and the door then refused it with a `400`.
16
+
-**Now.** Each of those parameters declares the page window the door enforces: a non-negative integer, or on the string-carrying spellings the digits a querystring carries it as (`'50'`). `-1`, `1.5`, `NaN`, `Infinity`, a blank or whitespace-only string, and a string that is not a count fail the parse. The bound is exported once, as `QueryPageWindowSchema` / `QueryPageWindowParamSchema` (`@objectstack/spec/data`), and the list-query normalizer in `@objectstack/metadata-protocol` now judges every window by that same schema, so the two accept sets are one declaration.
17
+
-**One answer moves, and only its code.** On `POST /api/v1/data/:object/query` (and the runtime dispatcher's `POST` twin) a malformed window is now refused by the body schema, like every other malformed body: `400 VALIDATION_FAILED` with `fields[0].field` naming `query.limit` / `query.offset`, where it was `400 INVALID_REQUEST` from the normalizer. Still a `400`, still before any read. `GET /api/v1/data/:object` has no body schema and answers `400 INVALID_REQUEST` naming the parameter as before.
18
+
-**Unchanged.** A well-formed window parses and pages exactly as before, `0` included (`$top=0` asks for the total alone). Windows inside `expand` and the engine and driver contracts (`QuerySchema`, `EngineQueryOptionsSchema`) are not narrowed: no door refuses those.
19
+
20
+
**The remedy.** Send the window as a whole count of zero or more, or leave the parameter out.
fix(plugin-auth)!: under `single`, no slug change makes another organization the Default Organization — re-slugging to `default` captured the platform-admin anchor
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: none
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. What narrows is a runtime write: a `sys_organization` slug changed to `default` under `single`. -->
11
+
12
+
**BREAKING** for a `single` deployment that holds several organizations, none slugged `default`, and re-slugs one of them to `default`. Under `single` the Default Organization is the organization slugged `default`, else the only one, and its `admin_full_access` grant confers platform administrator standing (ADR-0131 D5). A re-slug to `default` therefore made that organization the Default Organization, and every holder of a grant it owned a platform administrator: an organization owner could do it through better-auth's `POST /auth/organization/update`, and a seed or a plugin through an engine update.
13
+
14
+
-**FROM:** a slug changed to `default` on any organization was admitted, as long as the last-administrator guard found an administrator left standing. **TO:** under `single` it is admitted only on the organization that is already the Default Organization (the sole organization taking the slug the boot gives it). Any other organization is refused `403 PERMISSION_DENIED` by the last-administrator guard's `sys_organization``beforeUpdate` hook, for every engine write, `isSystem` included, so better-auth's `organization/update` answers it too. A read the guard cannot complete refuses as well.
15
+
-**Removed, with no in-place replacement: moving the Default Organization.** FROM: with several organizations, an operator could move `default` from one to another in two writes (the current one re-slugged away, then another re-slugged to `default`). TO: the second write is refused, because it is exactly the capture above, whoever makes it. An atomic swap was never possible (the slug is unique).
16
+
-**Stranding.** Re-slugging the Default Organization AWAY while other organizations exist is still admitted when an administrator stays standing (the last-administrator guard's update half, unchanged). After it, no organization names the Default Organization, so no organization can take `default`, the previous holder included, until the store holds one organization again or declares a wall. Do not re-slug a Default Organization away on a `single` deployment that holds several organizations.
17
+
-**Unchanged:** a slug changed to any other value; every slug change under a walled posture (`isolated`, `group`), where no grant anchors and a slug is just a slug; the data API, whose user-context PATCH and update-mode import were already refused a `slug` by the ADR-0092 identity write guard.
18
+
19
+
**What an operator does.** Keep another slug. To move the anchor, move the grant rather than the slug: an `admin_full_access` grant re-owned by the Default Organization anchors, and a grant owned elsewhere does not. To name a Default Organization on a `single` deployment that holds several organizations, hold one organization (merge the others into it; the sole organization is the Default Organization and may then take the slug `default`), or declare a walled posture (`OS_TENANCY_POSTURE=group` or `isolated`), as the tenancy organization census already prescribes.
fix(spec): `os migrate meta --stored` lists a stored object bound `external` to a stored or registered managed datasource
7
+
8
+
Clause-②: yes (widening)
9
+
10
+
Metadata-protocol: none
11
+
12
+
The stored-shape listing for `object-external-binding-managed-datasource-refused` resolved an object's named datasource only from the stack being converted. A stored object row reaches the conversion chain alone, so the listing judged only a binding on the primary datasource (`'default'`), while the object save door also refuses one on a datasource stored or registered beside the object. The stored-metadata pass therefore did not list a stored object row bound `external` to a managed datasource, the row whose next save is refused with `422 INVALID_METADATA`.
13
+
14
+
-**Now:** for each stored `object` row carrying `external` and naming a datasource, the pass reads that datasource the way the object save door does (the environment's stored row, else the definition the code layer registers; both go through one read) and hands it to the chain. The row is listed under `rows[].todos` with this entry's id exactly when that door refuses its next save. A datasource declared `schemaMode: 'external'` or `'validate-only'`, and a datasource no layer holds, give no listing, as at the door. Nothing is rewritten.
15
+
-**`@objectstack/spec`:**`ApplyConversionsOptions` (and so `StoredConversionOptions` for `applyConversionsToStoredItem`) gains an optional `knownDatasources`, a map of datasource definitions by name that the caller's door can see beyond the stack. Only the refused-shape listing reads it, so only when an `onTodo` sink is passed. A datasource the stack declares is read from the stack first. Without it, behaviour is unchanged.
Copy file name to clipboardExpand all lines: content/docs/references/api/protocol.mdx
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -904,9 +904,9 @@ Enable package response
904
904
|**search**|`string \| { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' \| 'or'>; … }`| optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
905
905
|**searchFields**|`string[]`| optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
|**limit**|`number`| optional | Max records to return (LIMIT) |
908
-
|**offset**|`number`| optional | Records to skip (OFFSET) |
909
-
|**top**|`number`| optional | Alias for limit (OData compatibility) |
907
+
|**limit**|`integer`| optional | Max records to return (LIMIT) |
908
+
|**offset**|`integer`| optional | Records to skip (OFFSET) |
909
+
|**top**|`integer`| optional | Alias for limit (OData compatibility) |
910
910
|**cursor**|`never`| optional |[REMOVED]`query.cursor` was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
911
911
|**joins**|`never`| optional |[REMOVED]`query.joins` was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner_id: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (`fields: ['title', 'owner_id']`): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted `fields` path is NOT a replacement — no driver ever resolved one and the ingress refuses it (`400 INVALID_FIELD`). |
|**expand**|`Record<string, { object: string; fields?: string[]; where?: any; search?: string \| object; … }>`| optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |
918
918
|**$filter**|`[string, string, any] \| [string, string] \| [string, object, ...object[]] \| object[] \| Record<string, any> \| any \| null`| optional | Transport spelling of `where` (OData `$filter`) |
919
919
|**filters**|`[string, string, any] \| [string, string] \| [string, object, ...object[]] \| object[] \| Record<string, any> \| any \| null`| optional | Transport spelling of `where` (plural of `filter`) |
920
-
|**$top**|`number \| string \| null`| optional | Transport spelling of `limit` (OData `$top`) — a number, or the digits a querystring carries it as |
921
-
|**$skip**|`number \| string \| null`| optional | Transport spelling of `offset` (OData `$skip`) — a number, or the digits a querystring carries it as |
920
+
|**$top**|`integer \| string \| null`| optional | Transport spelling of `limit` (OData `$top`) — a non-negative integer, or the digits a querystring carries it as |
921
+
|**$skip**|`integer \| string \| null`| optional | Transport spelling of `offset` (OData `$skip`) — a non-negative integer, or the digits a querystring carries it as |
|**orderBy**|`string`| optional | Alias for sort (OData compatibility). |
2058
-
|**top**|`number`| optional | Max records to return (limit). |
2059
-
|**skip**|`number`| optional | Records to skip (offset). |
2058
+
|**top**|`integer \| string`| optional | Max records to return (limit) — a non-negative integer. |
2059
+
|**skip**|`integer \| string`| optional | Records to skip (offset) — a non-negative integer. |
2060
2060
|**expand**|`string`| optional | Comma-separated list of lookup/master_detail field names to expand. Resolved to populate array and passed to the engine for batch $in expansion. |
|**distinct**|`never`| optional |[REMOVED]`query.distinct` was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. |
0 commit comments