Skip to content

Commit d047787

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15207-platform-setting-global-rung
2 parents 6d5cd47 + ef1fcb2 commit d047787

82 files changed

Lines changed: 1028 additions & 392 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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/platform-objects': patch
5+
'@objectstack/metadata-core': patch
6+
'@objectstack/metadata-protocol': patch
7+
'@objectstack/plugin-security': patch
8+
'@objectstack/plugin-auth': patch
9+
'@objectstack/plugin-sharing': patch
10+
'@objectstack/service-messaging': patch
11+
'@objectstack/service-automation': patch
12+
'@objectstack/service-realtime': patch
13+
---
14+
15+
A declared index states its uniqueness scope: bare `unique: true` on `indexes[]` is refused (protocol 18, ADR-0120 D1/D7), stored metadata converts it to `unique: 'global'` with zero drift, and `VISIBILITY_STRICT_OPTIONS` leaves `@objectstack/spec`'s public surface.
16+
17+
Clause-②: no (narrowing)
18+
19+
<!-- adr-0087: registered declared-index-bare-unique-true-retired, visibility-strict-options-unexported -->
20+
21+
**BREAKING**: an accept-set narrowing on a published authoring surface and one export removal, shipped as `minor` under the launch-window convention for accept-set narrowings (Changesets pre mode is not in on `main`).
22+
23+
**Why.** On a declared index, bare `unique: true` was the one `unique` spelling whose scope was positional. It built the index over exactly `fields`, one holder across the whole installation, while reading like "unique per organization" to an author who knew the field-level meaning. 17.x warned (lint `unique/unscoped-declared-index`). Protocol 18 refuses it, so the scope is always stated.
24+
25+
**What is refused.** A declared index (`objects[].indexes[]`, `objectExtensions[].indexes[]`) whose `unique` is bare `true`. The refusal names both replacements, and says which one keeps the index bare `true` built. It is raised by:
26+
27+
- the schema (`IndexSchema.unique`, now `false | 'global' | 'organization'`), at every door that parses: `ObjectSchema.create()` and `ObjectSchema.parse()`, `defineStack`, `os validate`, `os build`, and the runtime save door (`422 INVALID_METADATA`). `tsc` refuses it too, because the input type no longer admits `true`;
28+
- lint `unique/unscoped-declared-index`, now an `error` and a gating rule on all three commands. It is what refuses the spelling under `os lint`, which never parses.
29+
30+
**What converts.** The protocol-18 ADR-0087 conversion `declared-index-unique-scope` rewrites a declared index's bare `true` to `'global'` on every data-at-rest seam: stored `sys_metadata` rows (`applyConversionsToStoredItem`), built artifacts inside their declared-floor window, and `os migrate meta --from 17`. `'global'` is exactly the index bare `true` built, so the physical index is byte-identical and the drift plan is empty. It is retired from the authoring funnel, so a live author is refused and taught instead of converted silently.
31+
32+
**What stays accepted.** Field-level `unique: true` still means one holder per organization and stays valid indefinitely. On a declared index, `unique: false` or omitted, `'global'` and `'organization'` parse exactly as before.
33+
34+
## FROM → TO
35+
36+
| you wrote | write instead |
37+
|:--|:--|
38+
| `indexes: [{ fields: [...], unique: true }]` | `indexes: [{ fields: [...], unique: 'global' }]`: the same index, nothing on disk changes |
39+
| …the same, when you meant one holder per organization | `unique: 'organization'`: the driver prepends the NULL-safe organization key part at registration, and `os migrate plan` shows the index change |
40+
| `import { VISIBILITY_STRICT_OPTIONS } from '@objectstack/spec/shared'` (or the root entry) | delete the import: it was an internal option bag for the spec's own visibility-carrying schemas, and those schemas are unchanged |
41+
42+
**The one-line fix: on every declared index, write `unique: 'global'` where you wrote `unique: true`.** Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.
43+
44+
**Who is affected, measured.** At `e67ba80049`, an AST census found 48 declared indexes with bare `unique: true` in 39 source files of this repository, all platform and plugin objects. Every one is respelled `'global'` in this change, and the nine-key S5 corpus is pinned to build byte-identical indexes before and after (`driver-sql`'s `sql-driver-unique-tenancy.test.ts`). `examples/**` and `apps/**` carry none. Deployed metadata and other repositories were not measured.
45+
46+
### The kit
47+
48+
- **The refusal.** `IndexSchema.unique` in `data/object.zod.ts`, with its own prescription. The lint rule moved from `warning` to `error` and from advisory to gating.
49+
- **The conversion.** `declared-index-unique-scope` (`toMajor: 18`, retired from the load path, `retiredAfter: '17.7.0'`), with its S4/S5 fixture.
50+
- **The ledger.** The D3 semantic entries `declared-index-bare-unique-true-retired` (the scope each respelled index really meant is the author's call) and `visibility-strict-options-unexported`, plus a step-18 rationale fragment.
51+
- **The export.** `VISIBILITY_STRICT_OPTIONS` moved to the unbarrelled `shared/visibility-strict-options.ts`, beside its type `StrictObjectOptions`. `check:api-surface` counts the removal.
52+
- **The pins.** The "`'global'` is a synonym of `true`" driver pin retired with the bare spelling. The verbatim pin is stated in `'global'`.

‎content/docs/data-modeling/indexing.mdx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,15 @@ Two rules make the choice safe to write and safe to deploy:
6767
valid indefinitely — `email: Field.email({ unique: true })` is correct, and the
6868
explicit spelling is simply preferred in new code.
6969

70-
On a **declared index** it is the deprecated spelling of `'global'`: it
71-
materializes over exactly the listed columns. Because that reads like the
72-
field-level meaning but does the opposite, `os lint` / `os build` / `os validate`
73-
report it as `unique/unscoped-declared-index`, and it is rejected outright at
74-
protocol 18. State the scope.
70+
On a **declared index** it is refused since protocol 18. It was the positional
71+
spelling of `'global'` — it materialized over exactly the listed columns —
72+
which reads like the field-level meaning but does the opposite. `os validate`,
73+
`os build` and the runtime save door refuse it at the schema, `os lint`
74+
reports it as `unique/unscoped-declared-index`, and every refusal names both
75+
replacements. State the scope: `'global'` builds exactly the index bare `true`
76+
built. Metadata already stored or built with the bare spelling converts to
77+
`'global'` when it loads — the same physical index, no migration — and
78+
`os migrate meta --from 17` lists the edits for your sources.
7579
</Callout>
7680

7781
<Callout type="warn">
@@ -105,7 +109,8 @@ constraint by listing the column itself:
105109

106110
```typescript
107111
// the legacy spelling — still valid, still materialized exactly as written
108-
indexes: [{ fields: ['name', 'organization_id'], unique: true }]
112+
// (bare `true` before protocol 18, which converts to 'global')
113+
indexes: [{ fields: ['name', 'organization_id'], unique: 'global' }]
109114
```
110115

111116
This keeps working forever and forces no migration, so `os lint` only *suggests*

‎content/docs/protocol/objectql/schema.mdx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,11 @@ indexes:
628628
unique: global
629629
```
630630

631-
Bare `unique: true` on a *declared index* is the deprecated spelling of
632-
`'global'` (materialized over exactly the listed columns). It is warned by
633-
`os lint` / `os build` / `os validate` as `unique/unscoped-declared-index` and
634-
rejected at protocol 18 — state the scope instead. A legacy index that lists the
631+
Bare `unique: true` on a *declared index* is refused since protocol 18. It was
632+
the positional spelling of `'global'` (materialized over exactly the listed
633+
columns); `os validate` / `os build` refuse it at the schema and `os lint`
634+
reports it as `unique/unscoped-declared-index` — state the scope instead.
635+
Stored metadata carrying it converts to `'global'`, the same physical index. A legacy index that lists the
635636
organization column itself (`fields: [organization_id, code]`) keeps working
636637
unchanged; respelling its scope to `'organization'` makes the listed column
637638
NULL-safe in place.

‎content/docs/references/api/metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ Metadata query with filtering, sorting, and pagination
948948
| **datasource** | `string` | optional (default: `"default"`) | Target Datasource ID. "default" is the primary DB. |
949949
| **external** | `{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record<string, string>; … }` | optional | Remote table binding for federated (external) objects. |
950950
| **fields** | `Record<string, { name?: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; description?: string; … }>` | ✅ | Field definitions map. Keys must be snake_case identifiers; "__proto__", "constructor" and "prototype" are refused. |
951-
| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes |
951+
| **indexes** | `{ name?: string; fields: string[]; unique?: false \| 'global' \| 'organization' }[]` | optional | Database performance indexes |
952952
| **fieldGroups** | `{ key: string; label: string; icon?: string; description?: string; … }[]` | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. |
953953
| **tenancy** | `{ enabled: boolean; tenantField?: string }` | optional | Multi-tenancy configuration for SaaS applications |
954954
| **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). |

0 commit comments

Comments
 (0)