@rebasepro/types 0.13.1-canary.gef9608c → 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/dist/call_context.d.ts +70 -4
- package/dist/controllers/client.d.ts +47 -74
- package/dist/controllers/data.d.ts +345 -33
- package/dist/controllers/data_driver.d.ts +106 -8
- package/dist/errors.d.ts +30 -4
- package/dist/index.es.js +217 -12
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +38 -1
- package/dist/types/auth_adapter.d.ts +7 -0
- package/dist/types/backend.d.ts +2 -0
- package/dist/types/collections.d.ts +46 -8
- package/dist/types/cron.d.ts +50 -9
- package/dist/types/entities.d.ts +11 -0
- package/dist/types/entity_callbacks.d.ts +2 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/policy.d.ts +13 -13
- package/dist/types/properties.d.ts +31 -6
- package/dist/types/rls-functions.d.ts +84 -0
- package/dist/types/search.d.ts +231 -0
- package/package.json +1 -1
- package/src/call_context.ts +68 -4
- package/src/controllers/client.ts +47 -95
- package/src/controllers/data.ts +356 -33
- package/src/controllers/data_driver.ts +144 -11
- package/src/errors.ts +43 -4
- package/src/types/admin_block.ts +84 -0
- package/src/types/auth_adapter.ts +7 -0
- package/src/types/backend.ts +2 -0
- package/src/types/collections.ts +47 -8
- package/src/types/cron.ts +51 -9
- package/src/types/entities.ts +12 -0
- package/src/types/entity_callbacks.ts +2 -1
- package/src/types/filter-operators.ts +17 -1
- package/src/types/index.ts +2 -0
- package/src/types/policy.ts +13 -13
- package/src/types/properties.ts +32 -6
- package/src/types/rls-functions.ts +98 -0
- package/src/types/search.ts +247 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RebaseApiError } from "../errors";
|
|
1
2
|
import type { EntityStatus, EntityValues } from "../types/entities";
|
|
2
3
|
import type { CollectionConfig, FilterValues } from "../types/collections";
|
|
3
4
|
import type { RebaseCallContext } from "../call_context";
|
|
@@ -37,7 +38,7 @@ export interface VectorSearchParams {
|
|
|
37
38
|
export declare const DEFAULT_LIST_LIMIT = 50;
|
|
38
39
|
/** Rows returned for a vector-search list read when the client sends no `limit`. */
|
|
39
40
|
export declare const DEFAULT_VECTOR_LIST_LIMIT = 10;
|
|
40
|
-
/**
|
|
41
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
41
42
|
export declare const MAX_LIST_LIMIT = 1000;
|
|
42
43
|
/** Overridable bounds for {@link resolveClientListLimit}. */
|
|
43
44
|
export interface ListLimitBounds {
|
|
@@ -45,19 +46,36 @@ export interface ListLimitBounds {
|
|
|
45
46
|
defaultLimit?: number;
|
|
46
47
|
/** Default page size for vector-search reads. */
|
|
47
48
|
vectorDefaultLimit?: number;
|
|
48
|
-
/**
|
|
49
|
+
/** Largest limit a client may ask for. A larger one is rejected, not clamped. */
|
|
49
50
|
maxLimit?: number;
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
54
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
55
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
56
|
+
*
|
|
57
|
+
* @group Errors
|
|
58
|
+
*/
|
|
59
|
+
export declare class ListLimitError extends RebaseApiError {
|
|
60
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
61
|
+
readonly maxLimit: number;
|
|
62
|
+
constructor(message: string, maxLimit: number);
|
|
63
|
+
}
|
|
51
64
|
/**
|
|
52
65
|
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
53
66
|
*
|
|
54
|
-
* -
|
|
55
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
56
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
67
|
+
* - An absent / blank limit falls back to the mode default:
|
|
57
68
|
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
69
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
70
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
71
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
72
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
73
|
+
* tell apart from the whole collection.
|
|
58
74
|
*
|
|
59
75
|
* The return is never `undefined` — no ingress that routes its client limit
|
|
60
76
|
* through this can produce an unbounded read.
|
|
77
|
+
*
|
|
78
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
61
79
|
*/
|
|
62
80
|
export declare function resolveClientListLimit(rawLimit: number | string | null | undefined, opts?: ListLimitBounds & {
|
|
63
81
|
vectorSearch?: boolean;
|
|
@@ -82,6 +100,8 @@ export interface FetchCollectionProps<M extends Record<string, unknown> = Record
|
|
|
82
100
|
startAfter?: unknown;
|
|
83
101
|
orderBy?: string;
|
|
84
102
|
searchString?: string;
|
|
103
|
+
/** Ask each row which declared search field matched — populates `_matches`. */
|
|
104
|
+
searchExplain?: boolean;
|
|
85
105
|
order?: "desc" | "asc";
|
|
86
106
|
/** Vector similarity search configuration */
|
|
87
107
|
vectorSearch?: VectorSearchParams;
|
|
@@ -128,6 +148,25 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
|
|
|
128
148
|
/** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */
|
|
129
149
|
upsert?: boolean;
|
|
130
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
155
|
+
path: string;
|
|
156
|
+
/**
|
|
157
|
+
* The rows to update, each named by its address.
|
|
158
|
+
*
|
|
159
|
+
* Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
|
|
160
|
+
* values and is insert-shaped — `saveMany` passes `status: "new"` and no
|
|
161
|
+
* `id`, so it cannot express "update exactly this row". This can, and it is
|
|
162
|
+
* why bulk update is a separate driver method rather than a flag on that one.
|
|
163
|
+
*/
|
|
164
|
+
updates: {
|
|
165
|
+
id: string | number;
|
|
166
|
+
values: Partial<EntityValues<M>>;
|
|
167
|
+
}[];
|
|
168
|
+
collection?: CollectionConfig<M>;
|
|
169
|
+
}
|
|
131
170
|
/**
|
|
132
171
|
* @internal
|
|
133
172
|
*/
|
|
@@ -139,6 +178,14 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
|
|
|
139
178
|
};
|
|
140
179
|
collection?: CollectionConfig<M>;
|
|
141
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
184
|
+
export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
185
|
+
path: string;
|
|
186
|
+
ids: (string | number)[];
|
|
187
|
+
collection?: CollectionConfig<M>;
|
|
188
|
+
}
|
|
142
189
|
export type FilterCombinationValidProps = {
|
|
143
190
|
path: string;
|
|
144
191
|
databaseId?: string;
|
|
@@ -211,9 +258,51 @@ export interface DataDriver {
|
|
|
211
258
|
*/
|
|
212
259
|
saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
213
260
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
261
|
+
* Update many rows in one transaction, each addressed by id.
|
|
262
|
+
*
|
|
263
|
+
* Optional for the same reason `saveMany` is: a driver that cannot make the
|
|
264
|
+
* batch atomic should not pretend to. The REST layer reports
|
|
265
|
+
* `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
|
|
266
|
+
* writes, which would be neither atomic nor one round trip — the two things
|
|
267
|
+
* a caller reaches for a batch to get.
|
|
268
|
+
*/
|
|
269
|
+
updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
270
|
+
/**
|
|
271
|
+
* Delete the row `props.row` addresses.
|
|
272
|
+
*
|
|
273
|
+
* **Resolving means the row is gone because this call removed it.** A
|
|
274
|
+
* delete that matched nothing must reject with a not-found error
|
|
275
|
+
* (`ApiError.notFound`, `statusCode: 404`) rather than resolving quietly.
|
|
276
|
+
*
|
|
277
|
+
* The rule is here rather than in each driver because the two
|
|
278
|
+
* implementations answered differently and each had a test pinning its own
|
|
279
|
+
* habit: Postgres threw, Mongo logged a warning and resolved. Three things
|
|
280
|
+
* decide it in favour of rejecting.
|
|
281
|
+
*
|
|
282
|
+
* The REST layer already says 404 — `DELETE /api/data/<c>/<id>` reads the
|
|
283
|
+
* row before removing it — so a quiet resolve made the driver API disagree
|
|
284
|
+
* with the HTTP API about the same operation, and only in-process
|
|
285
|
+
* `rebase.data` callers could see the difference.
|
|
286
|
+
*
|
|
287
|
+
* A caller cannot tell "deleted" from "there was nothing there" without it,
|
|
288
|
+
* and those are different facts: one means the caller's model of the data
|
|
289
|
+
* was right, the other that it was stale. Silence hands back the wrong one
|
|
290
|
+
* and the caller carries on.
|
|
291
|
+
*
|
|
292
|
+
* And on a driver with row-level security, "matched nothing" is *also* how
|
|
293
|
+
* a policy refusal arrives — Postgres filters `DELETE` through `USING`
|
|
294
|
+
* rather than raising. A driver that resolves on zero rows therefore
|
|
295
|
+
* reports a refused delete as a completed one, which is the defect
|
|
296
|
+
* `explainZeroRowWrite` exists to prevent (see `write-denial.ts`).
|
|
297
|
+
*
|
|
298
|
+
* Conformance for both server drivers lives in
|
|
299
|
+
* `packages/server/test/contract/delete-contract.ts`, run by each driver's
|
|
300
|
+
* own suite against its own database. `packages/firebase`'s Firestore
|
|
301
|
+
* driver does not honour it: `deleteDoc` resolves for a missing document
|
|
302
|
+
* and reporting otherwise would cost a read on every delete. It runs in the
|
|
303
|
+
* browser against Firestore's own semantics rather than behind
|
|
304
|
+
* `rebase.data`, and that exception is stated here rather than left to be
|
|
305
|
+
* discovered.
|
|
217
306
|
*/
|
|
218
307
|
delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;
|
|
219
308
|
/**
|
|
@@ -221,6 +310,13 @@ export interface DataDriver {
|
|
|
221
310
|
* @param path Collection path
|
|
222
311
|
*/
|
|
223
312
|
deleteAll?(path: string): Promise<void>;
|
|
313
|
+
/**
|
|
314
|
+
* Delete many rows in one transaction, addressed by id.
|
|
315
|
+
*
|
|
316
|
+
* Ids rather than a filter, deliberately — see
|
|
317
|
+
* {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
|
|
318
|
+
*/
|
|
319
|
+
deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
|
|
224
320
|
/**
|
|
225
321
|
* Check if the given property is unique in the given collection
|
|
226
322
|
* @param path Collection path
|
|
@@ -303,6 +399,8 @@ export interface RestFetchService {
|
|
|
303
399
|
offset?: number;
|
|
304
400
|
startAfter?: Record<string, unknown>;
|
|
305
401
|
searchString?: string;
|
|
402
|
+
/** Ask each row which declared search fields matched — populates `_matches`. */
|
|
403
|
+
searchExplain?: boolean;
|
|
306
404
|
databaseId?: string;
|
|
307
405
|
vectorSearch?: VectorSearchParams;
|
|
308
406
|
}, include?: string[]): Promise<Record<string, unknown>[]>;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error codes every route can produce, as `RebaseApiError.code`.
|
|
3
|
+
*
|
|
4
|
+
* These are the defaults on `ApiError`'s static constructors server-side, so
|
|
5
|
+
* any endpoint can answer with one. They are **not** the complete set: routes
|
|
6
|
+
* pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,
|
|
7
|
+
* `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.
|
|
8
|
+
*
|
|
9
|
+
* Hence the union is deliberately open rather than closed. It exists to give
|
|
10
|
+
* autocomplete and to catch a typo in the common cases — `code` was a bare
|
|
11
|
+
* `string`, so `e.code === "NOT_FOUND"` and `e.code === "NOTFOUND"` were
|
|
12
|
+
* equally valid and only one of them worked. Closing it would be a lie that
|
|
13
|
+
* broke the moment a route added a code.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* if (e instanceof RebaseApiError) {
|
|
17
|
+
* switch (e.code) {
|
|
18
|
+
* case "NOT_FOUND": return null; // completed
|
|
19
|
+
* case "FORBIDDEN": return redirect();
|
|
20
|
+
* default: throw e; // routes' own codes land here
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @group Errors
|
|
25
|
+
*/
|
|
26
|
+
export type RebaseErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "DB_PERMISSION_DENIED" | "SCHEMA_DRIFT" | (string & {});
|
|
1
27
|
/**
|
|
2
28
|
* Structured initializer for {@link RebaseApiError}.
|
|
3
29
|
*
|
|
@@ -10,8 +36,8 @@ export interface RebaseErrorInit {
|
|
|
10
36
|
* logic errors that have no HTTP status.
|
|
11
37
|
*/
|
|
12
38
|
status?: number;
|
|
13
|
-
/** Stable, machine-readable error code
|
|
14
|
-
code?:
|
|
39
|
+
/** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
|
|
40
|
+
code?: RebaseErrorCode;
|
|
15
41
|
/** Structured error payload returned by the server, when present. */
|
|
16
42
|
details?: unknown;
|
|
17
43
|
/** The underlying error this one wraps, if any. */
|
|
@@ -44,8 +70,8 @@ export interface RebaseErrorInit {
|
|
|
44
70
|
export declare class RebaseApiError extends Error {
|
|
45
71
|
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
46
72
|
readonly status?: number;
|
|
47
|
-
/** Stable machine-readable error code, when the server supplied one. */
|
|
48
|
-
readonly code?:
|
|
73
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
74
|
+
readonly code?: RebaseErrorCode;
|
|
49
75
|
/** Structured error payload from the server, when present. */
|
|
50
76
|
readonly details?: unknown;
|
|
51
77
|
constructor(message: string, init?: RebaseErrorInit);
|
package/dist/index.es.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
var RebaseApiError = class extends Error {
|
|
27
27
|
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
28
28
|
status;
|
|
29
|
-
/** Stable machine-readable error code, when the server supplied one. */
|
|
29
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
30
30
|
code;
|
|
31
31
|
/** Structured error payload from the server, when present. */
|
|
32
32
|
details;
|
|
@@ -250,6 +250,19 @@ var ALL_WHERE_FILTER_OPS = [
|
|
|
250
250
|
/** All canonical operator strings for runtime validation. */
|
|
251
251
|
var CANONICAL_OPS = new Set(ALL_WHERE_FILTER_OPS);
|
|
252
252
|
/**
|
|
253
|
+
* The REST table as a `Map`, because the key `toCanonicalOp` is handed comes
|
|
254
|
+
* off the wire.
|
|
255
|
+
*
|
|
256
|
+
* Indexed as a plain object, every `Object.prototype` member answered:
|
|
257
|
+
* `toCanonicalOp("valueOf")` returned the inherited *function* as though it
|
|
258
|
+
* were a `WhereFilterOp`, and every caller here treats a defined result as
|
|
259
|
+
* "known operator". Same defect the REST codec's own lookup tables were
|
|
260
|
+
* converted away from in `filter-dialect.ts`; this is the copy that survived
|
|
261
|
+
* one package over, and it now sits under the operator validation the REST
|
|
262
|
+
* parser does, which would otherwise have admitted `["constructor", x]`.
|
|
263
|
+
*/
|
|
264
|
+
var REST_OP_LOOKUP = new Map(Object.entries(REST_TO_CANONICAL));
|
|
265
|
+
/**
|
|
253
266
|
* Resolve any operator string (canonical or REST short-code) to its
|
|
254
267
|
* canonical `WhereFilterOp` form. Returns `undefined` for unknown operators.
|
|
255
268
|
*
|
|
@@ -261,7 +274,7 @@ var CANONICAL_OPS = new Set(ALL_WHERE_FILTER_OPS);
|
|
|
261
274
|
*/
|
|
262
275
|
function toCanonicalOp(op) {
|
|
263
276
|
if (CANONICAL_OPS.has(op)) return op;
|
|
264
|
-
return
|
|
277
|
+
return REST_OP_LOOKUP.get(op);
|
|
265
278
|
}
|
|
266
279
|
//#endregion
|
|
267
280
|
//#region src/types/admin_block.ts
|
|
@@ -318,6 +331,7 @@ var ADMIN_COLLECTION_KEYS = [
|
|
|
318
331
|
"formAutoSave",
|
|
319
332
|
"formView",
|
|
320
333
|
"group",
|
|
334
|
+
"hideFromEntityViews",
|
|
321
335
|
"hideFromNavigation",
|
|
322
336
|
"hideIdFromCollection",
|
|
323
337
|
"hideIdFromForm",
|
|
@@ -382,6 +396,64 @@ var ADMIN_PROPERTY_KEYS = [
|
|
|
382
396
|
"urlPreview",
|
|
383
397
|
"widget"
|
|
384
398
|
];
|
|
399
|
+
/**
|
|
400
|
+
* Move flattened admin keys back down into the `admin` block.
|
|
401
|
+
*
|
|
402
|
+
* The admin panel works with a *flat* view model — the block merged onto the
|
|
403
|
+
* collection — so what comes back from a form has `icon` and `defaultViewMode`
|
|
404
|
+
* at the top level while `admin` still holds whatever the file was loaded with.
|
|
405
|
+
* This is the way back.
|
|
406
|
+
*
|
|
407
|
+
* **The top-level value wins.** It is the one the form just wrote; the block is
|
|
408
|
+
* the copy the collection was loaded with, and preferring it resolves every edit
|
|
409
|
+
* in favour of the value the user changed away from.
|
|
410
|
+
*
|
|
411
|
+
* This lives here, next to the key lists, because it had two implementations —
|
|
412
|
+
* `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in
|
|
413
|
+
* `@rebasepro/server`'s schema editor — that agreed on everything except that
|
|
414
|
+
* precedence, which is the only part that decides whether a save is visible.
|
|
415
|
+
*
|
|
416
|
+
* @group Models
|
|
417
|
+
*/
|
|
418
|
+
function nestAdminKeysOf(source, adminKeys) {
|
|
419
|
+
const keys = new Set(adminKeys);
|
|
420
|
+
const top = {};
|
|
421
|
+
const block = { ...source.admin ?? {} };
|
|
422
|
+
for (const [key, value] of Object.entries(source)) {
|
|
423
|
+
if (key === "admin") continue;
|
|
424
|
+
if (keys.has(key)) block[key] = value;
|
|
425
|
+
else top[key] = value;
|
|
426
|
+
}
|
|
427
|
+
if (Object.keys(block).length > 0) top.admin = block;
|
|
428
|
+
return top;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* {@link nestAdminKeysOf} for a collection.
|
|
432
|
+
*
|
|
433
|
+
* @group Models
|
|
434
|
+
*/
|
|
435
|
+
function nestAdminCollectionKeys(collection) {
|
|
436
|
+
return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* {@link nestAdminKeysOf} for a property, applied to its children too.
|
|
440
|
+
*
|
|
441
|
+
* A map property carries `properties`, an array property carries `of`, and both
|
|
442
|
+
* hold properties with `admin` blocks of their own. A flat `readOnly` left on a
|
|
443
|
+
* child is as dead — and as fatal at the next boot — as one left on the parent,
|
|
444
|
+
* so the walk goes all the way down.
|
|
445
|
+
*
|
|
446
|
+
* @group Models
|
|
447
|
+
*/
|
|
448
|
+
function nestAdminPropertyKeys(property) {
|
|
449
|
+
const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);
|
|
450
|
+
const children = nested.properties;
|
|
451
|
+
if (children && typeof children === "object" && !Array.isArray(children)) nested.properties = Object.fromEntries(Object.entries(children).map(([key, child]) => [key, child && typeof child === "object" && !Array.isArray(child) ? nestAdminPropertyKeys(child) : child]));
|
|
452
|
+
const of = nested.of;
|
|
453
|
+
if (Array.isArray(of)) nested.of = of.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? nestAdminPropertyKeys(entry) : entry);
|
|
454
|
+
else if (of && typeof of === "object") nested.of = nestAdminPropertyKeys(of);
|
|
455
|
+
return nested;
|
|
456
|
+
}
|
|
385
457
|
//#endregion
|
|
386
458
|
//#region src/types/data_source.ts
|
|
387
459
|
/**
|
|
@@ -578,6 +650,22 @@ function getDeclaredSubcollections(collection) {
|
|
|
578
650
|
return collection.subcollections;
|
|
579
651
|
}
|
|
580
652
|
//#endregion
|
|
653
|
+
//#region src/types/search.ts
|
|
654
|
+
/** The column name used when {@link SearchConfig.column} is not given. */
|
|
655
|
+
var DEFAULT_SEARCH_COLUMN = "search_vector";
|
|
656
|
+
/** The text search configuration used when {@link SearchConfig.language} is not given. */
|
|
657
|
+
var DEFAULT_SEARCH_LANGUAGE = "simple";
|
|
658
|
+
/** The weight a field carries when it does not name one. */
|
|
659
|
+
var DEFAULT_SEARCH_WEIGHT = "B";
|
|
660
|
+
/** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */
|
|
661
|
+
var DEFAULT_FUZZY_THRESHOLD = .3;
|
|
662
|
+
/**
|
|
663
|
+
* The relevance sort key. Valid only on a collection that declares a
|
|
664
|
+
* {@link SearchConfig} *and* on a query that carries a search string; anywhere
|
|
665
|
+
* else it is an unknown field and the request is refused.
|
|
666
|
+
*/
|
|
667
|
+
var RELEVANCE_SORT_FIELD = "_score";
|
|
668
|
+
//#endregion
|
|
581
669
|
//#region src/types/relations.ts
|
|
582
670
|
/** @group Models */
|
|
583
671
|
function hasForeignKeyOnTarget(relation) {
|
|
@@ -594,7 +682,7 @@ function isToMany(relation) {
|
|
|
594
682
|
//#endregion
|
|
595
683
|
//#region src/types/policy.ts
|
|
596
684
|
/**
|
|
597
|
-
* The id a request without a logged-in user reports as `
|
|
685
|
+
* The id a request without a logged-in user reports as `rebase.uid()`.
|
|
598
686
|
*
|
|
599
687
|
* A user-context request always sets `app.uid`: blank would read back as
|
|
600
688
|
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
@@ -602,7 +690,7 @@ function isToMany(relation) {
|
|
|
602
690
|
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
603
691
|
* is set.
|
|
604
692
|
*
|
|
605
|
-
* The consequence for policy authors is that **`
|
|
693
|
+
* The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
|
|
606
694
|
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
607
695
|
* {@link policy.authenticated} to mean "signed in", and
|
|
608
696
|
* {@link policy.serverContext} to mean "the trusted server context". Do not
|
|
@@ -619,7 +707,7 @@ var ANONYMOUS_USER_ID = "anonymous";
|
|
|
619
707
|
* JavaScript evaluator and the linter were all built on
|
|
620
708
|
* {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
|
|
621
709
|
* callers as `'anon'` — so `policy.authenticated()`, which compiled to
|
|
622
|
-
* `
|
|
710
|
+
* `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
|
|
623
711
|
* sanctioned way to write "signed in" granted to everyone, and the linter
|
|
624
712
|
* flagged the spelling that actually worked as a foreign convention.
|
|
625
713
|
*
|
|
@@ -701,6 +789,96 @@ var policy = {
|
|
|
701
789
|
authRoles: () => ({ kind: "authRoles" })
|
|
702
790
|
};
|
|
703
791
|
//#endregion
|
|
792
|
+
//#region src/types/rls-functions.ts
|
|
793
|
+
/**
|
|
794
|
+
* The SQL helper functions RLS policies call, and the schema they live in.
|
|
795
|
+
*
|
|
796
|
+
* ## One schema, and it is ours
|
|
797
|
+
*
|
|
798
|
+
* Rebase creates exactly one schema in a project's database: `rebase`. These
|
|
799
|
+
* three functions live in it alongside the framework's own tables, and that is
|
|
800
|
+
* the whole contract — a reader can look at a database and know precisely which
|
|
801
|
+
* namespace belongs to the framework and that nothing else was touched.
|
|
802
|
+
*
|
|
803
|
+
* It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
|
|
804
|
+
* `auth`, which is Supabase's name, chosen so that a developer who had written
|
|
805
|
+
* Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
|
|
806
|
+
* name was not Rebase's to take, and taking it had a concrete cost: pointing
|
|
807
|
+
* Rebase at a database that already had a Supabase `auth` schema meant
|
|
808
|
+
* `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
|
|
809
|
+
* `RETURNS uuid`, which Postgres rejects outright —
|
|
810
|
+
*
|
|
811
|
+
* ERROR: cannot change return type of existing function
|
|
812
|
+
* HINT: Use DROP FUNCTION auth.uid() first.
|
|
813
|
+
*
|
|
814
|
+
* — and the failure landed inside a catch-all that logged a warning and carried
|
|
815
|
+
* on, leaving a database with auth tables, no helper functions, and policies
|
|
816
|
+
* calling functions that did not exist. Under `rebase db migrate` the same
|
|
817
|
+
* statements aborted the migration instead.
|
|
818
|
+
*
|
|
819
|
+
* `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
|
|
820
|
+
* schema untouched and gains a `rebase` one, which is what a gradual migration
|
|
821
|
+
* needs.
|
|
822
|
+
*
|
|
823
|
+
* ## Why functions at all, rather than inlining `current_setting`
|
|
824
|
+
*
|
|
825
|
+
* Because the indirection has already been spent once. `uid()` resolves
|
|
826
|
+
* `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
|
|
827
|
+
* rolling deploy — old and new pods serving one database — both eras resolve
|
|
828
|
+
* the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
|
|
829
|
+
* bodies it would have been a rewrite of every policy on every table.
|
|
830
|
+
*
|
|
831
|
+
* ## Why the name is not configurable
|
|
832
|
+
*
|
|
833
|
+
* A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
|
|
834
|
+
* these strings are written into every policy in every database Rebase has
|
|
835
|
+
* provisioned. Everything that reads policies back — the SQL-to-policy parser
|
|
836
|
+
* behind the admin UI, the drift checker, `rls-check` — would have to know the
|
|
837
|
+
* configured value to recognise its own output. One frozen name is the feature.
|
|
838
|
+
*/
|
|
839
|
+
/** The schema Rebase owns. The only schema Rebase creates. */
|
|
840
|
+
var REBASE_SCHEMA = "rebase";
|
|
841
|
+
/**
|
|
842
|
+
* The principal of the current request, as text, or NULL in the server context.
|
|
843
|
+
*
|
|
844
|
+
* Never NULL for a user request — an anonymous one carries
|
|
845
|
+
* {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
|
|
846
|
+
* the trusted server plane and `IS NOT NULL` a tautology.
|
|
847
|
+
*/
|
|
848
|
+
var RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
849
|
+
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
850
|
+
var RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
851
|
+
/** The request's JWT claims as `jsonb`, or `{}`. */
|
|
852
|
+
var RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
|
|
853
|
+
/**
|
|
854
|
+
* The pre-1.0 spellings, for recognising policies and hand-written SQL that
|
|
855
|
+
* predate the move.
|
|
856
|
+
*
|
|
857
|
+
* Kept because policies outlive the server that wrote them: a database migrated
|
|
858
|
+
* by an older release still holds `auth.uid()` in its policy bodies until the
|
|
859
|
+
* next push or boot recompiles them, and anything that reads policies back has
|
|
860
|
+
* to recognise both eras or report the framework's own output as foreign drift.
|
|
861
|
+
* Also used to give a project whose `securityRules` contain raw `auth.uid()` a
|
|
862
|
+
* message naming the replacement, instead of a parse failure.
|
|
863
|
+
*/
|
|
864
|
+
var LEGACY_RLS_SCHEMA = "auth";
|
|
865
|
+
var LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;
|
|
866
|
+
var LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;
|
|
867
|
+
var LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;
|
|
868
|
+
/**
|
|
869
|
+
* Rewrites the pre-1.0 function calls in a fragment of policy SQL.
|
|
870
|
+
*
|
|
871
|
+
* Deliberately anchored on a word boundary and the schema qualifier, so a column
|
|
872
|
+
* called `auth_uid` or a table named `auth` is left alone.
|
|
873
|
+
*/
|
|
874
|
+
function rewriteLegacyRlsFunctions(sql) {
|
|
875
|
+
return sql.replace(/\bauth\.(uid|jwt|roles)\s*\(\s*\)/gi, (_match, fn) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`);
|
|
876
|
+
}
|
|
877
|
+
/** Whether a fragment of SQL still calls the pre-1.0 functions. */
|
|
878
|
+
function usesLegacyRlsFunctions(sql) {
|
|
879
|
+
return /\bauth\.(uid|jwt|roles)\s*\(\s*\)/i.test(sql);
|
|
880
|
+
}
|
|
881
|
+
//#endregion
|
|
704
882
|
//#region src/types/backend.ts
|
|
705
883
|
/**
|
|
706
884
|
* Type guard: does this admin support SQL operations?
|
|
@@ -1132,24 +1310,51 @@ function computeSchemaVersion(collections) {
|
|
|
1132
1310
|
var DEFAULT_LIST_LIMIT = 50;
|
|
1133
1311
|
/** Rows returned for a vector-search list read when the client sends no `limit`. */
|
|
1134
1312
|
var DEFAULT_VECTOR_LIST_LIMIT = 10;
|
|
1135
|
-
/**
|
|
1313
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
1136
1314
|
var MAX_LIST_LIMIT = 1e3;
|
|
1137
1315
|
/**
|
|
1316
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
1317
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
1318
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
1319
|
+
*
|
|
1320
|
+
* @group Errors
|
|
1321
|
+
*/
|
|
1322
|
+
var ListLimitError = class ListLimitError extends RebaseApiError {
|
|
1323
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
1324
|
+
maxLimit;
|
|
1325
|
+
constructor(message, maxLimit) {
|
|
1326
|
+
super(message, {
|
|
1327
|
+
status: 400,
|
|
1328
|
+
code: "INVALID_LIMIT"
|
|
1329
|
+
});
|
|
1330
|
+
this.name = "ListLimitError";
|
|
1331
|
+
this.maxLimit = maxLimit;
|
|
1332
|
+
Object.setPrototypeOf(this, ListLimitError.prototype);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
/**
|
|
1138
1336
|
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
1139
1337
|
*
|
|
1140
|
-
* -
|
|
1141
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
1142
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
1338
|
+
* - An absent / blank limit falls back to the mode default:
|
|
1143
1339
|
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
1340
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
1341
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
1342
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
1343
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
1344
|
+
* tell apart from the whole collection.
|
|
1144
1345
|
*
|
|
1145
1346
|
* The return is never `undefined` — no ingress that routes its client limit
|
|
1146
1347
|
* through this can produce an unbounded read.
|
|
1348
|
+
*
|
|
1349
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
1147
1350
|
*/
|
|
1148
1351
|
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
1149
1352
|
const maxLimit = opts.maxLimit ?? 1e3;
|
|
1150
1353
|
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
1151
|
-
const parsed = typeof rawLimit === "number" ? rawLimit :
|
|
1152
|
-
if (Number.
|
|
1354
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
|
|
1355
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
|
|
1356
|
+
if (parsed > maxLimit) throw new ListLimitError(`\`limit\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows per read and page through the rest with \`offset\` — answering with a smaller page would be indistinguishable from there being no more rows.`, maxLimit);
|
|
1357
|
+
return parsed;
|
|
1153
1358
|
}
|
|
1154
1359
|
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
1155
1360
|
}
|
|
@@ -1181,6 +1386,6 @@ function isPublicStoragePath(path) {
|
|
|
1181
1386
|
return p.startsWith("public/") || p.startsWith(`default/public/`);
|
|
1182
1387
|
}
|
|
1183
1388
|
//#endregion
|
|
1184
|
-
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_LIST_LIMIT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REST_TO_CANONICAL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, serializeCollections, storageEnvSuffix, toCanonicalOp };
|
|
1389
|
+
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, ListLimitError, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, REST_TO_CANONICAL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, rewriteLegacyRlsFunctions, serializeCollections, storageEnvSuffix, toCanonicalOp, usesLegacyRlsFunctions };
|
|
1185
1390
|
|
|
1186
1391
|
//# sourceMappingURL=index.es.js.map
|