@rebasepro/types 0.19.1 → 0.19.2-canary.g09316f6
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/dist/controllers/data.d.ts +515 -14
- package/dist/controllers/data_driver.d.ts +166 -4
- package/dist/controllers/storage.d.ts +30 -0
- package/dist/index.es.js +163 -3
- package/dist/index.es.js.map +1 -1
- package/dist/types/collections.d.ts +59 -0
- package/dist/types/filter-operators.d.ts +37 -6
- package/dist/types/index.d.ts +1 -0
- package/dist/types/policy.d.ts +39 -1
- package/dist/types/properties.d.ts +205 -8
- package/dist/types/relations.d.ts +71 -0
- package/dist/types/schema_editing.d.ts +6 -0
- package/dist/types/tenancy.d.ts +147 -0
- package/dist/types/websockets.d.ts +37 -0
- package/dist/users/user.d.ts +21 -0
- package/package.json +1 -1
|
@@ -62,6 +62,12 @@ export interface SchemaCommitPaths {
|
|
|
62
62
|
searchFile: string;
|
|
63
63
|
/** Vector columns and ANN indexes — like search, applied by Rebase not Atlas. */
|
|
64
64
|
vectorFile: string;
|
|
65
|
+
/**
|
|
66
|
+
* `autoValue: "on_update"` triggers and the function they share. Atlas's
|
|
67
|
+
* free tier will not parse a desired state containing a function, so this
|
|
68
|
+
* is applied by Rebase like search and vector.
|
|
69
|
+
*/
|
|
70
|
+
triggersFile: string;
|
|
65
71
|
}
|
|
66
72
|
export declare const DEFAULT_COMMIT_PATHS: SchemaCommitPaths;
|
|
67
73
|
/** One file the commit writes, as content rather than as a path on a disk. */
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class multi-tenancy: one declaration, every layer.
|
|
3
|
+
*
|
|
4
|
+
* A tenant-scoped collection was expert work. It took four separate,
|
|
5
|
+
* hand-written pieces that nothing checked against each other — a column, an
|
|
6
|
+
* `existsIn` or raw RLS rule, a value stamped on every insert by a callback,
|
|
7
|
+
* and an index somebody had to remember. Miss the index and the table scans;
|
|
8
|
+
* miss the stamp and the row is invisible the moment it is written; miss the
|
|
9
|
+
* rule and every tenant reads every other tenant's rows, which is the failure
|
|
10
|
+
* nothing surfaces until it is a disclosure.
|
|
11
|
+
*
|
|
12
|
+
* {@link CollectionTenantConfig} is the one place that says "this collection
|
|
13
|
+
* belongs to a tenant", and the four pieces are derived from it:
|
|
14
|
+
*
|
|
15
|
+
* - the column is `NOT NULL` and gets a btree index (`planSchema`);
|
|
16
|
+
* - a **restrictive** RLS policy is injected for every operation, so it
|
|
17
|
+
* composes with (rather than replaces) whatever `securityRules` the
|
|
18
|
+
* collection declares — tenancy narrows, it never grants;
|
|
19
|
+
* - the write path stamps the caller's tenant on create, refuses a write that
|
|
20
|
+
* names another tenant, and refuses an update that moves a row between
|
|
21
|
+
* tenants;
|
|
22
|
+
* - the OpenAPI document marks the field so a generated client can see it.
|
|
23
|
+
*
|
|
24
|
+
* @see CollectionTenantConfig
|
|
25
|
+
* @group Models
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The caller's tenant comes from a claim on their session token.
|
|
29
|
+
*
|
|
30
|
+
* The single-tenant-per-user shape: an identity provider (or Rebase's own
|
|
31
|
+
* custom-claims hook) puts the organization on the token, and every request
|
|
32
|
+
* carries it. Compiles to a comparison against `rebase.jwt() ->> '<claim>'`,
|
|
33
|
+
* which is the same value a hand-written rule would read — so the generated
|
|
34
|
+
* policy and anything an author writes beside it agree by construction.
|
|
35
|
+
*
|
|
36
|
+
* @group Models
|
|
37
|
+
*/
|
|
38
|
+
export interface TenantClaimSource {
|
|
39
|
+
/**
|
|
40
|
+
* The claim's name on the access token, e.g. `"org_id"`.
|
|
41
|
+
*
|
|
42
|
+
* Custom claims survive verification and reach RLS as `rebase.jwt()`; the
|
|
43
|
+
* identity claims (`uid`, `roles`, `aal`, `isAnonymous`) are written after
|
|
44
|
+
* them when a token is minted and cannot be shadowed, so naming one of
|
|
45
|
+
* those here is refused rather than quietly reading the identity.
|
|
46
|
+
*/
|
|
47
|
+
claim: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The caller's tenants come from rows of a membership collection.
|
|
51
|
+
*
|
|
52
|
+
* The many-tenants-per-user shape — a `memberships` table with a user column
|
|
53
|
+
* and a tenant column, which is how a person belongs to three organizations at
|
|
54
|
+
* once. Compiles to a correlated `EXISTS` over that table (`policy.existsIn`),
|
|
55
|
+
* so the database answers "is the caller a member of this row's tenant?" in the
|
|
56
|
+
* same query rather than in an N+1 of lookups.
|
|
57
|
+
*
|
|
58
|
+
* Nothing is put on the token, so nothing has to be re-minted when somebody
|
|
59
|
+
* joins or leaves a tenant — the next statement already sees the new row.
|
|
60
|
+
*
|
|
61
|
+
* @group Models
|
|
62
|
+
*/
|
|
63
|
+
export interface TenantMembershipSource {
|
|
64
|
+
membership: {
|
|
65
|
+
/** Slug of the collection holding the memberships. */
|
|
66
|
+
collection: string;
|
|
67
|
+
/** The property on it that holds the user id (compared to `rebase.uid()`). */
|
|
68
|
+
userField: string;
|
|
69
|
+
/** The property on it that holds the tenant id. */
|
|
70
|
+
tenantField: string;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Where the caller's tenant comes from. @group Models */
|
|
74
|
+
export type TenantSource = TenantClaimSource | TenantMembershipSource;
|
|
75
|
+
/** Narrow a {@link TenantSource} to its claim form. @group Models */
|
|
76
|
+
export declare function isTenantClaimSource(source: TenantSource): source is TenantClaimSource;
|
|
77
|
+
/** Narrow a {@link TenantSource} to its membership form. @group Models */
|
|
78
|
+
export declare function isTenantMembershipSource(source: TenantSource): source is TenantMembershipSource;
|
|
79
|
+
/**
|
|
80
|
+
* The roles tenancy does not apply to, when the collection names none.
|
|
81
|
+
*
|
|
82
|
+
* `admin`, mirroring the security baseline every collection already carries
|
|
83
|
+
* (`<table>_default_admin_read` / `_write`): the Studio, `dataAsAdmin` and a
|
|
84
|
+
* support operator all run with it, and a tenancy rule that locked them out
|
|
85
|
+
* would make the admin panel show an empty table on a collection full of rows.
|
|
86
|
+
*
|
|
87
|
+
* @group Models
|
|
88
|
+
*/
|
|
89
|
+
export declare const DEFAULT_TENANT_BYPASS_ROLES: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* Declare a collection tenant-scoped.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* export const posts = buildCollection({
|
|
95
|
+
* slug: "posts",
|
|
96
|
+
* properties: {
|
|
97
|
+
* orgId: { type: "string", validation: { required: true } },
|
|
98
|
+
* title: { type: "string" }
|
|
99
|
+
* },
|
|
100
|
+
* tenant: { field: "orgId", from: { claim: "org_id" } }
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* The property has to exist — this says what a column *means*, it does not
|
|
105
|
+
* conjure one into existence, exactly like `softDelete`. A config naming a
|
|
106
|
+
* property the collection does not declare is refused at boot rather than at
|
|
107
|
+
* the first read.
|
|
108
|
+
*
|
|
109
|
+
* ## What it composes with
|
|
110
|
+
*
|
|
111
|
+
* The injected policy is **restrictive**, so it is ANDed with every permissive
|
|
112
|
+
* policy on the table: `securityRules`, `ownerField`, the injected admin
|
|
113
|
+
* baseline. That is the only composition that is safe by construction — a
|
|
114
|
+
* permissive tenancy policy would OR with the author's rules and a single
|
|
115
|
+
* `access: "public"` rule would take the whole tenancy boundary off.
|
|
116
|
+
*
|
|
117
|
+
* Postgres-only. RLS is what enforces it, and an engine without row-level
|
|
118
|
+
* security cannot be given this guarantee by an application-layer filter that
|
|
119
|
+
* a raw query goes around.
|
|
120
|
+
*
|
|
121
|
+
* @group Models
|
|
122
|
+
*/
|
|
123
|
+
export interface CollectionTenantConfig<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
124
|
+
/**
|
|
125
|
+
* The property holding the tenant id.
|
|
126
|
+
*
|
|
127
|
+
* A `string` or `number` property, or a `reference` / `belongsTo` relation
|
|
128
|
+
* to the tenants collection — in which case the foreign key the relation
|
|
129
|
+
* already declares is the column, and no second one is created.
|
|
130
|
+
*
|
|
131
|
+
* The column is made `NOT NULL` and indexed: a nullable tenant column is a
|
|
132
|
+
* row that belongs to nobody and is therefore invisible to everybody, and
|
|
133
|
+
* an unindexed one turns every RLS-filtered read into a sequential scan.
|
|
134
|
+
*/
|
|
135
|
+
field: Extract<keyof M, string> | string;
|
|
136
|
+
/** Where the caller's tenant comes from. */
|
|
137
|
+
from: TenantSource;
|
|
138
|
+
/**
|
|
139
|
+
* Roles that see and write across every tenant.
|
|
140
|
+
*
|
|
141
|
+
* Defaults to {@link DEFAULT_TENANT_BYPASS_ROLES}. An empty array means
|
|
142
|
+
* "nobody bypasses" — the trusted server context still does, because it is
|
|
143
|
+
* what runs migrations and the auth flows, and a policy that excluded it
|
|
144
|
+
* would break the boot rather than protect a tenant.
|
|
145
|
+
*/
|
|
146
|
+
bypassRoles?: readonly string[];
|
|
147
|
+
}
|
|
@@ -39,6 +39,41 @@ export type WirePrimaryKeys = {
|
|
|
39
39
|
type: "string" | "number";
|
|
40
40
|
isUUID?: boolean;
|
|
41
41
|
}[];
|
|
42
|
+
/**
|
|
43
|
+
* What a `collection_update` frame says about the page it carries.
|
|
44
|
+
*
|
|
45
|
+
* The same shape a REST list's `meta` has, and for the same reason: a
|
|
46
|
+
* subscriber renders the list it was handed, and rendering it needs to know how
|
|
47
|
+
* many rows there are in total and whether there is another page.
|
|
48
|
+
*
|
|
49
|
+
* It used not to be sent. The frame carried rows and primary keys and nothing
|
|
50
|
+
* else, so the SDK issued a `GET /<collection>/count` **on every push** to
|
|
51
|
+
* recover it — one extra round trip per write, per subscriber, and a window in
|
|
52
|
+
* which the count and the rows described different states of the collection.
|
|
53
|
+
* The refetch already knows the query, so it counts once, beside the rows,
|
|
54
|
+
* inside the same RLS-bound transaction that read them.
|
|
55
|
+
*/
|
|
56
|
+
export interface CollectionUpdateMeta {
|
|
57
|
+
/** Rows matching the subscription's query. Absent when the count failed. */
|
|
58
|
+
total?: number;
|
|
59
|
+
/** Page size the rows were read with. */
|
|
60
|
+
limit: number;
|
|
61
|
+
/** Rows skipped to reach them. */
|
|
62
|
+
offset: number;
|
|
63
|
+
/** Whether rows exist beyond this page. */
|
|
64
|
+
hasMore: boolean;
|
|
65
|
+
/** The opaque cursor continuing this listing — see `PaginationMeta.nextCursor`. */
|
|
66
|
+
nextCursor?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The count could not be taken, so `total` is missing and `hasMore` is a
|
|
69
|
+
* floor rather than an answer.
|
|
70
|
+
*
|
|
71
|
+
* Flagged rather than guessed: a subscriber that knows the total is unknown
|
|
72
|
+
* can keep the last one it had, which is what the SDK does. Substituting
|
|
73
|
+
* `rows.length` would claim a page read at offset 10 held two rows.
|
|
74
|
+
*/
|
|
75
|
+
partial?: boolean;
|
|
76
|
+
}
|
|
42
77
|
export interface CollectionUpdateMessage extends WebSocketMessage {
|
|
43
78
|
type: "collection_update";
|
|
44
79
|
subscriptionId: string;
|
|
@@ -50,6 +85,8 @@ export interface CollectionUpdateMessage extends WebSocketMessage {
|
|
|
50
85
|
* unchanged rows' references needs an address to match them by.
|
|
51
86
|
*/
|
|
52
87
|
pks?: WirePrimaryKeys;
|
|
88
|
+
/** See {@link CollectionUpdateMeta}. */
|
|
89
|
+
meta?: CollectionUpdateMeta;
|
|
53
90
|
}
|
|
54
91
|
export interface SingleUpdateMessage extends WebSocketMessage {
|
|
55
92
|
type: "single_update";
|
package/dist/users/user.d.ts
CHANGED
|
@@ -58,5 +58,26 @@ export type User = {
|
|
|
58
58
|
* Accessible by the frontend, but only writable by the backend.
|
|
59
59
|
*/
|
|
60
60
|
readonly metadata?: Record<string, any>;
|
|
61
|
+
/**
|
|
62
|
+
* The **custom claims on this session's token**, as the server verified
|
|
63
|
+
* them — not the user row's {@link metadata}, which is a different fact
|
|
64
|
+
* with a different lifetime.
|
|
65
|
+
*
|
|
66
|
+
* The distinction matters for authorization. `metadata` is whatever the
|
|
67
|
+
* users table holds right now; `claims` is what the caller's token asserts,
|
|
68
|
+
* which is what the database sees: they reach RLS as `rebase.jwt()`, and
|
|
69
|
+
* `policy.authClaim("org_id")` reads one. Multi-tenancy's `claim` form is
|
|
70
|
+
* built on them.
|
|
71
|
+
*
|
|
72
|
+
* Identity claims are not here. `uid`, `roles`, `aal` and `isAnonymous` are
|
|
73
|
+
* written *after* the custom claims when a token is minted, precisely so a
|
|
74
|
+
* claims hook cannot assert them, and they have their own fields on this
|
|
75
|
+
* type. Keeping them out means a rule reading `claims` can never be reading
|
|
76
|
+
* something the session asserted about its own identity.
|
|
77
|
+
*
|
|
78
|
+
* Absent for a caller with no token — an API key, a service identity, an
|
|
79
|
+
* anonymous request.
|
|
80
|
+
*/
|
|
81
|
+
readonly claims?: Record<string, unknown>;
|
|
61
82
|
getIdToken?: (forceRefresh?: boolean) => Promise<string>;
|
|
62
83
|
};
|