@rebasepro/types 0.7.0 → 0.9.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/README.md +10 -10
- package/dist/controllers/auth.d.ts +1 -1
- package/dist/controllers/client.d.ts +237 -7
- package/dist/controllers/collection_registry.d.ts +3 -3
- package/dist/controllers/customization_controller.d.ts +1 -1
- package/dist/controllers/data.d.ts +285 -81
- package/dist/controllers/data_driver.d.ts +50 -37
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/local_config_persistence.d.ts +4 -4
- package/dist/controllers/navigation.d.ts +2 -2
- package/dist/controllers/registry.d.ts +17 -6
- package/dist/controllers/{side_entity_controller.d.ts → side_panel_controller.d.ts} +7 -7
- package/dist/controllers/storage.d.ts +39 -0
- package/dist/errors.d.ts +64 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +290 -16
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +305 -18
- package/dist/index.umd.js.map +1 -1
- package/dist/rebase_context.d.ts +8 -4
- package/dist/types/auth_adapter.d.ts +9 -6
- package/dist/types/backend.d.ts +30 -26
- package/dist/types/builders.d.ts +2 -2
- package/dist/types/collections.d.ts +211 -136
- package/dist/types/component_overrides.d.ts +61 -3
- package/dist/types/cron.d.ts +10 -1
- package/dist/types/data_source.d.ts +18 -5
- package/dist/types/database_adapter.d.ts +4 -3
- package/dist/types/entities.d.ts +7 -7
- package/dist/types/entity_actions.d.ts +7 -7
- package/dist/types/entity_callbacks.d.ts +55 -43
- package/dist/types/entity_views.d.ts +3 -3
- package/dist/types/filter-operators.d.ts +153 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/modify_collections.d.ts +2 -2
- package/dist/types/plugins.d.ts +9 -9
- package/dist/types/policy.d.ts +191 -0
- package/dist/types/properties.d.ts +153 -43
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/relations.d.ts +3 -3
- package/dist/types/slots.d.ts +14 -14
- package/dist/types/storage_source.d.ts +83 -0
- package/dist/types/websockets.d.ts +12 -13
- package/dist/users/user.d.ts +21 -9
- package/package.json +1 -1
- package/src/controllers/auth.tsx +1 -1
- package/src/controllers/client.ts +275 -7
- package/src/controllers/collection_registry.ts +3 -3
- package/src/controllers/customization_controller.tsx +1 -1
- package/src/controllers/data.ts +309 -97
- package/src/controllers/data_driver.ts +49 -40
- package/src/controllers/index.ts +1 -1
- package/src/controllers/local_config_persistence.tsx +4 -4
- package/src/controllers/navigation.ts +2 -2
- package/src/controllers/registry.ts +18 -6
- package/src/controllers/{side_entity_controller.tsx → side_panel_controller.tsx} +7 -7
- package/src/controllers/storage.ts +60 -1
- package/src/errors.ts +80 -0
- package/src/index.ts +1 -0
- package/src/rebase_context.tsx +8 -4
- package/src/types/auth_adapter.ts +9 -6
- package/src/types/backend.ts +41 -36
- package/src/types/builders.ts +2 -2
- package/src/types/collections.ts +256 -172
- package/src/types/component_overrides.ts +72 -5
- package/src/types/cron.ts +10 -1
- package/src/types/data_source.ts +29 -7
- package/src/types/database_adapter.ts +4 -3
- package/src/types/entities.ts +7 -7
- package/src/types/entity_actions.tsx +7 -7
- package/src/types/entity_callbacks.ts +58 -47
- package/src/types/entity_views.tsx +3 -3
- package/src/types/filter-operators.ts +240 -0
- package/src/types/index.ts +3 -2
- package/src/types/modify_collections.tsx +2 -2
- package/src/types/plugins.tsx +9 -9
- package/src/types/policy.ts +229 -0
- package/src/types/properties.ts +164 -44
- package/src/types/property_config.tsx +0 -1
- package/src/types/relations.ts +3 -3
- package/src/types/slots.tsx +14 -14
- package/src/types/storage_source.ts +90 -0
- package/src/types/websockets.ts +12 -14
- package/src/users/user.ts +22 -9
- package/dist/types/backend_hooks.d.ts +0 -109
- package/dist/types/entity_overrides.d.ts +0 -10
- package/src/types/backend_hooks.ts +0 -114
- package/src/types/entity_overrides.tsx +0 -11
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical filter operators and REST wire-format mappings.
|
|
3
|
+
*
|
|
4
|
+
* `WhereFilterOp` is THE operator type used at every layer — from React
|
|
5
|
+
* components through the SDK, server, and down to the database driver.
|
|
6
|
+
*
|
|
7
|
+
* PostgREST short-codes (`eq`, `gt`, `cs`, …) exist **only** at the
|
|
8
|
+
* HTTP wire boundary, handled by `serializeFilter` / `deserializeFilter`
|
|
9
|
+
* in `@rebasepro/common`.
|
|
10
|
+
*
|
|
11
|
+
* ┌──────────────────────┬───────────────┬──────────────────────────────┐
|
|
12
|
+
* │ Canonical │ REST short │ Meaning │
|
|
13
|
+
* ├──────────────────────┼───────────────┼──────────────────────────────┤
|
|
14
|
+
* │ "==" │ "eq" │ Equal │
|
|
15
|
+
* │ "!=" │ "neq" │ Not equal │
|
|
16
|
+
* │ ">" │ "gt" │ Greater than │
|
|
17
|
+
* │ ">=" │ "gte" │ Greater than or equal │
|
|
18
|
+
* │ "<" │ "lt" │ Less than │
|
|
19
|
+
* │ "<=" │ "lte" │ Less than or equal │
|
|
20
|
+
* │ "in" │ "in" │ Value in list │
|
|
21
|
+
* │ "not-in" │ "nin" │ Value not in list │
|
|
22
|
+
* │ "array-contains" │ "cs" │ Array contains element │
|
|
23
|
+
* │ "array-contains-any" │ "csa" │ Array contains any of │
|
|
24
|
+
* │ "like" │ "like" │ SQL LIKE (case-sensitive) │
|
|
25
|
+
* │ "ilike" │ "ilike" │ SQL ILIKE (case-insensitive) │
|
|
26
|
+
* │ "not-like" │ "nlike" │ NOT LIKE (case-sensitive) │
|
|
27
|
+
* │ "not-ilike" │ "nilike" │ NOT ILIKE (case-insensitive) │
|
|
28
|
+
* │ "is-null" │ "isnull" │ Field IS NULL │
|
|
29
|
+
* │ "is-not-null" │ "notnull" │ Field IS NOT NULL │
|
|
30
|
+
* └──────────────────────┴───────────────┴──────────────────────────────┘
|
|
31
|
+
*
|
|
32
|
+
* Pattern matching (`like`/`ilike`) uses SQL wildcard syntax: `%` matches any
|
|
33
|
+
* sequence of characters, `_` matches a single character. On MongoDB these are
|
|
34
|
+
* translated to anchored regular expressions; Firestore has no native pattern
|
|
35
|
+
* matching and rejects these operators (use `searchString` instead).
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Canonical sort representation: `[fieldName, direction]`.
|
|
42
|
+
*
|
|
43
|
+
* Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.
|
|
44
|
+
* The colon-string form (`"field:direction"`) exists only at the HTTP wire
|
|
45
|
+
* boundary, handled by `serializeOrderBy` / `deserializeOrderBy` in
|
|
46
|
+
* `@rebasepro/common`.
|
|
47
|
+
*
|
|
48
|
+
* Design note: the natural extension for multi-column sort is
|
|
49
|
+
* `OrderByTuple[]` — not implemented yet (server consumes only the first).
|
|
50
|
+
*
|
|
51
|
+
* @group Models
|
|
52
|
+
*/
|
|
53
|
+
export type OrderByTuple<Key extends string = string> = [Key, "asc" | "desc"];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Canonical filter operators supported across all database backends.
|
|
57
|
+
* Each DB driver translates these to its native query format.
|
|
58
|
+
*
|
|
59
|
+
* @group Models
|
|
60
|
+
*/
|
|
61
|
+
export type WhereFilterOp =
|
|
62
|
+
| "<"
|
|
63
|
+
| "<="
|
|
64
|
+
| "=="
|
|
65
|
+
| "!="
|
|
66
|
+
| ">="
|
|
67
|
+
| ">"
|
|
68
|
+
| "array-contains"
|
|
69
|
+
| "in"
|
|
70
|
+
| "not-in"
|
|
71
|
+
| "array-contains-any"
|
|
72
|
+
| "like"
|
|
73
|
+
| "ilike"
|
|
74
|
+
| "not-like"
|
|
75
|
+
| "not-ilike"
|
|
76
|
+
| "is-null"
|
|
77
|
+
| "is-not-null";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Used to define filters applied in collections.
|
|
81
|
+
*
|
|
82
|
+
* A single condition is a tuple `[operator, value]`.
|
|
83
|
+
* Multiple conditions on the same field use an array of tuples.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Single condition per field
|
|
87
|
+
* { status: ["==", "active"], price: [">=", 9.99] }
|
|
88
|
+
*
|
|
89
|
+
* // Multiple conditions on one field
|
|
90
|
+
* { age: [[">=", 18], ["<", 65]] }
|
|
91
|
+
*
|
|
92
|
+
* // Array operators
|
|
93
|
+
* { role: ["in", ["admin", "editor"]] }
|
|
94
|
+
* { tags: ["array-contains", "featured"] }
|
|
95
|
+
*
|
|
96
|
+
* // Pattern matching (SQL wildcards: % and _)
|
|
97
|
+
* { name: ["ilike", "%john%"] }
|
|
98
|
+
* { slug: ["like", "post-%"] }
|
|
99
|
+
*
|
|
100
|
+
* // Null checks (the value is ignored; `null` is conventional)
|
|
101
|
+
* { deleted_at: ["is-null", null] }
|
|
102
|
+
* { published_at: ["is-not-null", null] }
|
|
103
|
+
*
|
|
104
|
+
* @group Models
|
|
105
|
+
*/
|
|
106
|
+
export type FilterValues<Key extends string> =
|
|
107
|
+
Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][]>>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Relaxed filter type that also accepts pre-serialized PostgREST strings.
|
|
111
|
+
* **Internal only** — used at the wire-format boundary
|
|
112
|
+
* (`serializeFilter` / `deserializeFilter` in `@rebasepro/common`).
|
|
113
|
+
*
|
|
114
|
+
* Application code, UI components, and SDK consumers should use
|
|
115
|
+
* {@link FilterValues} instead.
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
export type WireFilterValues<Key extends string> =
|
|
120
|
+
Partial<Record<Key, [WhereFilterOp, unknown] | [WhereFilterOp, unknown][] | string>>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A pre-defined filter preset for quick access in the collection toolbar.
|
|
124
|
+
* Users can select a preset to instantly apply a set of filters and
|
|
125
|
+
* optionally a sort order.
|
|
126
|
+
*
|
|
127
|
+
* @group Models
|
|
128
|
+
*/
|
|
129
|
+
export interface FilterPreset<Key extends string = string> {
|
|
130
|
+
/**
|
|
131
|
+
* Display label shown in the preset menu.
|
|
132
|
+
* If omitted, a summary is auto-generated from the filter keys.
|
|
133
|
+
*/
|
|
134
|
+
label?: string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The filter values to apply when this preset is selected.
|
|
138
|
+
*/
|
|
139
|
+
filterValues: FilterValues<Key>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Optional sort override to apply alongside the filter values.
|
|
143
|
+
*/
|
|
144
|
+
sort?: OrderByTuple<Key>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PostgREST short-code operators. Wire format only — these never appear
|
|
149
|
+
* in application code. Used by `serializeFilter`/`deserializeFilter`
|
|
150
|
+
* in `@rebasepro/common`.
|
|
151
|
+
*/
|
|
152
|
+
export type RestFilterOp =
|
|
153
|
+
| "eq" | "neq"
|
|
154
|
+
| "gt" | "gte"
|
|
155
|
+
| "lt" | "lte"
|
|
156
|
+
| "in" | "nin"
|
|
157
|
+
| "cs" | "csa"
|
|
158
|
+
| "like" | "ilike"
|
|
159
|
+
| "nlike" | "nilike"
|
|
160
|
+
| "isnull" | "notnull";
|
|
161
|
+
|
|
162
|
+
/** Maps canonical operators to their REST short-code equivalents. */
|
|
163
|
+
export const CANONICAL_TO_REST: Readonly<Record<WhereFilterOp, RestFilterOp>> = {
|
|
164
|
+
"==": "eq",
|
|
165
|
+
"!=": "neq",
|
|
166
|
+
">": "gt",
|
|
167
|
+
">=": "gte",
|
|
168
|
+
"<": "lt",
|
|
169
|
+
"<=": "lte",
|
|
170
|
+
"in": "in",
|
|
171
|
+
"not-in": "nin",
|
|
172
|
+
"array-contains": "cs",
|
|
173
|
+
"array-contains-any": "csa",
|
|
174
|
+
"like": "like",
|
|
175
|
+
"ilike": "ilike",
|
|
176
|
+
"not-like": "nlike",
|
|
177
|
+
"not-ilike": "nilike",
|
|
178
|
+
"is-null": "isnull",
|
|
179
|
+
"is-not-null": "notnull"
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Maps REST short-code operators to their canonical equivalents. */
|
|
183
|
+
export const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilterOp>> = {
|
|
184
|
+
"eq": "==",
|
|
185
|
+
"neq": "!=",
|
|
186
|
+
"gt": ">",
|
|
187
|
+
"gte": ">=",
|
|
188
|
+
"lt": "<",
|
|
189
|
+
"lte": "<=",
|
|
190
|
+
"in": "in",
|
|
191
|
+
"nin": "not-in",
|
|
192
|
+
"cs": "array-contains",
|
|
193
|
+
"csa": "array-contains-any",
|
|
194
|
+
"like": "like",
|
|
195
|
+
"ilike": "ilike",
|
|
196
|
+
"nlike": "not-like",
|
|
197
|
+
"nilike": "not-ilike",
|
|
198
|
+
"isnull": "is-null",
|
|
199
|
+
"notnull": "is-not-null"
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Operators that test for null/not-null and therefore ignore their value.
|
|
204
|
+
* Codecs normalize the value of these conditions to `null`.
|
|
205
|
+
*/
|
|
206
|
+
export const NULL_OPS: ReadonlySet<WhereFilterOp> = new Set<WhereFilterOp>([
|
|
207
|
+
"is-null", "is-not-null"
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Every canonical operator, in a stable order. Useful for engine capability
|
|
212
|
+
* declarations ({@link DataSourceCapabilities.filterOperators}) and for
|
|
213
|
+
* building operator subsets.
|
|
214
|
+
* @group Models
|
|
215
|
+
*/
|
|
216
|
+
export const ALL_WHERE_FILTER_OPS: readonly WhereFilterOp[] = [
|
|
217
|
+
"<", "<=", "==", "!=", ">=", ">",
|
|
218
|
+
"in", "not-in",
|
|
219
|
+
"array-contains", "array-contains-any",
|
|
220
|
+
"like", "ilike", "not-like", "not-ilike",
|
|
221
|
+
"is-null", "is-not-null"
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
/** All canonical operator strings for runtime validation. */
|
|
225
|
+
const CANONICAL_OPS: ReadonlySet<string> = new Set<WhereFilterOp>(ALL_WHERE_FILTER_OPS);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Resolve any operator string (canonical or REST short-code) to its
|
|
229
|
+
* canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* toCanonicalOp("==") // "=="
|
|
233
|
+
* toCanonicalOp("eq") // "=="
|
|
234
|
+
* toCanonicalOp("cs") // "array-contains"
|
|
235
|
+
* toCanonicalOp("xyz") // undefined
|
|
236
|
+
*/
|
|
237
|
+
export function toCanonicalOp(op: string): WhereFilterOp | undefined {
|
|
238
|
+
if (CANONICAL_OPS.has(op)) return op as WhereFilterOp;
|
|
239
|
+
return (REST_TO_CANONICAL as Record<string, WhereFilterOp | undefined>)[op];
|
|
240
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
export * from "./entities";
|
|
2
|
+
export * from "./filter-operators";
|
|
2
3
|
export * from "./chips";
|
|
3
4
|
|
|
4
5
|
export * from "./properties";
|
|
5
6
|
export * from "./collections";
|
|
6
7
|
export * from "./relations";
|
|
8
|
+
export * from "./policy";
|
|
7
9
|
|
|
8
10
|
export * from "./locales";
|
|
9
11
|
export * from "./entity_link_builder";
|
|
10
12
|
export * from "./user_management_delegate";
|
|
11
13
|
export * from "./entity_callbacks";
|
|
12
|
-
export * from "./entity_overrides";
|
|
13
14
|
export * from "./export_import";
|
|
14
15
|
export * from "./modify_collections";
|
|
15
16
|
export * from "./formex";
|
|
@@ -23,8 +24,8 @@ export * from "./entity_actions";
|
|
|
23
24
|
export * from "./property_config";
|
|
24
25
|
export * from "./entity_views";
|
|
25
26
|
export * from "./data_source";
|
|
27
|
+
export * from "./storage_source";
|
|
26
28
|
export * from "./cron";
|
|
27
|
-
export * from "./backend_hooks";
|
|
28
29
|
export * from "./component_ref";
|
|
29
30
|
export * from "./auth_adapter";
|
|
30
31
|
export * from "./database_adapter";
|
package/src/types/plugins.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { PropsWithChildren } from "react";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { CollectionConfig, CollectionActionsProps, EntityTableController, SelectionController } from "./collections";
|
|
4
4
|
import type { EntityStatus } from "./entities";
|
|
5
5
|
import type { InferPropertyType, Property } from "./properties";
|
|
6
6
|
import type { FormContext } from "./entity_views";
|
|
@@ -181,19 +181,19 @@ export interface PluginHooks {
|
|
|
181
181
|
/**
|
|
182
182
|
* Modify a single collection before it is rendered (synchronous).
|
|
183
183
|
*/
|
|
184
|
-
modifyCollection?: (collection:
|
|
184
|
+
modifyCollection?: (collection: CollectionConfig) => CollectionConfig;
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
187
|
* Async version of modifyCollection — supports fetching remote config.
|
|
188
188
|
* Runs during navigation resolution. If provided alongside `modifyCollection`,
|
|
189
189
|
* the sync version runs first, then the async version.
|
|
190
190
|
*/
|
|
191
|
-
modifyCollectionAsync?: (collection:
|
|
191
|
+
modifyCollectionAsync?: (collection: CollectionConfig) => Promise<CollectionConfig>;
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
194
|
* Modify, add or remove collections.
|
|
195
195
|
*/
|
|
196
|
-
injectCollections?: (collections:
|
|
196
|
+
injectCollections?: (collections: CollectionConfig[]) => CollectionConfig[];
|
|
197
197
|
|
|
198
198
|
/**
|
|
199
199
|
* Callback called when columns are reordered via drag and drop.
|
|
@@ -202,7 +202,7 @@ export interface PluginHooks {
|
|
|
202
202
|
fullPath: string;
|
|
203
203
|
parentCollectionSlugs: string[];
|
|
204
204
|
parentEntityIds: string[];
|
|
205
|
-
collection:
|
|
205
|
+
collection: CollectionConfig;
|
|
206
206
|
newPropertiesOrder: string[];
|
|
207
207
|
}) => void;
|
|
208
208
|
|
|
@@ -213,7 +213,7 @@ export interface PluginHooks {
|
|
|
213
213
|
fullPath: string;
|
|
214
214
|
parentCollectionSlugs: string[];
|
|
215
215
|
parentEntityIds: string[];
|
|
216
|
-
collection:
|
|
216
|
+
collection: CollectionConfig;
|
|
217
217
|
kanbanColumnProperty: string;
|
|
218
218
|
newColumnsOrder: string[];
|
|
219
219
|
}) => void;
|
|
@@ -291,7 +291,7 @@ export interface FieldBuilderConfig {
|
|
|
291
291
|
* Props passed to home page collection card action components.
|
|
292
292
|
* @group Models
|
|
293
293
|
*/
|
|
294
|
-
export interface PluginHomePageActionsProps<EP extends object = object, M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User, EC extends
|
|
294
|
+
export interface PluginHomePageActionsProps<EP extends object = object, M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User, EC extends CollectionConfig<M> = CollectionConfig<M>> {
|
|
295
295
|
slug: string;
|
|
296
296
|
collection: EC;
|
|
297
297
|
context: RebaseContext<USER>;
|
|
@@ -301,7 +301,7 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
|
|
|
301
301
|
* Props passed to form action components in entity edit/form views.
|
|
302
302
|
* @group Models
|
|
303
303
|
*/
|
|
304
|
-
export interface PluginFormActionProps<USER extends User = User, EC extends
|
|
304
|
+
export interface PluginFormActionProps<USER extends User = User, EC extends CollectionConfig = CollectionConfig> {
|
|
305
305
|
entityId?: string | number;
|
|
306
306
|
path: string;
|
|
307
307
|
parentCollectionSlugs: string[];
|
|
@@ -318,7 +318,7 @@ export interface PluginFormActionProps<USER extends User = User, EC extends Enti
|
|
|
318
318
|
* Parameters passed to the field builder wrap function.
|
|
319
319
|
* @group Models
|
|
320
320
|
*/
|
|
321
|
-
export type PluginFieldBuilderParams<M extends Record<string, unknown> = Record<string, unknown>, EC extends
|
|
321
|
+
export type PluginFieldBuilderParams<M extends Record<string, unknown> = Record<string, unknown>, EC extends CollectionConfig<M> = CollectionConfig<M>> = {
|
|
322
322
|
fieldConfigId: string;
|
|
323
323
|
propertyKey: string;
|
|
324
324
|
property: Property;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, engine-agnostic policy expressions.
|
|
3
|
+
*
|
|
4
|
+
* A {@link PolicyExpression} is the single source of truth for a row-level
|
|
5
|
+
* security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL
|
|
6
|
+
* (authoritative enforcement) and independently evaluated in JavaScript (to
|
|
7
|
+
* drive the admin UI, and — in future — to enforce on engines without native
|
|
8
|
+
* RLS such as MongoDB). Because both the SQL and the JS decision derive from
|
|
9
|
+
* the *same* expression, the UI matches database enforcement by construction —
|
|
10
|
+
* no drift between two hand-written implementations.
|
|
11
|
+
*
|
|
12
|
+
* The only escape hatch that cannot be evaluated client-side is the
|
|
13
|
+
* {@link RawPolicyExpression} node (`{ kind: "raw" }`): it preserves full
|
|
14
|
+
* PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the
|
|
15
|
+
* JavaScript evaluator (never silently allowed) and reflected exactly in the UI
|
|
16
|
+
* via server-computed capability flags.
|
|
17
|
+
*
|
|
18
|
+
* @group Models
|
|
19
|
+
*/
|
|
20
|
+
export type PolicyExpression =
|
|
21
|
+
| TruePolicyExpression
|
|
22
|
+
| FalsePolicyExpression
|
|
23
|
+
| AndPolicyExpression
|
|
24
|
+
| OrPolicyExpression
|
|
25
|
+
| NotPolicyExpression
|
|
26
|
+
| ComparePolicyExpression
|
|
27
|
+
| RolesOverlapPolicyExpression
|
|
28
|
+
| RolesContainPolicyExpression
|
|
29
|
+
| AuthenticatedPolicyExpression
|
|
30
|
+
| ExistsInPolicyExpression
|
|
31
|
+
| RawPolicyExpression;
|
|
32
|
+
|
|
33
|
+
/** Always allows. Compiles to `true`. @group Models */
|
|
34
|
+
export interface TruePolicyExpression {
|
|
35
|
+
kind: "true";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Always denies. Compiles to `false`. @group Models */
|
|
39
|
+
export interface FalsePolicyExpression {
|
|
40
|
+
kind: "false";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Logical AND — every operand must pass. @group Models */
|
|
44
|
+
export interface AndPolicyExpression {
|
|
45
|
+
kind: "and";
|
|
46
|
+
operands: readonly PolicyExpression[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Logical OR — at least one operand must pass. @group Models */
|
|
50
|
+
export interface OrPolicyExpression {
|
|
51
|
+
kind: "or";
|
|
52
|
+
operands: readonly PolicyExpression[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Logical negation. @group Models */
|
|
56
|
+
export interface NotPolicyExpression {
|
|
57
|
+
kind: "not";
|
|
58
|
+
operand: PolicyExpression;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */
|
|
62
|
+
export type PolicyCompareOperator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compares two operands, e.g. `owner_id = auth.uid()`.
|
|
66
|
+
* @group Models
|
|
67
|
+
*/
|
|
68
|
+
export interface ComparePolicyExpression {
|
|
69
|
+
kind: "compare";
|
|
70
|
+
op: PolicyCompareOperator;
|
|
71
|
+
left: PolicyOperand;
|
|
72
|
+
right: PolicyOperand;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when the user holds *at least one* of the given application roles.
|
|
77
|
+
* Compiles to `string_to_array(auth.roles(), ',') && ARRAY[...]`.
|
|
78
|
+
* @group Models
|
|
79
|
+
*/
|
|
80
|
+
export interface RolesOverlapPolicyExpression {
|
|
81
|
+
kind: "rolesOverlap";
|
|
82
|
+
roles: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* True when the user holds *all* of the given application roles.
|
|
87
|
+
* Compiles to `string_to_array(auth.roles(), ',') @> ARRAY[...]`.
|
|
88
|
+
* @group Models
|
|
89
|
+
*/
|
|
90
|
+
export interface RolesContainPolicyExpression {
|
|
91
|
+
kind: "rolesContain";
|
|
92
|
+
roles: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* True when there is an authenticated user (`auth.uid() IS NOT NULL`).
|
|
97
|
+
* @group Models
|
|
98
|
+
*/
|
|
99
|
+
export interface AuthenticatedPolicyExpression {
|
|
100
|
+
kind: "authenticated";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Membership / relational access: true when at least one row exists in another
|
|
105
|
+
* collection (a join/membership table) matching `where`. This is what lets you
|
|
106
|
+
* scope reads to "rows whose team the caller belongs to" without an N+1
|
|
107
|
+
* per-row lookup — it compiles to a single correlated `EXISTS` subquery.
|
|
108
|
+
*
|
|
109
|
+
* Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column
|
|
110
|
+
* of the joined collection, while {@link OuterFieldPolicyOperand}
|
|
111
|
+
* (`policy.outerField`) references a column of the row being checked (the outer
|
|
112
|
+
* table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to
|
|
113
|
+
* the caller.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* // documents visible only to members of the document's team:
|
|
118
|
+
* policy.existsIn({
|
|
119
|
+
* collection: "team_members",
|
|
120
|
+
* where: policy.and(
|
|
121
|
+
* policy.compare(policy.field("team_id"), "eq", policy.outerField("team_id")),
|
|
122
|
+
* policy.compare(policy.field("user_id"), "eq", policy.authUid()),
|
|
123
|
+
* ),
|
|
124
|
+
* })
|
|
125
|
+
* // → EXISTS (SELECT 1 FROM team_members _ex0
|
|
126
|
+
* // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = auth.uid())
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript
|
|
130
|
+
* evaluator treats it as *unknown* (it cannot run a subquery client-side), so
|
|
131
|
+
* enforcement is always the database's.
|
|
132
|
+
* @group Models
|
|
133
|
+
*/
|
|
134
|
+
export interface ExistsInPolicyExpression {
|
|
135
|
+
kind: "existsIn";
|
|
136
|
+
/** Slug of the collection to search (the join / membership table). */
|
|
137
|
+
collection: string;
|
|
138
|
+
/** Condition evaluated against the joined collection's rows. */
|
|
139
|
+
where: PolicyExpression;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A raw PostgreSQL boolean expression — the full-power escape hatch.
|
|
144
|
+
*
|
|
145
|
+
* Columns can be referenced as `{column_name}`. This is Postgres-only and
|
|
146
|
+
* **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary
|
|
147
|
+
* SQL, so it treats this node as *unknown* rather than guessing.
|
|
148
|
+
* @group Models
|
|
149
|
+
*/
|
|
150
|
+
export interface RawPolicyExpression {
|
|
151
|
+
kind: "raw";
|
|
152
|
+
sql: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* An operand referenced by a {@link ComparePolicyExpression}.
|
|
157
|
+
* @group Models
|
|
158
|
+
*/
|
|
159
|
+
export type PolicyOperand =
|
|
160
|
+
| FieldPolicyOperand
|
|
161
|
+
| OuterFieldPolicyOperand
|
|
162
|
+
| LiteralPolicyOperand
|
|
163
|
+
| AuthUidPolicyOperand
|
|
164
|
+
| AuthRolesPolicyOperand;
|
|
165
|
+
|
|
166
|
+
/** A column value on the row being evaluated. @group Models */
|
|
167
|
+
export interface FieldPolicyOperand {
|
|
168
|
+
kind: "field";
|
|
169
|
+
/** The property/column name (resolved to its DB column when compiled). */
|
|
170
|
+
name: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}
|
|
175
|
+
* — i.e. the row the RLS policy is being evaluated for, referenced from within the
|
|
176
|
+
* subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.
|
|
177
|
+
* @group Models
|
|
178
|
+
*/
|
|
179
|
+
export interface OuterFieldPolicyOperand {
|
|
180
|
+
kind: "outerField";
|
|
181
|
+
/** The property/column name on the outer collection. */
|
|
182
|
+
name: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A constant value. @group Models */
|
|
186
|
+
export interface LiteralPolicyOperand {
|
|
187
|
+
kind: "literal";
|
|
188
|
+
value: string | number | boolean | null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The current user's id — compiles to `auth.uid()`. @group Models */
|
|
192
|
+
export interface AuthUidPolicyOperand {
|
|
193
|
+
kind: "authUid";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The current user's roles as an array — compiles to
|
|
198
|
+
* `string_to_array(auth.roles(), ',')`.
|
|
199
|
+
* @group Models
|
|
200
|
+
*/
|
|
201
|
+
export interface AuthRolesPolicyOperand {
|
|
202
|
+
kind: "authRoles";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Constructor helpers ──────────────────────────────────────────────
|
|
206
|
+
// Small, dependency-free builders so callers (and the desugaring in
|
|
207
|
+
// `@rebasepro/common`) can assemble expressions without object-literal noise.
|
|
208
|
+
|
|
209
|
+
/** @group Models */
|
|
210
|
+
export const policy = {
|
|
211
|
+
true: (): TruePolicyExpression => ({ kind: "true" }),
|
|
212
|
+
false: (): FalsePolicyExpression => ({ kind: "false" }),
|
|
213
|
+
and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: "and", operands: operands as PolicyExpression[] }),
|
|
214
|
+
or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: "or", operands: operands as PolicyExpression[] }),
|
|
215
|
+
not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: "not", operand }),
|
|
216
|
+
compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>
|
|
217
|
+
({ kind: "compare", op, left, right }),
|
|
218
|
+
rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: "rolesOverlap", roles: roles as string[] }),
|
|
219
|
+
rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: "rolesContain", roles: roles as string[] }),
|
|
220
|
+
authenticated: (): AuthenticatedPolicyExpression => ({ kind: "authenticated" }),
|
|
221
|
+
existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>
|
|
222
|
+
({ kind: "existsIn", collection: args.collection, where: args.where }),
|
|
223
|
+
raw: (sql: string): RawPolicyExpression => ({ kind: "raw", sql }),
|
|
224
|
+
field: (name: string): FieldPolicyOperand => ({ kind: "field", name }),
|
|
225
|
+
outerField: (name: string): OuterFieldPolicyOperand => ({ kind: "outerField", name }),
|
|
226
|
+
literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: "literal", value }),
|
|
227
|
+
authUid: (): AuthUidPolicyOperand => ({ kind: "authUid" }),
|
|
228
|
+
authRoles: (): AuthRolesPolicyOperand => ({ kind: "authRoles" })
|
|
229
|
+
};
|