@abloatai/ablo 0.12.0 → 0.14.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/AGENTS.md +2 -2
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/dist/BaseSyncedStore.js +39 -32
- package/dist/batching/index.d.ts +57 -0
- package/dist/batching/index.js +150 -0
- package/dist/cli.cjs +158 -40
- package/dist/client/Ablo.d.ts +16 -25
- package/dist/client/Ablo.js +1 -1
- package/dist/client/auth.js +11 -0
- package/dist/client/createModelProxy.d.ts +33 -8
- package/dist/client/createModelProxy.js +4 -4
- package/dist/errorCodes.d.ts +3 -1
- package/dist/errorCodes.js +10 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +38 -84
- package/dist/schema/model.js +12 -12
- package/dist/schema/roles.d.ts +49 -0
- package/dist/schema/roles.js +21 -0
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/serialize.d.ts +4 -2
- package/dist/schema/serialize.js +4 -2
- package/dist/schema/sugar.d.ts +7 -28
- package/dist/schema/sugar.js +2 -7
- package/dist/schema/sync-delta-row.d.ts +2 -0
- package/dist/schema/sync-delta-row.js +2 -1
- package/dist/schema/tenancy.d.ts +67 -28
- package/dist/schema/tenancy.js +93 -23
- package/dist/server/commit.d.ts +8 -3
- package/docs/api.md +7 -6
- package/docs/cli.md +43 -4
- package/docs/client-behavior.md +2 -2
- package/docs/coordination.md +12 -12
- package/docs/examples/agent-human.md +6 -6
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +4 -4
- package/docs/identity.md +27 -20
- package/docs/index.md +0 -1
- package/docs/integration-guide.md +12 -9
- package/docs/interaction-model.md +1 -1
- package/docs/mcp.md +17 -5
- package/docs/quickstart.md +3 -3
- package/docs/react.md +69 -0
- package/llms.txt +2 -3
- package/package.json +8 -2
- package/docs/mcp/claude-code.md +0 -35
- package/docs/mcp/cursor.md +0 -35
- package/docs/mcp/windsurf.md +0 -33
- package/docs/roadmap.md +0 -55
- package/docs/the-loop.md +0 -21
- package/llms-full.txt +0 -396
package/dist/schema/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export { field, indexed, getFieldMeta } from './field.js';
|
|
|
27
27
|
// Relation builders
|
|
28
28
|
export { relation } from './relation.js';
|
|
29
29
|
// Tenancy — the single source of truth for how a model's rows are tenant-scoped.
|
|
30
|
-
export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
30
|
+
export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
31
31
|
// Database plane — which DB a model's rows live in (`tenant` portable to a BYO
|
|
32
32
|
// customer DB, `control` = Ablo's own). Sibling axis to `tenancy`.
|
|
33
33
|
export { planeSchema, DEFAULT_PLANE } from './plane.js';
|
|
@@ -46,7 +46,7 @@ export { model, scopeKindOf, } from './model.js';
|
|
|
46
46
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
47
47
|
export { mutable, readOnly } from './sugar.js';
|
|
48
48
|
// Schema definition + type inference
|
|
49
|
-
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
49
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
|
|
50
50
|
// Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
|
|
51
51
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
52
52
|
// Schema projection — derive an app's subset from one canonical schema.
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { z } from 'zod';
|
|
20
20
|
import type { RelationDef } from './relation.js';
|
|
21
|
-
import type { EntityRole } from './roles.js';
|
|
21
|
+
import type { EntityRole, GroupsInput } from './roles.js';
|
|
22
22
|
import { type FieldMeta } from './field.js';
|
|
23
|
-
import { type Tenancy, type
|
|
24
|
-
export type { ScopedViaRef, Tenancy } from './tenancy.js';
|
|
23
|
+
import { type Tenancy, type PolicyInput } from './tenancy.js';
|
|
24
|
+
export type { ScopedViaRef, Tenancy, PolicyInput } from './tenancy.js';
|
|
25
25
|
import { type SchemaPlane } from './plane.js';
|
|
26
26
|
/**
|
|
27
27
|
* Controls when model data is loaded from the server.
|
|
@@ -95,49 +95,27 @@ export interface ModelOptions {
|
|
|
95
95
|
*/
|
|
96
96
|
tableName?: string;
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* tenant
|
|
98
|
+
* **Axis 1 — row-access policy (tenant isolation / RLS).** Decides who may
|
|
99
|
+
* *read* a row at all. Named after Postgres/Supabase, where a `policy` is the
|
|
100
|
+
* rule that scopes which rows a tenant sees. A discriminated union on `by` —
|
|
101
|
+
* one option replacing the old `orgScoped`/`scopedVia`/`orgColumn` trio:
|
|
101
102
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* `teamMember.team_id → team.id`, `users.id ← member.user_id`).
|
|
115
|
-
*
|
|
116
|
-
* Emits, in place of the missing `organization_id = $1` clause:
|
|
117
|
-
*
|
|
118
|
-
* WHERE <table>.<localKey> IN
|
|
119
|
-
* (SELECT <parentKey> FROM <parentTable> WHERE <parentOrgColumn> = $1)
|
|
120
|
-
*
|
|
121
|
-
* Use this INSTEAD of `orgScoped: false` for any `load: 'instant'`
|
|
122
|
-
* model whose rows would otherwise leak cross-tenant on bootstrap —
|
|
123
|
-
* dropping the filter entirely exposes the entire DB to every client.
|
|
103
|
+
* - `{ by: 'column' }` — row-local tenancy column (the DEFAULT when omitted).
|
|
104
|
+
* `column` overrides the name (default `organization_id`).
|
|
105
|
+
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key
|
|
106
|
+
* when THIS table has no tenancy column of its own (e.g. `slide_layers` →
|
|
107
|
+
* slide → deck → org). Emits, in place of `organization_id = $1`:
|
|
108
|
+
* `WHERE <table>.<fk> IN (SELECT <parentKey> FROM <parent> WHERE
|
|
109
|
+
* <parentTenantColumn> = $1)`. Use this for any `load: 'instant'` child
|
|
110
|
+
* table that would otherwise leak cross-tenant on bootstrap.
|
|
111
|
+
* - `{ by: 'none' }` — genuinely global / reference data (the `organizations`
|
|
112
|
+
* table itself, global lookups). ⚠ Makes the whole table readable
|
|
113
|
+
* cross-tenant — only correct for tenant-less tables. Because it's an
|
|
114
|
+
* explicit, named branch (not a falsy flag) it can't be reached by accident.
|
|
124
115
|
*
|
|
125
|
-
*
|
|
126
|
-
* the SQL compiler validates them to keep this away from injection
|
|
127
|
-
* paths.
|
|
116
|
+
* Normalized into the canonical {@link Tenancy} by `resolvePolicy` at build.
|
|
128
117
|
*/
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Override the physical tenancy column name (default `organization_id`).
|
|
132
|
-
* Authoring sugar — normalized into the canonical {@link tenancy} descriptor.
|
|
133
|
-
*/
|
|
134
|
-
orgColumn?: string;
|
|
135
|
-
/**
|
|
136
|
-
* Canonical tenancy descriptor. You normally don't set this — `orgScoped`,
|
|
137
|
-
* `scopedVia`, and `orgColumn` are normalized into it at build time. Set it
|
|
138
|
-
* directly only to author the union form explicitly.
|
|
139
|
-
*/
|
|
140
|
-
tenancy?: Tenancy;
|
|
118
|
+
policy?: PolicyInput;
|
|
141
119
|
/**
|
|
142
120
|
* Which database plane this model's rows live in. `tenant` (default) =
|
|
143
121
|
* the tenant data plane, emitted into a customer's BYO/dedicated DB by
|
|
@@ -146,53 +124,29 @@ export interface ModelOptions {
|
|
|
146
124
|
*/
|
|
147
125
|
plane?: SchemaPlane;
|
|
148
126
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* string to override the kind explicitly (`scope: 'matter'`).
|
|
153
|
-
*
|
|
154
|
-
* Replaces the old `syncGroupFormat` template string: there is no `{id}`
|
|
155
|
-
* placeholder to author — the engine mints the branded {@link SyncGroup} from
|
|
156
|
-
* `(kind, id)`. Child models inherit a root's group via their `belongsTo`
|
|
157
|
-
* relations (a `document` with `dataroomId` fans into `dataroom:<id>`), so
|
|
158
|
-
* only the root declares anything.
|
|
159
|
-
*/
|
|
160
|
-
scope?: boolean | string;
|
|
161
|
-
/**
|
|
162
|
-
* Declares this model as a **membership edge** that grants an identity access
|
|
163
|
-
* to a scope root — the relation-driven equivalent of "this user can see that
|
|
164
|
-
* dataroom." Both values are *relation names* already declared on this model:
|
|
165
|
-
* `subject` is the `belongsTo` to the identity (e.g. a `user`), `scope` is the
|
|
166
|
-
* `belongsTo` to the scope-root entity (e.g. a `dataroom`).
|
|
127
|
+
* **Axis 2 — sync-group routing.** Decides which delta *channels* a row fans
|
|
128
|
+
* into. Orthogonal to {@link policy} (read access). One namespaced object
|
|
129
|
+
* replacing the old flat `scope`/`grants`/`entityRoles`:
|
|
167
130
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
131
|
+
* - `root` — mark this model a scope root; its records form the group
|
|
132
|
+
* `<kind>:<id>` (kind defaults from the lowercased typename, e.g. `Deck` →
|
|
133
|
+
* `deck:<id>`; pass a string to override, `root: 'matter'`). Child models
|
|
134
|
+
* inherit a root's group via their `belongsTo` relations. Was `scope` —
|
|
135
|
+
* renamed so it no longer collides with the old `scopedVia` tenancy sugar.
|
|
136
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
137
|
+
* Both values name `belongsTo` relations on this model (`subject` → identity,
|
|
138
|
+
* `scope` → scope root). Only needed for sub-org sharing.
|
|
139
|
+
* - `roles` — explicit non-relational record→group roles (the inbox-fan-out
|
|
140
|
+
* escape hatch, keyed on a plain field). Was `entityRoles`. One or many.
|
|
171
141
|
*
|
|
172
142
|
* ```ts
|
|
173
143
|
* // dataroomMember: { userId, dataroomId }
|
|
174
|
-
* grants: { subject: 'user', scope: 'dataroom' }
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Not needed for org-level access — a `dataroom.organizationId` already routes
|
|
178
|
-
* via the `org:<id>` group. `grants` is only for sub-org membership.
|
|
179
|
-
*/
|
|
180
|
-
grants?: GrantsRef;
|
|
181
|
-
/**
|
|
182
|
-
* Explicit record→group roles for routing that isn't relational — a group
|
|
183
|
-
* keyed on a plain field rather than the record's own id or a `belongsTo`
|
|
184
|
-
* scope root. The escape hatch for cases like per-recipient inbox fan-out:
|
|
185
|
-
*
|
|
186
|
-
* ```ts
|
|
187
|
-
* // a message routes to its addressee's inbox, keyed on `toId`
|
|
188
|
-
* entityRoles: [entityRole({ kind: 'inbox', source: 'toId' })]
|
|
144
|
+
* groups: { grants: { subject: 'user', scope: 'dataroom' } }
|
|
145
|
+
* // a message → its addressee's inbox, keyed on `toId`
|
|
146
|
+
* groups: { roles: [entityRole({ kind: 'inbox', source: 'toId' })] }
|
|
189
147
|
* ```
|
|
190
|
-
*
|
|
191
|
-
* Prefer {@link scope} (self group) + `belongsTo` relations (parent groups)
|
|
192
|
-
* when the routing follows the relation graph — reach for `entityRoles` only
|
|
193
|
-
* when it genuinely doesn't.
|
|
194
148
|
*/
|
|
195
|
-
|
|
149
|
+
groups?: GroupsInput;
|
|
196
150
|
/**
|
|
197
151
|
* Whether clients may issue CREATE/UPDATE/DELETE mutations for this
|
|
198
152
|
* model via the `commit` wire protocol. Default: **true** — declaring a
|
package/dist/schema/model.js
CHANGED
|
@@ -20,8 +20,10 @@ import { z } from 'zod';
|
|
|
20
20
|
import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
|
|
21
21
|
// Tenancy is owned by `tenancy.ts` (single source of truth). `ScopedViaRef` is
|
|
22
22
|
// re-exported so existing `import { ScopedViaRef } from './model'` call sites
|
|
23
|
-
// keep resolving
|
|
24
|
-
|
|
23
|
+
// keep resolving. Authoring uses the `policy` option (`PolicyInput`, named for
|
|
24
|
+
// Postgres/Supabase RLS), normalized to the canonical `Tenancy` by
|
|
25
|
+
// `resolvePolicy` at build time.
|
|
26
|
+
import { resolvePolicy } from './tenancy.js';
|
|
25
27
|
import { DEFAULT_PLANE } from './plane.js';
|
|
26
28
|
/** Normalize the `entityRoles` option (single | array | undefined) to an array. */
|
|
27
29
|
function normalizeEntityRoles(input) {
|
|
@@ -88,17 +90,15 @@ export function model(shape, relations, options) {
|
|
|
88
90
|
typename: options?.typename,
|
|
89
91
|
persist: options?.persist,
|
|
90
92
|
tableName: options?.tableName,
|
|
91
|
-
//
|
|
92
|
-
tenancy
|
|
93
|
-
|
|
94
|
-
orgScoped: options?.orgScoped,
|
|
95
|
-
scopedVia: options?.scopedVia,
|
|
96
|
-
orgColumn: options?.orgColumn,
|
|
97
|
-
}),
|
|
93
|
+
// Axis 1 — normalize the `policy` authoring option into the one canonical
|
|
94
|
+
// tenancy descriptor (defaults to a row-local org column).
|
|
95
|
+
tenancy: resolvePolicy(options?.policy),
|
|
98
96
|
plane: options?.plane ?? DEFAULT_PLANE,
|
|
99
|
-
|
|
100
|
-
grants
|
|
101
|
-
|
|
97
|
+
// Axis 2 — unpack the `groups` routing namespace into the wire fields the
|
|
98
|
+
// server reads (`scope`/`grants`/`entityRoles` on ModelDef/ModelJSON).
|
|
99
|
+
scope: options?.groups?.root,
|
|
100
|
+
grants: options?.groups?.grants,
|
|
101
|
+
entityRoles: normalizeEntityRoles(options?.groups?.roles),
|
|
102
102
|
mutable: options?.mutable ?? true,
|
|
103
103
|
lazyObservable: options?.lazyObservable,
|
|
104
104
|
computed: options?.computed,
|
package/dist/schema/roles.d.ts
CHANGED
|
@@ -137,6 +137,55 @@ export declare const grantsRefSchema: z.ZodObject<{
|
|
|
137
137
|
subject: z.ZodString;
|
|
138
138
|
scope: z.ZodString;
|
|
139
139
|
}, z.core.$strip>;
|
|
140
|
+
/**
|
|
141
|
+
* The AUTHORING form of a model's sync-group routing — the `groups: { ... }`
|
|
142
|
+
* option. One namespaced object collects the three independent routing knobs
|
|
143
|
+
* that used to be flat, collision-prone model options (`scope` / `grants` /
|
|
144
|
+
* `entityRoles`). Distinct axis from `policy` (tenant isolation): the policy
|
|
145
|
+
* decides who may *read* a row, `groups` decides which delta *channels* a row
|
|
146
|
+
* fans into.
|
|
147
|
+
*
|
|
148
|
+
* - `root` — mark this model a scope root; its records form `<kind>:<id>`
|
|
149
|
+
* (kind defaults from typename). Was the flat `scope` option — renamed to
|
|
150
|
+
* `root` so it no longer collides with the (now removed) `scopedVia` tenancy
|
|
151
|
+
* sugar or the inner `grants.scope` relation name.
|
|
152
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
153
|
+
* - `roles` — explicit non-relational record→group roles (e.g. inbox fan-out
|
|
154
|
+
* keyed on a plain field). Was `entityRoles`. Accepts one role or an array.
|
|
155
|
+
*/
|
|
156
|
+
export declare const groupsInputSchema: z.ZodObject<{
|
|
157
|
+
root: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
|
|
158
|
+
grants: z.ZodOptional<z.ZodObject<{
|
|
159
|
+
subject: z.ZodString;
|
|
160
|
+
scope: z.ZodString;
|
|
161
|
+
}, z.core.$strip>>;
|
|
162
|
+
roles: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<{
|
|
163
|
+
kind: string;
|
|
164
|
+
source: {
|
|
165
|
+
field: string;
|
|
166
|
+
multi: boolean;
|
|
167
|
+
};
|
|
168
|
+
}, unknown, z.core.$ZodTypeInternals<{
|
|
169
|
+
kind: string;
|
|
170
|
+
source: {
|
|
171
|
+
field: string;
|
|
172
|
+
multi: boolean;
|
|
173
|
+
};
|
|
174
|
+
}, unknown>>, z.ZodArray<z.ZodType<{
|
|
175
|
+
kind: string;
|
|
176
|
+
source: {
|
|
177
|
+
field: string;
|
|
178
|
+
multi: boolean;
|
|
179
|
+
};
|
|
180
|
+
}, unknown, z.core.$ZodTypeInternals<{
|
|
181
|
+
kind: string;
|
|
182
|
+
source: {
|
|
183
|
+
field: string;
|
|
184
|
+
multi: boolean;
|
|
185
|
+
};
|
|
186
|
+
}, unknown>>>]>>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
export type GroupsInput = z.infer<typeof groupsInputSchema>;
|
|
140
189
|
/** Build an identity-anchored role. `multi` defaults to `false`. */
|
|
141
190
|
export declare function identityRole(spec: {
|
|
142
191
|
readonly kind: string;
|
package/dist/schema/roles.js
CHANGED
|
@@ -112,6 +112,27 @@ export const grantsRefSchema = z.object({
|
|
|
112
112
|
subject: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, 'grants.subject must name a relation'),
|
|
113
113
|
scope: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, 'grants.scope must name a relation'),
|
|
114
114
|
});
|
|
115
|
+
/**
|
|
116
|
+
* The AUTHORING form of a model's sync-group routing — the `groups: { ... }`
|
|
117
|
+
* option. One namespaced object collects the three independent routing knobs
|
|
118
|
+
* that used to be flat, collision-prone model options (`scope` / `grants` /
|
|
119
|
+
* `entityRoles`). Distinct axis from `policy` (tenant isolation): the policy
|
|
120
|
+
* decides who may *read* a row, `groups` decides which delta *channels* a row
|
|
121
|
+
* fans into.
|
|
122
|
+
*
|
|
123
|
+
* - `root` — mark this model a scope root; its records form `<kind>:<id>`
|
|
124
|
+
* (kind defaults from typename). Was the flat `scope` option — renamed to
|
|
125
|
+
* `root` so it no longer collides with the (now removed) `scopedVia` tenancy
|
|
126
|
+
* sugar or the inner `grants.scope` relation name.
|
|
127
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
128
|
+
* - `roles` — explicit non-relational record→group roles (e.g. inbox fan-out
|
|
129
|
+
* keyed on a plain field). Was `entityRoles`. Accepts one role or an array.
|
|
130
|
+
*/
|
|
131
|
+
export const groupsInputSchema = z.object({
|
|
132
|
+
root: scopeSchema.optional(),
|
|
133
|
+
grants: grantsRefSchema.optional(),
|
|
134
|
+
roles: z.union([entityRoleSchema, z.array(entityRoleSchema)]).optional(),
|
|
135
|
+
});
|
|
115
136
|
// ── Factories ───────────────────────────────────────────────────────────────
|
|
116
137
|
function makeRole(spec) {
|
|
117
138
|
return { kind: spec.kind, source: { field: spec.source, multi: spec.multi ?? false } };
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
24
|
import type { RelationDef } from './relation.js';
|
|
25
25
|
import type { IdentityRole } from './roles.js';
|
|
26
|
-
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
26
|
+
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
|
|
27
27
|
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
28
28
|
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
29
29
|
/** Plug point for custom conventions (e.g. mixed legacy databases). */
|
package/dist/schema/schema.js
CHANGED
|
@@ -25,7 +25,7 @@ import { scopeSchema, grantsRefSchema } from './roles.js';
|
|
|
25
25
|
// Sync-group roles (identity + entity) live in `./roles.js`. Re-exported here
|
|
26
26
|
// so the long-standing `@ablo/schema` / `./schema.js` import paths keep working
|
|
27
27
|
// after the rehome — see roles.ts for the full vocabulary.
|
|
28
|
-
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
28
|
+
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
|
|
29
29
|
function resolveCasing(fn) {
|
|
30
30
|
if (fn === undefined)
|
|
31
31
|
return (x) => x;
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
|
-
* mutable,
|
|
17
|
-
* entityRoles, persist, autoFill, requiredFields, lazyObservable)
|
|
16
|
+
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
+
* grants, entityRoles, persist, autoFill, requiredFields, lazyObservable).
|
|
18
|
+
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
+
* `model()`-build; only the canonical wire fields cross here.
|
|
18
20
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
19
21
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
20
22
|
* - identity roles (already pure data)
|
package/dist/schema/serialize.js
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
|
-
* mutable,
|
|
17
|
-
* entityRoles, persist, autoFill, requiredFields, lazyObservable)
|
|
16
|
+
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
+
* grants, entityRoles, persist, autoFill, requiredFields, lazyObservable).
|
|
18
|
+
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
+
* `model()`-build; only the canonical wire fields cross here.
|
|
18
20
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
19
21
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
20
22
|
* - identity roles (already pure data)
|
package/dist/schema/sugar.d.ts
CHANGED
|
@@ -63,37 +63,16 @@ export interface SugarOptions<R extends RelationRecord = RelationRecord, C exten
|
|
|
63
63
|
*/
|
|
64
64
|
tableName?: string;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Row-access policy (tenant isolation / RLS) — who may *read* a row.
|
|
67
|
+
* Discriminated union on `by` (`column` | `parent` | `none`). See
|
|
68
|
+
* {@link ModelOptions.policy}.
|
|
68
69
|
*/
|
|
69
|
-
|
|
70
|
+
policy?: ModelOptions['policy'];
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
72
|
+
* Sync-group routing — which delta *channels* a row fans into
|
|
73
|
+
* (`root` / `grants` / `roles`). See {@link ModelOptions.groups}.
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Override the row-local tenancy column name. See
|
|
77
|
-
* {@link ModelOptions.orgColumn}.
|
|
78
|
-
*/
|
|
79
|
-
orgColumn?: ModelOptions['orgColumn'];
|
|
80
|
-
/** Canonical tenancy descriptor. See {@link ModelOptions.tenancy}. */
|
|
81
|
-
tenancy?: ModelOptions['tenancy'];
|
|
82
|
-
/**
|
|
83
|
-
* Mark this model a scope root — its records form the group `<kind>:<id>`
|
|
84
|
-
* (kind defaults from typename). See {@link ModelOptions.scope}.
|
|
85
|
-
*/
|
|
86
|
-
scope?: ModelOptions['scope'];
|
|
87
|
-
/**
|
|
88
|
-
* Membership edge granting identity → scope-root access. Both fields are
|
|
89
|
-
* relation names on this model. See {@link ModelOptions.grants}.
|
|
90
|
-
*/
|
|
91
|
-
grants?: ModelOptions['grants'];
|
|
92
|
-
/**
|
|
93
|
-
* Explicit non-relational record→group roles (e.g. inbox fan-out keyed on a
|
|
94
|
-
* field). See {@link ModelOptions.entityRoles}.
|
|
95
|
-
*/
|
|
96
|
-
entityRoles?: ModelOptions['entityRoles'];
|
|
75
|
+
groups?: ModelOptions['groups'];
|
|
97
76
|
/** Max rows loaded during bootstrap. Only applies to `.instant`. */
|
|
98
77
|
bootstrapLimit?: number;
|
|
99
78
|
/** Bootstrap sort order (e.g. `'created_at DESC'`). */
|
package/dist/schema/sugar.js
CHANGED
|
@@ -46,13 +46,8 @@ function build(shape, opts, baseline) {
|
|
|
46
46
|
lazyObservable: opts?.lazyObservable ?? baseline.lazyObservable,
|
|
47
47
|
typename: opts?.typename,
|
|
48
48
|
tableName: opts?.tableName,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
orgColumn: opts?.orgColumn,
|
|
52
|
-
tenancy: opts?.tenancy,
|
|
53
|
-
scope: opts?.scope,
|
|
54
|
-
grants: opts?.grants,
|
|
55
|
-
entityRoles: opts?.entityRoles,
|
|
49
|
+
policy: opts?.policy,
|
|
50
|
+
groups: opts?.groups,
|
|
56
51
|
bootstrapLimit: opts?.bootstrapLimit,
|
|
57
52
|
bootstrapOrderBy: opts?.bootstrapOrderBy,
|
|
58
53
|
persist: opts?.persist,
|
|
@@ -85,6 +85,7 @@ export declare const deltaAttributionSchema: z.ZodObject<{
|
|
|
85
85
|
system: "system";
|
|
86
86
|
}>>;
|
|
87
87
|
capabilityId: z.ZodNullable<z.ZodString>;
|
|
88
|
+
delegationChainRootUserId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
88
89
|
confirmationState: z.ZodNullable<z.ZodEnum<{
|
|
89
90
|
auto: "auto";
|
|
90
91
|
previewed: "previewed";
|
|
@@ -129,6 +130,7 @@ export declare const syncDeltaRowSchema: z.ZodObject<{
|
|
|
129
130
|
system: "system";
|
|
130
131
|
}>>;
|
|
131
132
|
capabilityId: z.ZodNullable<z.ZodString>;
|
|
133
|
+
delegationChainRootUserId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
132
134
|
confirmationState: z.ZodNullable<z.ZodEnum<{
|
|
133
135
|
auto: "auto";
|
|
134
136
|
previewed: "previewed";
|
|
@@ -67,7 +67,7 @@ export const syncDeltaCoreSchema = z.object({
|
|
|
67
67
|
createdAt: z.string().optional(),
|
|
68
68
|
transactionId: z.string().nullable(),
|
|
69
69
|
});
|
|
70
|
-
// ── Attribution — `control` plane
|
|
70
|
+
// ── Attribution — `control` plane ─────────────────────────────────────────────
|
|
71
71
|
export const deltaAttributionSchema = z.object({
|
|
72
72
|
/** Legacy single-actor column, derived during the dual-write window. */
|
|
73
73
|
createdBy: z.string().nullable(),
|
|
@@ -76,6 +76,7 @@ export const deltaAttributionSchema = z.object({
|
|
|
76
76
|
onBehalfOfId: z.string().nullable(),
|
|
77
77
|
onBehalfOfKind: participantKindSchema.nullable(),
|
|
78
78
|
capabilityId: z.string().nullable(),
|
|
79
|
+
delegationChainRootUserId: z.string().nullable().optional(),
|
|
79
80
|
confirmationState: confirmationStateSchema.nullable(),
|
|
80
81
|
backfillProvenance: backfillProvenanceSchema.nullable(),
|
|
81
82
|
});
|
package/dist/schema/tenancy.d.ts
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tenancy — the single source of truth for how a model's rows are scoped to a
|
|
3
|
-
* tenant.
|
|
4
|
-
*
|
|
5
|
-
* one Zod discriminated union, resolved in one place and consumed everywhere
|
|
6
|
-
* (provision/RLS, introspection, runtime, CLI).
|
|
3
|
+
* tenant. There are exactly two layers here, and keeping them separate is the
|
|
4
|
+
* whole point:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* 1. The CANONICAL form — {@link Tenancy}, a Zod discriminated union. This is
|
|
7
|
+
* what every consumer (provision/RLS, introspection, runtime, CLI) reads
|
|
8
|
+
* and what crosses the wire in `ModelJSON`. One shape, exhaustively
|
|
9
|
+
* switchable, so the type system holds the isolation boundary.
|
|
10
|
+
* 2. The AUTHORING form — {@link PolicyInput}, the `policy: { by }` option a
|
|
11
|
+
* schema author writes. The name follows Postgres/Supabase RLS vocabulary:
|
|
12
|
+
* a `policy` is the rule that decides which rows a tenant may read.
|
|
13
|
+
* {@link resolvePolicy} maps it to the canonical {@link Tenancy} at
|
|
14
|
+
* `model()`-build time (much as Supabase's `create policy` compiles to a
|
|
15
|
+
* `pg_policy` row), so the authoring vocabulary never reaches the wire or
|
|
16
|
+
* any consumer.
|
|
17
|
+
*
|
|
18
|
+
* Why one authoring option (`policy`) instead of the old
|
|
19
|
+
* `orgScoped`/`scopedVia`/`orgColumn` trio: those three were synonyms for one
|
|
20
|
+
* decision ("how is this row scoped?"), and the most dangerous of them
|
|
21
|
+
* (`orgScoped: false`) silently exposed a whole table cross-tenant. Collapsing
|
|
22
|
+
* them into a single discriminated union makes the opt-out (`{ by: 'none' }`) a
|
|
23
|
+
* loud, deliberate branch instead of a falsy flag — one concept, one name.
|
|
13
24
|
*/
|
|
14
25
|
import { z } from 'zod';
|
|
15
26
|
/** Default physical tenancy column. The ONLY place this literal is canonical. */
|
|
16
27
|
export declare const DEFAULT_ORG_COLUMN = "organization_id";
|
|
17
28
|
/**
|
|
18
29
|
* Scope a table's rows through a parent table (for rows that carry no tenancy
|
|
19
|
-
* column of their own — e.g. `slide_layers` → slide → deck → org).
|
|
30
|
+
* column of their own — e.g. `slide_layers` → slide → deck → org). This is the
|
|
31
|
+
* CANONICAL `parent` payload; authors write the friendlier {@link PolicyInput}
|
|
32
|
+
* `{ by: 'parent', fk, parent }` shape, normalized into this by
|
|
33
|
+
* {@link resolvePolicy}.
|
|
20
34
|
*/
|
|
21
35
|
export declare const scopedViaRefSchema: z.ZodObject<{
|
|
22
36
|
localKey: z.ZodString;
|
|
@@ -25,7 +39,7 @@ export declare const scopedViaRefSchema: z.ZodObject<{
|
|
|
25
39
|
parentOrgColumn: z.ZodOptional<z.ZodString>;
|
|
26
40
|
}, z.core.$strip>;
|
|
27
41
|
export type ScopedViaRef = z.infer<typeof scopedViaRefSchema>;
|
|
28
|
-
/** How a model's rows are scoped to a tenant. */
|
|
42
|
+
/** How a model's rows are scoped to a tenant — the CANONICAL, wire-facing form. */
|
|
29
43
|
export declare const tenancySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
44
|
kind: z.ZodLiteral<"column">;
|
|
31
45
|
column: z.ZodString;
|
|
@@ -42,25 +56,50 @@ export declare const tenancySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
42
56
|
}, z.core.$strip>], "kind">;
|
|
43
57
|
export type Tenancy = z.infer<typeof tenancySchema>;
|
|
44
58
|
/**
|
|
45
|
-
*
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
59
|
+
* The AUTHORING form of tenancy — what a schema author writes as the model's
|
|
60
|
+
* `policy` option (Postgres/Supabase RLS vocabulary: a policy is the rule that
|
|
61
|
+
* scopes which rows a tenant may read). A Zod discriminated union on `by`, so
|
|
62
|
+
* the three branches are mutually exclusive and the dangerous opt-out
|
|
63
|
+
* (`{ by: 'none' }`) is an explicit, named choice rather than a falsy flag.
|
|
64
|
+
*
|
|
65
|
+
* - `{ by: 'column' }` — row-local tenancy column (the default).
|
|
66
|
+
* `column` overrides the name (default {@link DEFAULT_ORG_COLUMN}).
|
|
67
|
+
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when
|
|
68
|
+
* this table has no tenancy column of its own. `parentKey` (default `'id'`)
|
|
69
|
+
* and `parentTenantColumn` (default {@link DEFAULT_ORG_COLUMN}) are overrides.
|
|
70
|
+
* - `{ by: 'none' }` — genuinely global / reference data. ⚠ Makes
|
|
71
|
+
* the whole table readable cross-tenant — only correct for tenant-less tables.
|
|
48
72
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
export declare const policyInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
74
|
+
by: z.ZodLiteral<"column">;
|
|
75
|
+
column: z.ZodOptional<z.ZodString>;
|
|
76
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
77
|
+
by: z.ZodLiteral<"parent">;
|
|
78
|
+
fk: z.ZodString;
|
|
79
|
+
parent: z.ZodString;
|
|
80
|
+
parentKey: z.ZodOptional<z.ZodString>;
|
|
81
|
+
parentTenantColumn: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
83
|
+
by: z.ZodLiteral<"none">;
|
|
84
|
+
}, z.core.$strip>], "by">;
|
|
85
|
+
export type PolicyInput = z.infer<typeof policyInputSchema>;
|
|
58
86
|
/**
|
|
59
|
-
* Normalize authoring
|
|
60
|
-
* at model
|
|
61
|
-
*
|
|
62
|
-
*
|
|
87
|
+
* Normalize the authoring {@link PolicyInput} into the one canonical
|
|
88
|
+
* {@link Tenancy}. Called once, at `model()`-build, so `ModelDef`/`ModelJSON`
|
|
89
|
+
* and every consumer see only the canonical union. Omitting `policy` defaults
|
|
90
|
+
* to a row-local `organization_id` column.
|
|
63
91
|
*/
|
|
64
|
-
export declare function
|
|
92
|
+
export declare function resolvePolicy(input?: PolicyInput): Tenancy;
|
|
93
|
+
/**
|
|
94
|
+
* Read the canonical {@link Tenancy} off an already-built model def (or parsed
|
|
95
|
+
* `ModelJSON`), defaulting to a row-local `organization_id` column when absent.
|
|
96
|
+
*
|
|
97
|
+
* This is the READ-side helper — consumers (provision/RLS, membership resolver,
|
|
98
|
+
* DDL, CLI) call it to get a model's tenancy without re-deriving the default in
|
|
99
|
+
* each place. It is NOT the authoring normalizer; that's {@link resolveIsolation}.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveTenancy(def: {
|
|
102
|
+
tenancy?: Tenancy;
|
|
103
|
+
}): Tenancy;
|
|
65
104
|
/** The physical tenancy column for a column-scoped model, else `null`. */
|
|
66
105
|
export declare function tenancyColumn(t: Tenancy): string | null;
|