@rebasepro/types 0.21.2-canary.g1ea48be → 0.23.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/dist/controllers/client.d.ts +13 -0
- package/dist/controllers/data_driver.d.ts +10 -0
- package/dist/index.es.js +3 -1
- package/dist/index.es.js.map +1 -1
- package/dist/types/auth_adapter.d.ts +34 -0
- package/dist/types/backend.d.ts +69 -2
- package/dist/types/cron.d.ts +5 -2
- package/dist/types/database_adapter.d.ts +6 -2
- package/dist/types/entity_callbacks.d.ts +195 -1
- package/dist/types/postgres_introspection.d.ts +10 -0
- package/dist/types/schema_editing.d.ts +2 -2
- package/dist/types/search.d.ts +60 -4
- package/dist/types/security_rules.d.ts +28 -1
- package/package.json +2 -2
|
@@ -490,6 +490,40 @@ export interface AuthAdapter {
|
|
|
490
490
|
id: string;
|
|
491
491
|
values: Record<string, unknown>;
|
|
492
492
|
}, clearPassword?: string): Promise<UserCreationFinalizeResult>;
|
|
493
|
+
/**
|
|
494
|
+
* Check, and put in stored form, updates to rows of the auth collection
|
|
495
|
+
* that arrive through the data API — `PATCH /<users>/:id`, `PATCH /bulk`,
|
|
496
|
+
* `_batch`, the realtime socket.
|
|
497
|
+
*
|
|
498
|
+
* Those rows are the users, so an update to one is user administration,
|
|
499
|
+
* and has to hold to what the admin user routes hold to: no update that
|
|
500
|
+
* leaves the project without an administrator, and an email stored the
|
|
501
|
+
* way sign-in looks it up. Throws to refuse; the updates are judged
|
|
502
|
+
* together, so a bulk demotion of every admin is refused even though each
|
|
503
|
+
* row alone would pass.
|
|
504
|
+
*
|
|
505
|
+
* If not implemented, the values are written as they arrived.
|
|
506
|
+
*
|
|
507
|
+
* @returns Each update's values, as they should be written, in order.
|
|
508
|
+
*/
|
|
509
|
+
prepareUserUpdates?(updates: ReadonlyArray<{
|
|
510
|
+
uid: string;
|
|
511
|
+
values: Record<string, unknown>;
|
|
512
|
+
}>): Promise<Record<string, unknown>[]>;
|
|
513
|
+
/**
|
|
514
|
+
* Check a deletion of these users through the data API before any row goes,
|
|
515
|
+
* and run the hooks that may veto it (`beforeUserDelete`). Throws to
|
|
516
|
+
* refuse — for one that would leave the project without an administrator,
|
|
517
|
+
* among others.
|
|
518
|
+
*
|
|
519
|
+
* If not implemented, the rows are deleted as any other.
|
|
520
|
+
*/
|
|
521
|
+
prepareUserDeletions?(uids: readonly string[]): Promise<void>;
|
|
522
|
+
/**
|
|
523
|
+
* After those users' rows are deleted through the data API: end their
|
|
524
|
+
* sessions and run the after-delete hooks (`afterUserDelete`).
|
|
525
|
+
*/
|
|
526
|
+
finalizeUserDeletions?(uids: readonly string[]): Promise<void>;
|
|
493
527
|
/**
|
|
494
528
|
* A static secret key for server-to-server / script authentication.
|
|
495
529
|
*
|
package/dist/types/backend.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionConfig, FilterValues, WhereFilterOp } from "./collections.js";
|
|
2
|
+
import type { CollectionCallbacks } from "./entity_callbacks.js";
|
|
2
3
|
import type { OrderByTuple } from "./filter-operators.js";
|
|
3
4
|
import type { LogicalCondition } from "../controllers/data.js";
|
|
4
5
|
import type { AuthAdapter } from "./auth_adapter.js";
|
|
@@ -88,7 +89,14 @@ export interface ConditionBuilder<T = unknown> {
|
|
|
88
89
|
*/
|
|
89
90
|
buildFilterConditions<M extends Record<string, unknown>>(filter: FilterValues<Extract<keyof M, string>>, collectionPath: string, ...args: unknown[]): T[];
|
|
90
91
|
/**
|
|
91
|
-
* Build search conditions for text search
|
|
92
|
+
* Build search conditions for text search.
|
|
93
|
+
*
|
|
94
|
+
* At most one condition comes back, already complete: callers OR what they
|
|
95
|
+
* get, and OR is right across the searchable *fields* but wrong across the
|
|
96
|
+
* *terms* of the search string — a person typing two words means both, and
|
|
97
|
+
* the two halves of a name are two different fields. The empty array still
|
|
98
|
+
* means "nothing here can be searched", which every caller reads as "no
|
|
99
|
+
* rows".
|
|
92
100
|
*/
|
|
93
101
|
buildSearchConditions(searchString: string, properties: Record<string, unknown>, ...args: unknown[]): T[];
|
|
94
102
|
/**
|
|
@@ -307,6 +315,17 @@ export interface CollectionRegistryInterface {
|
|
|
307
315
|
* Get the currently registered global callbacks, if any.
|
|
308
316
|
*/
|
|
309
317
|
getGlobalCallbacks(): any | undefined;
|
|
318
|
+
/**
|
|
319
|
+
* Take the global callbacks declared on `initializeRebaseBackend({ callbacks })`.
|
|
320
|
+
*
|
|
321
|
+
* A driver resolves callbacks from the registry it builds for itself, so
|
|
322
|
+
* this is how the backend's global ones reach it: the coordinator hands
|
|
323
|
+
* them over after `initializeDriver` returns. Optional so a registry for a
|
|
324
|
+
* driver that runs no callbacks still type-checks, but a project that
|
|
325
|
+
* declares global callbacks over a registry without it is refused at boot
|
|
326
|
+
* — hooks that never run look exactly like hooks that passed.
|
|
327
|
+
*/
|
|
328
|
+
setGlobalCallbacks?(callbacks: CollectionCallbacks): void;
|
|
310
329
|
}
|
|
311
330
|
/**
|
|
312
331
|
* Abstract data transformer interface.
|
|
@@ -331,11 +350,18 @@ export interface DataTransformer {
|
|
|
331
350
|
export interface SQLAdmin {
|
|
332
351
|
/**
|
|
333
352
|
* Execute raw SQL against the database.
|
|
353
|
+
*
|
|
354
|
+
* `isolateSession` runs it on a connection of its own and resets that
|
|
355
|
+
* connection's session state — role, session authorization, settings —
|
|
356
|
+
* before it is reused. For SQL a person wrote (the Studio editor), which
|
|
357
|
+
* may `SET ROLE` and must not leave that on a pooled connection the
|
|
358
|
+
* server's own queries use next.
|
|
334
359
|
*/
|
|
335
360
|
executeSql(sql: string, options?: {
|
|
336
361
|
database?: string;
|
|
337
362
|
role?: string;
|
|
338
363
|
params?: unknown[];
|
|
364
|
+
isolateSession?: boolean;
|
|
339
365
|
}): Promise<Record<string, unknown>[]>;
|
|
340
366
|
/**
|
|
341
367
|
* Fetch the available databases on the server.
|
|
@@ -560,6 +586,47 @@ export interface BackendInstance extends BackendLifecycle {
|
|
|
560
586
|
* Factory function type for creating backend instances
|
|
561
587
|
*/
|
|
562
588
|
export type BackendFactory<TConfig extends BackendConfig = BackendConfig> = (config: TConfig) => BackendInstance;
|
|
589
|
+
/**
|
|
590
|
+
* A caller of the data API as its rate limiter buckets one outside an HTTP
|
|
591
|
+
* request: a realtime socket's frame, counted in the same bucket the same
|
|
592
|
+
* person's HTTP requests are.
|
|
593
|
+
* @group Backend
|
|
594
|
+
*/
|
|
595
|
+
export interface DataRateLimitCaller {
|
|
596
|
+
/**
|
|
597
|
+
* The signed-in user's uid. Absent, or the anonymous principal, buckets
|
|
598
|
+
* the caller by address at the anonymous allowance.
|
|
599
|
+
*/
|
|
600
|
+
uid?: string;
|
|
601
|
+
/**
|
|
602
|
+
* A header of the request that opened the connection, by lower-case name:
|
|
603
|
+
* the proxy headers an address is read from when proxies are declared.
|
|
604
|
+
*/
|
|
605
|
+
header(name: string): string | undefined;
|
|
606
|
+
/** The address the connection comes from. */
|
|
607
|
+
socketAddress?: string;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The data API's request limits, handed to the realtime socket so its frames
|
|
611
|
+
* carry the ones an HTTP request to the same rows does.
|
|
612
|
+
* @group Backend
|
|
613
|
+
*/
|
|
614
|
+
export interface RealtimeSocketLimits {
|
|
615
|
+
/**
|
|
616
|
+
* The largest frame accepted, in bytes — the data API's body limit. `0` or
|
|
617
|
+
* less for none. Defaults to the server's default body limit.
|
|
618
|
+
*/
|
|
619
|
+
maxPayload?: number;
|
|
620
|
+
/**
|
|
621
|
+
* Count one data request from `caller` in the data API's per-caller
|
|
622
|
+
* buckets and say whether it is allowed; `null` for a caller who is not
|
|
623
|
+
* limited. Absent when the deployment has rate limiting off.
|
|
624
|
+
*/
|
|
625
|
+
dataRateLimit?: (caller: DataRateLimitCaller) => Promise<{
|
|
626
|
+
allowed: boolean;
|
|
627
|
+
retryAfterMs: number;
|
|
628
|
+
} | null>;
|
|
629
|
+
}
|
|
563
630
|
/**
|
|
564
631
|
* A `BackendBootstrapper` encapsulates all driver-specific initialization logic.
|
|
565
632
|
*
|
|
@@ -765,7 +832,7 @@ export interface BackendBootstrapper {
|
|
|
765
832
|
/**
|
|
766
833
|
* Initialize WebSocket server for realtime operations.
|
|
767
834
|
*/
|
|
768
|
-
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver.js").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;
|
|
835
|
+
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver.js").DataDriver, config?: unknown, authAdapter?: AuthAdapter, limits?: RealtimeSocketLimits): Promise<void> | void;
|
|
769
836
|
}
|
|
770
837
|
/**
|
|
771
838
|
* Result of `BackendBootstrapper.initializeDriver()`.
|
package/dist/types/cron.d.ts
CHANGED
|
@@ -43,7 +43,9 @@ export interface CronJobDefinition {
|
|
|
43
43
|
enabled?: boolean;
|
|
44
44
|
/**
|
|
45
45
|
* Maximum number of seconds the handler may run before being
|
|
46
|
-
* considered timed-out. Default: 300 (5 min).
|
|
46
|
+
* considered timed-out. Default: 300 (5 min). `Infinity` means no
|
|
47
|
+
* timeout. Zero, a negative number or `NaN` is refused when the job
|
|
48
|
+
* loads, and the job is listed as rejected.
|
|
47
49
|
*/
|
|
48
50
|
timeoutSeconds?: number;
|
|
49
51
|
/**
|
|
@@ -95,7 +97,8 @@ export interface CronJobContext {
|
|
|
95
97
|
/** A simple logger scoped to this job run. */
|
|
96
98
|
log: (...args: unknown[]) => void;
|
|
97
99
|
/**
|
|
98
|
-
* Aborted when the run exceeds `timeoutSeconds
|
|
100
|
+
* Aborted when the run exceeds `timeoutSeconds`, or when the server
|
|
101
|
+
* shuts down and the run has not finished within the shutdown's wait.
|
|
99
102
|
*
|
|
100
103
|
* The timeout has always stopped the scheduler *waiting* — it loses the
|
|
101
104
|
* race and the run is recorded as failed. It has never stopped the handler:
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { DataDriver } from "../controllers/data_driver.js";
|
|
23
23
|
import type { CollectionConfig } from "./collections.js";
|
|
24
|
-
import type { CollectionRegistryInterface, DatabaseAdmin, InitializedDriver, RealtimeProvider, BootstrappedAuth } from "./backend.js";
|
|
24
|
+
import type { CollectionRegistryInterface, DatabaseAdmin, InitializedDriver, RealtimeProvider, RealtimeSocketLimits, BootstrappedAuth } from "./backend.js";
|
|
25
25
|
import type { HistoryConfig } from "../controllers/client.js";
|
|
26
26
|
/**
|
|
27
27
|
* A `DatabaseAdapter` provides data persistence for Rebase.
|
|
@@ -73,8 +73,12 @@ export interface DatabaseAdapter {
|
|
|
73
73
|
* already passed it, so it was dropped at every adapter that routed through
|
|
74
74
|
* here — turning an adapter-authenticated server's socket into one that
|
|
75
75
|
* accepted every client as already authenticated.
|
|
76
|
+
*
|
|
77
|
+
* `limits` are the data API's body and per-caller rate limits, for the
|
|
78
|
+
* socket's frames: it serves the same rows, and without them a frame had
|
|
79
|
+
* neither. Forwarded for the same reason `adapter` is.
|
|
76
80
|
*/
|
|
77
|
-
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: import("./auth_adapter.js").AuthAdapter): Promise<void> | void;
|
|
81
|
+
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: import("./auth_adapter.js").AuthAdapter, limits?: RealtimeSocketLimits): Promise<void> | void;
|
|
78
82
|
/**
|
|
79
83
|
* Ask the database whether it is there, before boot's first real query. See
|
|
80
84
|
* `BackendBootstrapper.verifyConnection` for the contract.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { CollectionConfig } from "./collections.js";
|
|
1
|
+
import type { CollectionConfig, FilterValues } from "./collections.js";
|
|
2
|
+
import type { OrderByTuple } from "./filter-operators.js";
|
|
2
3
|
import type { EntityStatus, EntityValues } from "./entities.js";
|
|
3
4
|
import type { User } from "../users/index.js";
|
|
4
5
|
import type { RebaseCallContext } from "../call_context.js";
|
|
6
|
+
import type { LogicalCondition } from "../controllers/data.js";
|
|
5
7
|
/**
|
|
6
8
|
* Lifecycle callbacks for entity CRUD operations.
|
|
7
9
|
*
|
|
@@ -25,6 +27,78 @@ export type CollectionCallbacks<M extends Record<string, unknown> = Record<strin
|
|
|
25
27
|
* @param props
|
|
26
28
|
*/
|
|
27
29
|
afterRead?(props: AfterReadProps<M, USER>): Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Callback used **before** a read is compiled, to narrow which rows it asks
|
|
32
|
+
* for.
|
|
33
|
+
*
|
|
34
|
+
* {@link CollectionCallbacks.afterRead} sees rows that have already been
|
|
35
|
+
* fetched, so nothing in userland could influence *which* rows a read
|
|
36
|
+
* requests — a tenant scope, a visibility window, a per-role row filter all
|
|
37
|
+
* had to be either pushed into every call site or written as an RLS policy.
|
|
38
|
+
* This is the hook for that.
|
|
39
|
+
*
|
|
40
|
+
* Return conditions to **add**. They are AND-ed into the query the caller
|
|
41
|
+
* sent, alongside its own `filter`, its `logical` group, the soft-delete
|
|
42
|
+
* condition and the relation scope of a nested path.
|
|
43
|
+
*
|
|
44
|
+
* ### Additive by construction
|
|
45
|
+
*
|
|
46
|
+
* The return type cannot express a removal, a replacement or an `OR` with
|
|
47
|
+
* the caller's own conditions: it is a filter to AND in, and `AND(q, c)` is
|
|
48
|
+
* a subset of `q` for every `c`. That is deliberate and it is the reason
|
|
49
|
+
* the hook is shaped this way rather than as `(query) => query`. A hook
|
|
50
|
+
* handed the parsed query and asked to return one could drop a condition,
|
|
51
|
+
* and on an RLS data plane a dropped condition is a widened read — the same
|
|
52
|
+
* reasoning that makes `UnknownFilterFieldsMode` in
|
|
53
|
+
* `@rebasepro/server-postgres` default to `"error"` rather than to dropping
|
|
54
|
+
* what it cannot compile.
|
|
55
|
+
*
|
|
56
|
+
* For the same reason the returned filter is compiled with unknown fields
|
|
57
|
+
* **always** fatal, whatever the process-wide mode is: a scoping condition
|
|
58
|
+
* on a renamed column must refuse the request, never run without it.
|
|
59
|
+
*
|
|
60
|
+
* ### Every read path, no bypass
|
|
61
|
+
*
|
|
62
|
+
* It fires on the listing, the single get, the count, the aggregate, the
|
|
63
|
+
* search, the vector read, the nested-path listing, the realtime refetch
|
|
64
|
+
* that builds subscription frames, and on the rows loaded for a relation or
|
|
65
|
+
* an `include` — where it is the **target** collection's hook that applies,
|
|
66
|
+
* because those are the target's rows. A hook honoured by the listing and
|
|
67
|
+
* not by the count is a page that says "1 of 4 results".
|
|
68
|
+
*
|
|
69
|
+
* Two reads are deliberately not narrowed, and both would be wrong to
|
|
70
|
+
* narrow:
|
|
71
|
+
*
|
|
72
|
+
* - **`checkUniqueField`.** It asks whether a value exists *anywhere* in
|
|
73
|
+
* the table. Narrowed, it would answer "unique" for a value a row the
|
|
74
|
+
* caller cannot see already holds, and the insert would then fail on the
|
|
75
|
+
* constraint instead.
|
|
76
|
+
* - **A write's own pre-read** is narrowed, not exempt: an update or delete
|
|
77
|
+
* addressed at a row this hook excludes answers "not found", which is the
|
|
78
|
+
* same answer the read gives.
|
|
79
|
+
*
|
|
80
|
+
* ### Postgres only, for now
|
|
81
|
+
*
|
|
82
|
+
* Implemented by `@rebasepro/server-postgres`. A collection served by
|
|
83
|
+
* another engine that declares one is **refused at boot** rather than
|
|
84
|
+
* served with the hook silently inert — see `assertBeforeQueryIsPostgresOnly`.
|
|
85
|
+
* That is the same treatment a `search` block gets on a non-Postgres
|
|
86
|
+
* collection, and for the same reason: a security-shaped hook that does
|
|
87
|
+
* nothing is worse than one that is absent.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* callbacks: {
|
|
92
|
+
* beforeQuery: ({ context }) => {
|
|
93
|
+
* if (context.user?.roles?.includes("admin")) return;
|
|
94
|
+
* return { filter: { owner_id: ["==", context.user?.uid ?? null] } };
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @param props
|
|
100
|
+
*/
|
|
101
|
+
beforeQuery?(props: BeforeQueryProps<M, USER>): Promise<QueryNarrowing<M> | void> | QueryNarrowing<M> | void;
|
|
28
102
|
/**
|
|
29
103
|
* Callback used before saving, you need to return the values that will get
|
|
30
104
|
* saved. If you throw an error in this method the process stops, and an
|
|
@@ -83,6 +157,126 @@ export interface AfterReadProps<M extends Record<string, unknown> = Record<strin
|
|
|
83
157
|
*/
|
|
84
158
|
context: RebaseCallContext<USER>;
|
|
85
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Which read a {@link CollectionCallbacks.beforeQuery} hook is narrowing.
|
|
162
|
+
*
|
|
163
|
+
* Named rather than inferred because the four are not interchangeable to a
|
|
164
|
+
* hook that logs, measures or short-circuits: `"count"` and `"aggregate"` serve
|
|
165
|
+
* a total rather than rows, and `"relation"` is the target collection's own
|
|
166
|
+
* hook firing over rows reached from a parent.
|
|
167
|
+
*
|
|
168
|
+
* @group Models
|
|
169
|
+
*/
|
|
170
|
+
export type ReadOperation = "list" | "get" | "count" | "aggregate" | "relation";
|
|
171
|
+
/**
|
|
172
|
+
* The parsed read, as a {@link CollectionCallbacks.beforeQuery} hook sees it.
|
|
173
|
+
*
|
|
174
|
+
* Read-only throughout, and that is the point rather than a courtesy: a hook
|
|
175
|
+
* that could edit this object would be able to *widen* the read, which is the
|
|
176
|
+
* one thing this hook is built not to permit. Mutating it is a compile error;
|
|
177
|
+
* narrowing is what the return value is for.
|
|
178
|
+
*
|
|
179
|
+
* Fields are present exactly when the caller sent them. An absent `filter` is
|
|
180
|
+
* a query with no filter, not an empty one.
|
|
181
|
+
*
|
|
182
|
+
* @group Models
|
|
183
|
+
*/
|
|
184
|
+
export interface ReadQuery<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
185
|
+
/** The caller's own field filter. */
|
|
186
|
+
readonly filter?: Readonly<FilterValues<Extract<keyof M, string>>>;
|
|
187
|
+
/** The caller's `or(...)` / `and(...)` group, if any. */
|
|
188
|
+
readonly logical?: Readonly<LogicalCondition>;
|
|
189
|
+
/** The text typed into a search box, if this is a search. */
|
|
190
|
+
readonly searchString?: string;
|
|
191
|
+
/** Page size, when the caller asked for one. */
|
|
192
|
+
readonly limit?: number;
|
|
193
|
+
/** Offset pagination, when the caller asked for one. */
|
|
194
|
+
readonly offset?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Sort keys, normalized to tuples — one entry per key, in order of
|
|
197
|
+
* significance, whichever of the several authoring forms the caller used.
|
|
198
|
+
*/
|
|
199
|
+
readonly orderBy?: readonly Readonly<OrderByTuple>[];
|
|
200
|
+
/** Column projection, when the caller narrowed it with `fields`. */
|
|
201
|
+
readonly fields?: readonly string[];
|
|
202
|
+
/**
|
|
203
|
+
* The parent this read hangs off, for `operation: "relation"` and for a
|
|
204
|
+
* nested path like `authors/1/posts`.
|
|
205
|
+
*
|
|
206
|
+
* A hook that scopes by tenant usually ignores it. A hook that needs to
|
|
207
|
+
* know it is looking at *someone's* rows rather than all of them does not.
|
|
208
|
+
*/
|
|
209
|
+
readonly relatedTo?: {
|
|
210
|
+
readonly parentSlug: string;
|
|
211
|
+
readonly parentId?: string | number;
|
|
212
|
+
readonly relationName: string;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* What a {@link CollectionCallbacks.beforeQuery} hook returns: conditions to
|
|
217
|
+
* AND into the read.
|
|
218
|
+
*
|
|
219
|
+
* Returning nothing (`undefined`, or no `return` at all) adds nothing and
|
|
220
|
+
* leaves the compiled SQL byte-identical to what it would have been.
|
|
221
|
+
*
|
|
222
|
+
* Both fields are the same declarative language a caller's own query uses, so
|
|
223
|
+
* a relation path (`author.tenant_id`), a JSON path (`meta->>tier`) and every
|
|
224
|
+
* `WhereFilterOp` works here too. There is no raw-SQL member, and that is
|
|
225
|
+
* deliberate: raw SQL could be `OR`-ed against the caller's conditions and so
|
|
226
|
+
* could widen the read.
|
|
227
|
+
*
|
|
228
|
+
* @group Models
|
|
229
|
+
*/
|
|
230
|
+
export interface QueryNarrowing<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
231
|
+
/**
|
|
232
|
+
* A field filter to AND in.
|
|
233
|
+
*
|
|
234
|
+
* Compiled with unknown fields fatal, always: a scope condition that names
|
|
235
|
+
* a column the table does not have refuses the request rather than running
|
|
236
|
+
* without it.
|
|
237
|
+
*/
|
|
238
|
+
filter?: FilterValues<Extract<keyof M, string>>;
|
|
239
|
+
/**
|
|
240
|
+
* An `or(...)` / `and(...)` group to AND in, for a scope that is a
|
|
241
|
+
* disjunction — "mine, or shared with me", say.
|
|
242
|
+
*
|
|
243
|
+
* Still additive: the group is AND-ed into the query as a whole, so an
|
|
244
|
+
* `or` inside it can only ever choose between rows the rest of the query
|
|
245
|
+
* already admits.
|
|
246
|
+
*/
|
|
247
|
+
logical?: LogicalCondition;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Parameters passed to a {@link CollectionCallbacks.beforeQuery} hook.
|
|
251
|
+
*
|
|
252
|
+
* @group Models
|
|
253
|
+
*/
|
|
254
|
+
export interface BeforeQueryProps<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
|
|
255
|
+
/**
|
|
256
|
+
* Collection being read. For `operation: "relation"` this is the
|
|
257
|
+
* **target** collection — the one the rows belong to.
|
|
258
|
+
*/
|
|
259
|
+
collection: CollectionConfig<M>;
|
|
260
|
+
/**
|
|
261
|
+
* Full path being read. Might contain unresolved aliases, and for a nested
|
|
262
|
+
* read it is the target collection's own path rather than the nested one —
|
|
263
|
+
* the nested parent is on {@link ReadQuery.relatedTo}.
|
|
264
|
+
*/
|
|
265
|
+
path: string;
|
|
266
|
+
/** Which read this is. */
|
|
267
|
+
operation: ReadOperation;
|
|
268
|
+
/** The parsed read, read-only. */
|
|
269
|
+
query: ReadQuery<M>;
|
|
270
|
+
/**
|
|
271
|
+
* Context of the app status.
|
|
272
|
+
*
|
|
273
|
+
* `context.data` is the caller's own plane, so a lookup made here is
|
|
274
|
+
* itself RLS-scoped — and it is a read issued from inside a read, on the
|
|
275
|
+
* same connection. Keep it to something cheap and cacheable, or read it
|
|
276
|
+
* once per request outside the hook.
|
|
277
|
+
*/
|
|
278
|
+
context: RebaseCallContext<USER>;
|
|
279
|
+
}
|
|
86
280
|
/**
|
|
87
281
|
* Parameters passed to hooks before a entity is saved
|
|
88
282
|
* @group Models
|
|
@@ -56,8 +56,18 @@ export interface TableJunctionInfo {
|
|
|
56
56
|
*/
|
|
57
57
|
export interface TablePolicyInfo {
|
|
58
58
|
policy_name: string;
|
|
59
|
+
/**
|
|
60
|
+
* The policy's `TO` list — *database* roles, `public` for every
|
|
61
|
+
* connection — as `pg_policies.roles` reports it.
|
|
62
|
+
*/
|
|
59
63
|
roles: string[];
|
|
64
|
+
/** `SELECT`, `INSERT`, `UPDATE`, `DELETE` or `ALL`, as `pg_policies.cmd` reports it. */
|
|
60
65
|
cmd: string;
|
|
66
|
+
/**
|
|
67
|
+
* `pg_policies.permissive`. Absent means `PERMISSIVE`, Postgres's own
|
|
68
|
+
* default.
|
|
69
|
+
*/
|
|
70
|
+
permissive?: "PERMISSIVE" | "RESTRICTIVE";
|
|
61
71
|
qual?: string;
|
|
62
72
|
with_check?: string;
|
|
63
73
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* changing a type, moving a primary key.
|
|
25
25
|
*/
|
|
26
26
|
export type SchemaChangeVerdict = "safe" | "diverges" | "needs-migration";
|
|
27
|
-
export type SchemaChangeKind = "add-collection" | "remove-collection" | "add-property" | "remove-property" | "change-property-type" | "rename-column" | "add-enum-value" | "remove-enum-value" | "change-required" | "change-primary-key";
|
|
27
|
+
export type SchemaChangeKind = "add-collection" | "remove-collection" | "add-property" | "remove-property" | "change-property-type" | "rename-column" | "add-enum-value" | "remove-enum-value" | "change-required" | "change-primary-key" | "rename-table";
|
|
28
28
|
export interface SchemaChange {
|
|
29
29
|
kind: SchemaChangeKind;
|
|
30
30
|
verdict: SchemaChangeVerdict;
|
|
@@ -106,7 +106,7 @@ export interface SchemaChangePlan {
|
|
|
106
106
|
export interface WithheldSchemaConstraint {
|
|
107
107
|
/** `schema.table.column`. */
|
|
108
108
|
target: string;
|
|
109
|
-
kind: "not-null";
|
|
109
|
+
kind: "not-null" | "unique";
|
|
110
110
|
/** What is in the way, naming the obstacle rather than the rule. */
|
|
111
111
|
reason: string;
|
|
112
112
|
/** What would make it applicable. */
|
package/dist/types/search.d.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## Why this is opt-in
|
|
5
5
|
*
|
|
6
|
-
* Without a `search` block, `.search()`
|
|
7
|
-
*
|
|
8
|
-
* properties
|
|
9
|
-
* this block is the only way
|
|
6
|
+
* Without a `search` block, `.search()` is `ILIKE '%term%'` per whitespace-
|
|
7
|
+
* separated term, OR-ed across the collection's top-level, non-enum `string`
|
|
8
|
+
* properties and AND-ed across the terms — so two typed words may be found in
|
|
9
|
+
* two different columns, as they are here. Declaring this block is the only way
|
|
10
|
+
* to get anything more than that.
|
|
10
11
|
*
|
|
11
12
|
* The default has three limits that no amount of tuning inside it can fix:
|
|
12
13
|
* it cannot reach inside `map` (JSONB) or `array` properties, it has no notion
|
|
@@ -66,6 +67,53 @@ export interface SearchConfig {
|
|
|
66
67
|
* failure this whole block exists to prevent.
|
|
67
68
|
*/
|
|
68
69
|
fields: readonly (string | SearchField)[];
|
|
70
|
+
/**
|
|
71
|
+
* How a search string is matched against the indexed fields.
|
|
72
|
+
*
|
|
73
|
+
* - `"fts"` (default) — one `@@ websearch_to_tsquery` against the generated
|
|
74
|
+
* `tsvector`. Stems, drops stopwords, reaches inside JSONB and arrays,
|
|
75
|
+
* uses the GIN index, and ranks. It matches **whole lexemes**, so `seb`
|
|
76
|
+
* does not find `sebastian` and `audit` does not find `Auditor`.
|
|
77
|
+
* - `"hybrid"` — that predicate `OR` a substring match over the same
|
|
78
|
+
* declared fields, with accents folded on both sides. So one collection
|
|
79
|
+
* gets accent folding *and* substring/prefix matching, which is what a
|
|
80
|
+
* search box is: measured on five rows in `search-mode-matrix.test.ts`,
|
|
81
|
+
* `munoz` finds `Sebastian Munoz`, `seb` finds both Sebastians, `audit`
|
|
82
|
+
* finds the `ISO 14001 Lead Auditor`, and `iso 14001` does **not** drag
|
|
83
|
+
* in the `ISO 9001` row the way a loose `fuzzy` threshold does.
|
|
84
|
+
*
|
|
85
|
+
* ### Why this is a mode rather than the default
|
|
86
|
+
*
|
|
87
|
+
* The substring half cannot use the GIN index — a leading `%` never can —
|
|
88
|
+
* so it is a scan over the declared fields' text, evaluated per row. The
|
|
89
|
+
* `@@` half still runs first and still uses the index; what the mode costs
|
|
90
|
+
* is the rows the index rejected, which the planner has to look at anyway
|
|
91
|
+
* to apply the `OR`. On a large table that is the difference between an
|
|
92
|
+
* index scan and a sequential one, and that is the author's call to make
|
|
93
|
+
* rather than this default's.
|
|
94
|
+
*
|
|
95
|
+
* ### Changing this on a live collection
|
|
96
|
+
*
|
|
97
|
+
* Safe, and deliberately so. `mode` is **query-side only**: it changes no
|
|
98
|
+
* generated column, no generation expression and no index, so it does not
|
|
99
|
+
* trip the boot-time refusal a changed `search` block otherwise gets
|
|
100
|
+
* (`searchDriftMessage` — rebuilding a STORED generated column rewrites the
|
|
101
|
+
* table under an ACCESS EXCLUSIVE lock). Turning `"hybrid"` on for a
|
|
102
|
+
* collection whose column already exists takes a deploy and nothing else.
|
|
103
|
+
*
|
|
104
|
+
* The accent folding on the substring half is likewise query-side and
|
|
105
|
+
* unconditional under `"hybrid"` — it does **not** require
|
|
106
|
+
* {@link SearchConfig.unaccent}, which is what makes the switch free. What
|
|
107
|
+
* `unaccent` still buys is folding on the `@@` half, where the lexemes are
|
|
108
|
+
* stored, and that one *is* a column rebuild.
|
|
109
|
+
*
|
|
110
|
+
* It does add the `unaccent` extension and one IMMUTABLE helper function to
|
|
111
|
+
* the database if they are not there already. Both are `IF NOT EXISTS` /
|
|
112
|
+
* `CREATE OR REPLACE`, so both are additive and idempotent.
|
|
113
|
+
*
|
|
114
|
+
* @default "fts"
|
|
115
|
+
*/
|
|
116
|
+
mode?: SearchMode;
|
|
69
117
|
/**
|
|
70
118
|
* The Postgres text search configuration, which decides stemming and
|
|
71
119
|
* stopwords. `"spanish"` stems `auditores` to `auditor` and drops `de`;
|
|
@@ -154,6 +202,12 @@ export interface SearchField {
|
|
|
154
202
|
*/
|
|
155
203
|
weight?: SearchWeight;
|
|
156
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* How {@link SearchConfig.mode} matches a search string.
|
|
207
|
+
*
|
|
208
|
+
* @group Search
|
|
209
|
+
*/
|
|
210
|
+
export type SearchMode = "fts" | "hybrid";
|
|
157
211
|
/**
|
|
158
212
|
* Postgres tsvector weight classes, strongest to weakest.
|
|
159
213
|
*
|
|
@@ -162,6 +216,8 @@ export interface SearchField {
|
|
|
162
216
|
export type SearchWeight = "A" | "B" | "C" | "D";
|
|
163
217
|
/** The column name used when {@link SearchConfig.column} is not given. */
|
|
164
218
|
export declare const DEFAULT_SEARCH_COLUMN = "search_vector";
|
|
219
|
+
/** The matching strategy used when {@link SearchConfig.mode} is not given. */
|
|
220
|
+
export declare const DEFAULT_SEARCH_MODE: SearchMode;
|
|
165
221
|
/** The text search configuration used when {@link SearchConfig.language} is not given. */
|
|
166
222
|
export declare const DEFAULT_SEARCH_LANGUAGE = "simple";
|
|
167
223
|
/** The weight a field carries when it does not name one. */
|
|
@@ -53,7 +53,7 @@ export type SecurityOperation = "select" | "insert" | "update" | "delete" | "all
|
|
|
53
53
|
*
|
|
54
54
|
* @group Models
|
|
55
55
|
*/
|
|
56
|
-
export type SecurityRule = OwnerSecurityRule | PublicSecurityRule | StructuredSecurityRule | RawSQLSecurityRule | RolesOnlySecurityRule;
|
|
56
|
+
export type SecurityRule = OwnerSecurityRule | PublicSecurityRule | StructuredSecurityRule | RawSQLSecurityRule | RawSQLCheckOnlySecurityRule | RolesOnlySecurityRule;
|
|
57
57
|
/**
|
|
58
58
|
* Shared fields for all SecurityRule variants.
|
|
59
59
|
* @group Models
|
|
@@ -297,6 +297,33 @@ export interface RawSQLSecurityRule extends SecurityRuleBase {
|
|
|
297
297
|
condition?: never;
|
|
298
298
|
check?: never;
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Security rule with a raw SQL `WITH CHECK` clause and no `USING` clause — the
|
|
302
|
+
* shape of every INSERT policy, which checks the row being written and has no
|
|
303
|
+
* existing row to filter.
|
|
304
|
+
*
|
|
305
|
+
* On an operation that takes a `USING` clause, the missing one grants no rows,
|
|
306
|
+
* as a Postgres policy without one does.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* // Anyone may insert a row they own
|
|
310
|
+
* { operation: "insert", withCheck: "{owner_id} = rebase.uid()" }
|
|
311
|
+
*
|
|
312
|
+
* @group Models
|
|
313
|
+
*/
|
|
314
|
+
export interface RawSQLCheckOnlySecurityRule extends SecurityRuleBase {
|
|
315
|
+
/**
|
|
316
|
+
* Raw SQL expression for the `WITH CHECK` clause.
|
|
317
|
+
* This controls which *new/updated* row values are allowed.
|
|
318
|
+
* Applied to INSERT and UPDATE.
|
|
319
|
+
*/
|
|
320
|
+
withCheck: string;
|
|
321
|
+
using?: never;
|
|
322
|
+
ownerField?: never;
|
|
323
|
+
access?: never;
|
|
324
|
+
condition?: never;
|
|
325
|
+
check?: never;
|
|
326
|
+
}
|
|
300
327
|
/**
|
|
301
328
|
* Security rule that only filters by application roles, without any
|
|
302
329
|
* row-level condition (USING/WITH CHECK).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Rebase type definitions — shared interfaces and controller types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@types/node": "^26.1.2",
|
|
47
47
|
"@types/object-hash": "^3.0.6",
|
|
48
48
|
"@types/react-measure": "^2.0.12",
|
|
49
|
-
"hono": "^4.13.
|
|
49
|
+
"hono": "^4.13.5",
|
|
50
50
|
"jest": "^30.4.2",
|
|
51
51
|
"ts-jest": "^29.4.12",
|
|
52
52
|
"typescript": "^6.0.3",
|