@rebasepro/types 0.13.1-canary.gf57a27e → 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 +17 -8
- package/dist/controllers/client.d.ts +31 -15
- package/dist/controllers/data.d.ts +84 -9
- package/dist/controllers/data_driver.d.ts +58 -8
- package/dist/index.es.js +106 -8
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +37 -0
- package/dist/types/auth_adapter.d.ts +7 -0
- package/dist/types/collections.d.ts +10 -5
- package/dist/types/properties.d.ts +9 -2
- package/package.json +1 -1
- package/src/call_context.ts +17 -8
- package/src/controllers/client.ts +31 -15
- package/src/controllers/data.ts +95 -9
- package/src/controllers/data_driver.ts +95 -11
- package/src/types/admin_block.ts +83 -0
- package/src/types/auth_adapter.ts +7 -0
- package/src/types/collections.ts +10 -5
- package/src/types/filter-operators.ts +17 -1
- package/src/types/properties.ts +9 -2
package/dist/call_context.d.ts
CHANGED
|
@@ -23,11 +23,13 @@ export type RebaseCallContext<USER extends User = User> = {
|
|
|
23
23
|
* Use it to call backend functions, access storage, send email, etc.
|
|
24
24
|
*
|
|
25
25
|
* ⚠️ **Not the same trust level as {@link data}.** Server-side this is the
|
|
26
|
-
* app singleton, so `client.dataAsAdmin` is **always** the
|
|
27
|
-
* plane —
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* app singleton, so `client.dataAsAdmin` is **always** the admin-scoped
|
|
27
|
+
* plane — scoped as `{ uid: "service", roles: ["admin"] }`, so policies are
|
|
28
|
+
* evaluated against that identity rather than skipped — while {@link data},
|
|
29
|
+
* one property over, follows whoever triggered the callback. On a user
|
|
30
|
+
* request, reaching for `context.client.dataAsAdmin` silently escalates a
|
|
31
|
+
* user-scoped operation to admin. For queries in a callback use
|
|
32
|
+
* {@link data}; come here for functions, storage and email.
|
|
31
33
|
*
|
|
32
34
|
* @example
|
|
33
35
|
* // In a beforeSave callback:
|
|
@@ -50,14 +52,21 @@ export type RebaseCallContext<USER extends User = User> = {
|
|
|
50
52
|
* user-scoped. The callback runs on the RLS-bound transaction opened for
|
|
51
53
|
* that request, so policies apply to reads *and* writes — a callback
|
|
52
54
|
* cannot see a row its caller could not.
|
|
53
|
-
* - Triggered by
|
|
54
|
-
*
|
|
55
|
+
* - Triggered by **`rebase.dataAsAdmin` or a cron** (the same singleton):
|
|
56
|
+
* admin-scoped, not unscoped. That driver is scoped as
|
|
57
|
+
* `{ uid: "service", roles: ["admin"] }`, so the callback still runs on an
|
|
58
|
+
* RLS-bound transaction — policies are evaluated against that identity.
|
|
59
|
+
* - Triggered by **the base driver** (auth flows, migrations): unscoped, on
|
|
60
|
+
* the owner connection, bypassing RLS.
|
|
55
61
|
*
|
|
56
62
|
* So a callback that reads a sibling row will find it when an admin task
|
|
57
63
|
* saves and may find nothing when an end user saves — without an error,
|
|
58
64
|
* because RLS filters rather than raises. Write callbacks that tolerate
|
|
59
65
|
* that, or reach for {@link client}`.dataAsAdmin` deliberately when the
|
|
60
|
-
* callback genuinely has to see
|
|
66
|
+
* callback genuinely has to see what an admin may see. Note what that does
|
|
67
|
+
* *not* buy you: `policy.serverContext()` (`auth.uid() IS NULL`) is false
|
|
68
|
+
* for the service identity, so a collection whose only rule is
|
|
69
|
+
* `serverContext()` stays closed to it.
|
|
61
70
|
*
|
|
62
71
|
* Verified end-to-end against Postgres rather than asserted — see
|
|
63
72
|
* `"scopes context.data to the caller when a callback runs on a user
|
|
@@ -296,18 +296,31 @@ export interface RebaseClient<DB = unknown> {
|
|
|
296
296
|
/** Unified Data access layer */
|
|
297
297
|
data: RebaseSdkData<DB>;
|
|
298
298
|
/**
|
|
299
|
-
* Admin-scoped
|
|
299
|
+
* Admin-scoped data accessor — **not** an RLS bypass.
|
|
300
300
|
*
|
|
301
301
|
* Present on the **server** singleton only (see {@link RebaseServerClient}).
|
|
302
|
-
* It runs
|
|
303
|
-
*
|
|
304
|
-
*
|
|
302
|
+
* It runs as the service identity `{ uid: "service", roles: ["admin"] }`,
|
|
303
|
+
* and the driver is scoped with `withAuth()` at boot, so every read and
|
|
304
|
+
* write runs in a transaction that has switched to the restricted
|
|
305
|
+
* `rebase_user` role with `app.uid = 'service'`: policies are evaluated,
|
|
306
|
+
* against that identity. This is the correct tool for trusted background
|
|
307
|
+
* work (cron jobs, migrations, service-to-service tasks).
|
|
308
|
+
*
|
|
309
|
+
* Two consequences the name does not suggest:
|
|
310
|
+
*
|
|
311
|
+
* - `policy.serverContext()` compiles to `auth.uid() IS NULL` and is
|
|
312
|
+
* therefore **false** here. A collection with `disableDefaultPolicies:
|
|
313
|
+
* true` whose only rule is `serverContext()` refuses these writes
|
|
314
|
+
* (`42501`) and returns zero rows — HTTP 200, empty — for these reads.
|
|
315
|
+
* - Its reach equals an `admin`-roled application user's reach. It is not a
|
|
316
|
+
* private channel. The true bypass is {@link sql}, which runs on the
|
|
317
|
+
* owner connection and never goes through `withAuth`.
|
|
305
318
|
*
|
|
306
319
|
* ⚠️ **Do NOT use it to serve user-facing data.** Inside a request handler,
|
|
307
320
|
* user-scoped queries must go through the request-scoped driver
|
|
308
|
-
* (`c.var.driver`), which carries the caller's identity
|
|
309
|
-
*
|
|
310
|
-
*
|
|
321
|
+
* (`c.var.driver`), which carries the caller's identity. Reaching for
|
|
322
|
+
* `dataAsAdmin` (or its alias {@link data}) in a request handler serves
|
|
323
|
+
* every caller whatever an admin may see.
|
|
311
324
|
*
|
|
312
325
|
* Undefined in the browser SDK.
|
|
313
326
|
*/
|
|
@@ -405,17 +418,20 @@ export interface RebaseClient<DB = unknown> {
|
|
|
405
418
|
* the admin-scoped {@link dataAsAdmin} accessor, raw {@link sql}, and the
|
|
406
419
|
* {@link email} service are all present (non-optional).
|
|
407
420
|
*
|
|
408
|
-
* **Trust levels.** {@link dataAsAdmin} is the admin-scoped
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
421
|
+
* **Trust levels.** {@link dataAsAdmin} is the admin-scoped driver — scoped as
|
|
422
|
+
* `{ uid: "service", roles: ["admin"] }`, so policies are still evaluated
|
|
423
|
+
* against that identity rather than skipped — and it is the only name for it
|
|
424
|
+
* here: the `data` alias that used to sit beside it is deliberately `Omit`ted
|
|
425
|
+
* from {@link RebaseClient} so the privilege has to be spelled out at every
|
|
426
|
+
* call site. {@link sql} is the unconditional bypass: raw SQL on the owner
|
|
427
|
+
* connection, no policies. For user-scoped queries inside a request handler use
|
|
428
|
+
* the request-scoped driver (`c.var.driver`) instead — never `dataAsAdmin`.
|
|
414
429
|
*/
|
|
415
430
|
export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>, "data"> {
|
|
416
431
|
/**
|
|
417
|
-
* Admin-scoped
|
|
418
|
-
* See {@link RebaseClient.dataAsAdmin}
|
|
432
|
+
* Admin-scoped data accessor (RLS is evaluated as the service identity, not
|
|
433
|
+
* skipped). Always present server-side. See {@link RebaseClient.dataAsAdmin}
|
|
434
|
+
* for the full safety contract.
|
|
419
435
|
*/
|
|
420
436
|
dataAsAdmin: RebaseSdkData<DB>;
|
|
421
437
|
/**
|
|
@@ -2,7 +2,66 @@ import type { VectorSearchParams } from "./data_driver";
|
|
|
2
2
|
import type { ComputedSortField, SearchMatch } from "../types/search";
|
|
3
3
|
import { Entity, EntityValues } from "../types/entities";
|
|
4
4
|
import { WhereFilterOp, FieldPath, FilterValues, OrderByTuple } from "../types/filter-operators";
|
|
5
|
+
/**
|
|
6
|
+
* Operator-blind filter value: whatever the column holds, a list of it, or null.
|
|
7
|
+
*
|
|
8
|
+
* @deprecated Superseded by {@link WhereValueFor}, which correlates the value
|
|
9
|
+
* with the operator. Kept exported because it is public API and downstream code
|
|
10
|
+
* annotates with it; every `where()` overload in this file uses `WhereValueFor`.
|
|
11
|
+
*/
|
|
5
12
|
export type WhereValue<T> = T | T[] | null;
|
|
13
|
+
/**
|
|
14
|
+
* The element type of an array column, and the column's own type otherwise.
|
|
15
|
+
*
|
|
16
|
+
* A generated SDK emits an `array` property as `Array<X>` and a to-many
|
|
17
|
+
* relation as `Array<TargetRow>`, so this is what `array-contains` compares
|
|
18
|
+
* against on either.
|
|
19
|
+
*/
|
|
20
|
+
export type ElementOf<T> = T extends readonly (infer E)[] ? E : T;
|
|
21
|
+
/**
|
|
22
|
+
* The `id` of a row-shaped element, and `never` for anything else.
|
|
23
|
+
*
|
|
24
|
+
* A to-many relation is emitted as `Array<TargetRow>`, but the filter compilers
|
|
25
|
+
* compare a relation by **id** — `buildRelationFilterPredicate` in
|
|
26
|
+
* `@rebasepro/server-postgres` unwraps a relation value down to its id — so
|
|
27
|
+
* `where("tags", "array-contains", tagId)` is the call that works, and the
|
|
28
|
+
* element type alone would refuse it.
|
|
29
|
+
*/
|
|
30
|
+
export type IdOf<E> = E extends {
|
|
31
|
+
id: infer I;
|
|
32
|
+
} ? I : never;
|
|
33
|
+
/**
|
|
34
|
+
* One member of an array column: its element, or — when the element is a row —
|
|
35
|
+
* that row's id, which is what a relation filter is actually compared against.
|
|
36
|
+
*/
|
|
37
|
+
export type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;
|
|
38
|
+
/**
|
|
39
|
+
* The value a given operator takes on a column of type `T`.
|
|
40
|
+
*
|
|
41
|
+
* `WhereValue<T>` was one value type for all sixteen operators, which made
|
|
42
|
+
* `array-contains` uncallable from a generated SDK — it is the one operator
|
|
43
|
+
* whose value is an *element* of the column rather than the column's own type,
|
|
44
|
+
* so on `tags: string[]` it wanted a `string[]` and the documented
|
|
45
|
+
* `.where("tags", "array-contains", "featured")` was a compile error. The
|
|
46
|
+
* spelling that did compile, `["featured"]`, builds `@> ARRAY[$1]` with the
|
|
47
|
+
* whole array bound as the single element and matches nothing: the correct
|
|
48
|
+
* query rejected, the accepted query silently wrong.
|
|
49
|
+
*
|
|
50
|
+
* The branches mirror `buildSingleFilterCondition` in `@rebasepro/server-postgres`:
|
|
51
|
+
*
|
|
52
|
+
* - `array-contains` → one element of the column (or a related row's id).
|
|
53
|
+
* - `in` / `not-in` / `array-contains-any` → a list of elements; a bare element
|
|
54
|
+
* is read as the one-element list, and `null` is a null check.
|
|
55
|
+
* - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern. Always a
|
|
56
|
+
* string, including on numeric and date columns, which the driver casts.
|
|
57
|
+
* - `is-null` / `is-not-null` → nothing; the value is ignored everywhere.
|
|
58
|
+
* - everything else → the column's own type, or `null` for a null comparison.
|
|
59
|
+
*
|
|
60
|
+
* Distributes over `Op`, so a caller holding an unnarrowed `WhereFilterOp`
|
|
61
|
+
* (a dynamic filter UI, say) gets the union of every branch and stays as
|
|
62
|
+
* permissive as it was.
|
|
63
|
+
*/
|
|
64
|
+
export type WhereValueFor<Op extends WhereFilterOp, T> = Op extends "array-contains" ? WhereElementOf<T> : Op extends "in" | "not-in" | "array-contains-any" ? readonly WhereElementOf<T>[] | WhereElementOf<T> | null : Op extends "like" | "ilike" | "not-like" | "not-ilike" ? string : Op extends "is-null" | "is-not-null" ? null | undefined : T | null;
|
|
6
65
|
export interface LogicalCondition {
|
|
7
66
|
type: "and" | "or";
|
|
8
67
|
conditions: (FilterCondition | LogicalCondition)[];
|
|
@@ -45,9 +104,14 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
|
|
|
45
104
|
/**
|
|
46
105
|
* Maximum number of items to return.
|
|
47
106
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
107
|
+
* Omit it and the backend applies {@link DEFAULT_LIST_LIMIT}, so a read is
|
|
108
|
+
* never unbounded. Provide it and it must be a whole number between 1 and
|
|
109
|
+
* {@link MAX_LIST_LIMIT}: the backend **rejects** anything else with a 400
|
|
110
|
+
* rather than trimming it to fit, because a page quietly smaller than the
|
|
111
|
+
* one you asked for is indistinguishable from having reached the end of the
|
|
112
|
+
* collection. To read past the ceiling, page with `offset` — or let
|
|
113
|
+
* {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}
|
|
114
|
+
* do it for you.
|
|
51
115
|
*/
|
|
52
116
|
limit?: number;
|
|
53
117
|
/**
|
|
@@ -160,7 +224,7 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
|
|
|
160
224
|
* @group Data
|
|
161
225
|
*/
|
|
162
226
|
export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
163
|
-
where<K extends keyof M & string>(column: K, operator:
|
|
227
|
+
where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
|
|
164
228
|
where(logicalCondition: LogicalCondition): this;
|
|
165
229
|
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
|
|
166
230
|
limit(count: number): this;
|
|
@@ -265,7 +329,7 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
265
329
|
* one place the two halves of this API are not interchangeable.
|
|
266
330
|
*/
|
|
267
331
|
count?(params?: FindParams<M>): Promise<number>;
|
|
268
|
-
where<K extends keyof M & string>(column: K, operator:
|
|
332
|
+
where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;
|
|
269
333
|
where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
|
|
270
334
|
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
|
|
271
335
|
limit(count: number): QueryBuilderInterface<M>;
|
|
@@ -444,7 +508,7 @@ export type FindAllParams<M extends Record<string, unknown> = Record<string, unk
|
|
|
444
508
|
* @group Data
|
|
445
509
|
*/
|
|
446
510
|
export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
447
|
-
where<K extends keyof M & string>(column: K, operator:
|
|
511
|
+
where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
|
|
448
512
|
where(logicalCondition: LogicalCondition): this;
|
|
449
513
|
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
|
|
450
514
|
limit(count: number): this;
|
|
@@ -509,9 +573,20 @@ export interface WriteOptions {
|
|
|
509
573
|
* it performs it again. On a table with a server-assigned id that is a
|
|
510
574
|
* duplicate row, because the id the client chose was never used.
|
|
511
575
|
*
|
|
576
|
+
* A key names **one** request, not a job. It records the method, the path
|
|
577
|
+
* and the body it was claimed for, so re-sending that exact request replays
|
|
578
|
+
* its answer, while the same key on a different one is refused with
|
|
579
|
+
* `IDEMPOTENCY_KEY_REUSED` (422) rather than silently answered with the
|
|
580
|
+
* first request's result. Pass a fresh key — a uuid — per call; a reusable
|
|
581
|
+
* business id shared by the create and the delete of one import means the
|
|
582
|
+
* second of them never runs.
|
|
583
|
+
*
|
|
512
584
|
* Set by the offline queue on every replay. Honoured for 24 hours and scoped
|
|
513
|
-
* to the authenticated user
|
|
514
|
-
*
|
|
585
|
+
* to the authenticated user — an unauthenticated caller has no principal to
|
|
586
|
+
* scope it to, so the key is ignored there. A retry sent while the first
|
|
587
|
+
* attempt is still being answered gets `IDEMPOTENCY_KEY_IN_PROGRESS` (409)
|
|
588
|
+
* and should be sent again. A server that cannot store keys ignores the
|
|
589
|
+
* header rather than refusing the write.
|
|
515
590
|
*/
|
|
516
591
|
idempotencyKey?: string;
|
|
517
592
|
}
|
|
@@ -729,7 +804,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
729
804
|
* Count the number of records matching the given filter.
|
|
730
805
|
*/
|
|
731
806
|
count?(params?: FindParams<M>): Promise<number>;
|
|
732
|
-
where<K extends keyof M & string>(column: K, operator:
|
|
807
|
+
where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;
|
|
733
808
|
where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
|
|
734
809
|
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
|
|
735
810
|
limit(count: number): SDKQueryBuilderInterface<M>;
|
|
@@ -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;
|
|
@@ -250,9 +268,41 @@ export interface DataDriver {
|
|
|
250
268
|
*/
|
|
251
269
|
updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
252
270
|
/**
|
|
253
|
-
* Delete
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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.
|
|
256
306
|
*/
|
|
257
307
|
delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;
|
|
258
308
|
/**
|
package/dist/index.es.js
CHANGED
|
@@ -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
|
|
@@ -383,6 +396,64 @@ var ADMIN_PROPERTY_KEYS = [
|
|
|
383
396
|
"urlPreview",
|
|
384
397
|
"widget"
|
|
385
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
|
+
}
|
|
386
457
|
//#endregion
|
|
387
458
|
//#region src/types/data_source.ts
|
|
388
459
|
/**
|
|
@@ -1239,24 +1310,51 @@ function computeSchemaVersion(collections) {
|
|
|
1239
1310
|
var DEFAULT_LIST_LIMIT = 50;
|
|
1240
1311
|
/** Rows returned for a vector-search list read when the client sends no `limit`. */
|
|
1241
1312
|
var DEFAULT_VECTOR_LIST_LIMIT = 10;
|
|
1242
|
-
/**
|
|
1313
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
1243
1314
|
var MAX_LIST_LIMIT = 1e3;
|
|
1244
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
|
+
/**
|
|
1245
1336
|
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
1246
1337
|
*
|
|
1247
|
-
* -
|
|
1248
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
1249
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
1338
|
+
* - An absent / blank limit falls back to the mode default:
|
|
1250
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.
|
|
1251
1345
|
*
|
|
1252
1346
|
* The return is never `undefined` — no ingress that routes its client limit
|
|
1253
1347
|
* through this can produce an unbounded read.
|
|
1348
|
+
*
|
|
1349
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
1254
1350
|
*/
|
|
1255
1351
|
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
1256
1352
|
const maxLimit = opts.maxLimit ?? 1e3;
|
|
1257
1353
|
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
1258
|
-
const parsed = typeof rawLimit === "number" ? rawLimit :
|
|
1259
|
-
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;
|
|
1260
1358
|
}
|
|
1261
1359
|
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
1262
1360
|
}
|
|
@@ -1288,6 +1386,6 @@ function isPublicStoragePath(path) {
|
|
|
1288
1386
|
return p.startsWith("public/") || p.startsWith(`default/public/`);
|
|
1289
1387
|
}
|
|
1290
1388
|
//#endregion
|
|
1291
|
-
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, 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, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, rewriteLegacyRlsFunctions, serializeCollections, storageEnvSuffix, toCanonicalOp, usesLegacyRlsFunctions };
|
|
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 };
|
|
1292
1390
|
|
|
1293
1391
|
//# sourceMappingURL=index.es.js.map
|