@abeedoo/radish-schemas 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -31,7 +31,7 @@ export {
31
31
  * @property {string} minCliVersion - Minimum radish-cli version compatible with this package
32
32
  */
33
33
  export const VERSIONING = {
34
- packageVersion: '1.11.0',
34
+ packageVersion: '1.12.0',
35
35
  currentSpecVersion: 1,
36
36
  supportedSpecVersions: [1],
37
37
  minCliVersion: '0.1.0'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abeedoo/radish-schemas",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Shared JSON schemas, validators, and prompts for Radish CLI ecosystem",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -127,19 +127,30 @@ The following entities are already provided by the system. DO NOT recreate them
127
127
  ```
128
128
  `matchUserField` supports array match — if `user.orgIds` is an array, checks if `record.orgId` is in that array.
129
129
 
130
- 9. **Tenancy** (optional): If the app is multi-tenant — every record belongs to an organization, workspace, clinic, or similar — mark that one entity as the boundary:
130
+ 9. **Tenancy** (optional): If the app is multi-tenant — every record belongs to an organization, workspace, clinic, or similar — mark that one entity with `"tenant": true`, then declare only the exceptions:
131
131
  ```json
132
132
  {
133
- "Org": { "plural": "orgs", "tenancy": "boundary", "fields": { ... } },
133
+ "Org": { "plural": "orgs", "tenant": true, "fields": { ... } },
134
134
  "User": { "plural": "users", "fields": { "orgIds": { "type": "objectId[]", "ref": "Org" } } },
135
- "Invoice": { "plural": "invoices", "fields": { "orgId": { "type": "objectId", "ref": "Org", "required": true } } }
135
+ "Invoice": { "plural": "invoices", "fields": { "orgId": { "type": "objectId", "ref": "Org", "required": true } } },
136
+ "Setting": { "plural": "settings", "tenancy": "optional", "fields": { "orgId": { "type": "objectId", "ref": "Org" } } },
137
+ "AuditLog": { "plural": "auditLogs", "tenancy": "none", "fields": { "orgId": { "type": "objectId", "ref": "Org" } } }
136
138
  }
137
139
  ```
138
- - **`"boundary"`**: at most one entity per blueprint. Every entity with a ref to it is automatically confined to the caller's active tenant — no per-entity declaration needed. `User` must have exactly one field ref-ing it (`objectId[]` if a user can belong to several tenants, `objectId` for exactly one); that field is discovered automatically as the membership field.
139
- - **`"global"`**: for an entity that refs the boundary entity but must NOT be confined by it — a tenant id recorded for provenance rather than ownership, such as an audit row naming the org an action occurred in that admins read across orgs. Rare; only needed when the ref exists.
140
- - **Omit entirely** for ordinary entities, including reference data with no ref to the boundary entity — those are global by construction, since there is nothing to partition on.
140
+ Two separate keys, because they answer questions about different entities:
141
141
 
142
- Do not guess at this. A tenant and a group look identical in a blueprint (an entity, a membership field, an id on the records they cover), but a tenant **confines** results while a group **widens** them. Mark `"boundary"` only when the app description actually describes tenant isolation; if it describes teams, clans, or groups that share access, leave `tenancy` off and model it with `scope` instead.
142
+ - **`"tenant": true`** — on the boundary entity itself, and at most one entity per blueprint. `true` is the only valid value; omit the key everywhere else. Nothing is partitioned until some entity declares it. Once declared, every entity with a ref to it is confined automatically — no per-entity declaration needed. `User` must have exactly one field ref-ing it (`objectId[]` if a user can belong to several tenants, `objectId` for exactly one); that field is discovered as the membership field.
143
+ - **`"tenancy"`** — on a *member* entity, to record an exception:
144
+ - **absent** (the default): strictly partitioned if it refs the boundary entity. This is the safe state — a forgotten key confines rather than exposes.
145
+ - **`"optional"`**: partitioned, but rows with NO tenant are visible to everyone — a shared default with per-tenant overrides. Use for settings, templates, categories, feature flags.
146
+ - **`"none"`**: never confined despite ref-ing the boundary — a tenant id kept for provenance rather than ownership, such as an audit row naming the org an action occurred in.
147
+ - **Omit both** on ordinary entities, including reference data with no ref to the boundary — those are global by construction, since there is nothing to partition on.
148
+
149
+ Do not guess at any of this:
150
+ - A tenant and a group look identical in a blueprint (an entity, a membership field, an id on the records they cover), but a tenant **confines** results while a group **widens** them. Set `"tenant": true` only when the description actually describes tenant isolation; if it describes teams, clans, or groups that share access, leave these keys off and model it with `scope` instead.
151
+ - Never infer `"optional"` from the ref field being non-required. A nullable tenant ref means "shared with everyone" in one app and "not backfilled yet, keep hidden" in another — identical shape, opposite intent. Declare `"optional"` only when the description says the rows are genuinely shared.
152
+
153
+ Note that `ownership` is a different axis: it says who owns rows (`"user"` or `"system"`), not what partitions them.
143
154
 
144
155
  10. **Enhanced Enums**: Use key-value pairs for better UX:
145
156
  ```json
@@ -55,13 +55,17 @@
55
55
  },
56
56
  "ownership": {
57
57
  "type": "string",
58
- "enum": ["user", "system", "tenant"],
58
+ "enum": ["user", "system"],
59
59
  "default": "user"
60
60
  },
61
+ "tenant": {
62
+ "const": true,
63
+ "description": "This entity is the tenancy boundary (Org, Workspace, Clinic). Nothing is partitioned until exactly one entity declares this. Every entity with a ref to it is then confined to the caller's tenants. Only true is valid — omit the key on every other entity. Unrelated to ownership, which says who owns rows rather than what partitions them."
64
+ },
61
65
  "tenancy": {
62
66
  "type": "string",
63
- "enum": ["boundary", "global"],
64
- "description": "\"boundary\": this entity is the tenancy boundary (Org, Workspace, Clinic) — every entity with a ref to it is confined to the caller's active tenant. \"global\": this entity has a ref to the boundary entity but is NOT confined by it (a tenant id kept for provenance, not ownership). Absent: ordinary — confined if it refs the boundary entity, unconstrained if it does not."
67
+ "enum": ["optional", "none"],
68
+ "description": "How tenancy applies to THIS entity. Absent (the default): strictly partitioned, if it refs the boundary entity. \"optional\": partitioned, but rows with NO tenant are visible to everyone — a shared default with per-tenant overrides. \"none\": never confined, even though it refs the boundary (a tenant id kept for provenance, not ownership)."
65
69
  },
66
70
  "extends": {
67
71
  "type": "string",