@rebasepro/types 0.13.0 → 0.13.1-canary.g249daa1
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 +61 -4
- package/dist/controllers/client.d.ts +16 -59
- package/dist/controllers/data.d.ts +298 -30
- package/dist/controllers/data_driver.d.ts +48 -0
- package/dist/errors.d.ts +30 -4
- package/dist/index.es.js +113 -5
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +1 -1
- package/dist/types/backend.d.ts +2 -0
- package/dist/types/collections.d.ts +36 -3
- 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 +22 -4
- package/dist/types/rls-functions.d.ts +84 -0
- package/dist/types/search.d.ts +231 -0
- package/package.json +2 -2
- package/src/call_context.ts +59 -4
- package/src/controllers/client.ts +16 -80
- package/src/controllers/data.ts +298 -30
- package/src/controllers/data_driver.ts +49 -0
- package/src/errors.ts +43 -4
- package/src/types/admin_block.ts +2 -0
- package/src/types/backend.ts +2 -0
- package/src/types/collections.ts +37 -3
- 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/index.ts +2 -0
- package/src/types/policy.ts +13 -13
- package/src/types/properties.ts +23 -4
- package/src/types/rls-functions.ts +98 -0
- package/src/types/search.ts +247 -0
package/dist/call_context.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataDriver } from "./controllers/data_driver";
|
|
1
2
|
import type { StorageSource } from "./controllers/storage";
|
|
2
3
|
import type { RebaseClient } from "./controllers/client";
|
|
3
4
|
import type { RebaseSdkData } from "./controllers/data";
|
|
@@ -19,7 +20,14 @@ export type RebaseCallContext<USER extends User = User> = {
|
|
|
19
20
|
* The Rebase client instance.
|
|
20
21
|
* Available in all entity callbacks (beforeSave, afterSave, afterRead,
|
|
21
22
|
* beforeDelete, afterDelete) and in CollectionActionsProps via context.
|
|
22
|
-
* Use it to call backend functions, access
|
|
23
|
+
* Use it to call backend functions, access storage, send email, etc.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ **Not the same trust level as {@link data}.** Server-side this is the
|
|
26
|
+
* app singleton, so `client.dataAsAdmin` is **always** the RLS-bypassing
|
|
27
|
+
* plane — while {@link data}, one property over, follows whoever triggered
|
|
28
|
+
* the callback. On a user request, reaching for `context.client.dataAsAdmin`
|
|
29
|
+
* silently escalates a user-scoped operation to admin. For queries in a
|
|
30
|
+
* callback use {@link data}; come here for functions, storage and email.
|
|
23
31
|
*
|
|
24
32
|
* @example
|
|
25
33
|
* // In a beforeSave callback:
|
|
@@ -35,11 +43,60 @@ export type RebaseCallContext<USER extends User = User> = {
|
|
|
35
43
|
* Unified data access — `context.data.products.create(...)`.
|
|
36
44
|
* Access any collection as a dynamic property.
|
|
37
45
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* **Inherits the privilege of whatever triggered the callback.** This is not
|
|
47
|
+
* a fixed trust level, and it is the one thing to know about this accessor:
|
|
48
|
+
*
|
|
49
|
+
* - Triggered by a **user request** (REST, realtime, an admin-panel edit):
|
|
50
|
+
* user-scoped. The callback runs on the RLS-bound transaction opened for
|
|
51
|
+
* that request, so policies apply to reads *and* writes — a callback
|
|
52
|
+
* cannot see a row its caller could not.
|
|
53
|
+
* - Triggered by **server-context work** (`rebase.dataAsAdmin`, a cron):
|
|
54
|
+
* unscoped, on the owner connection, bypassing RLS.
|
|
55
|
+
*
|
|
56
|
+
* So a callback that reads a sibling row will find it when an admin task
|
|
57
|
+
* saves and may find nothing when an end user saves — without an error,
|
|
58
|
+
* because RLS filters rather than raises. Write callbacks that tolerate
|
|
59
|
+
* that, or reach for {@link client}`.dataAsAdmin` deliberately when the
|
|
60
|
+
* callback genuinely has to see past its caller.
|
|
61
|
+
*
|
|
62
|
+
* Verified end-to-end against Postgres rather than asserted — see
|
|
63
|
+
* `"scopes context.data to the caller when a callback runs on a user
|
|
64
|
+
* request"` in `server-postgres`' `rls-enforcement` e2e suite. The
|
|
65
|
+
* documentation previously claimed the opposite (that callbacks always have
|
|
66
|
+
* full access), which is the unsafe direction to be wrong in.
|
|
67
|
+
*
|
|
68
|
+
* Returns flat rows (`{ id, ...columns }`), identical in *shape* to the
|
|
69
|
+
* frontend SDK client — so `context.data` in a backend callback and
|
|
70
|
+
* `client.data` in the frontend are accessed the same way (`row.title`,
|
|
71
|
+
* never `row.values.title`). Shape only: privilege differs as above.
|
|
41
72
|
*/
|
|
42
73
|
data: RebaseSdkData;
|
|
74
|
+
/**
|
|
75
|
+
* The driver executing the operation this callback is attached to.
|
|
76
|
+
*
|
|
77
|
+
* Present server-side only. Declared here because it is already public in
|
|
78
|
+
* practice — the backend has always passed it, and the callbacks guide
|
|
79
|
+
* documented `context.driver.withAuth(user)` in all six locales. The
|
|
80
|
+
* contract simply did not name it, so `buildCallContext` was cast through
|
|
81
|
+
* `as unknown as RebaseCallContext` and nothing about the object was
|
|
82
|
+
* type-checked at all.
|
|
83
|
+
*
|
|
84
|
+
* The guide no longer recommends `withAuth` — {@link data} is already
|
|
85
|
+
* user-scoped on a user request, so the manual re-scoping it described was
|
|
86
|
+
* answering a problem that did not exist. The field stays declared rather
|
|
87
|
+
* than removed: it is on the runtime object, dropping it would break anyone
|
|
88
|
+
* who found it, and a named optional is better than a silent extra.
|
|
89
|
+
*
|
|
90
|
+
* `withAuth` is not on {@link DataDriver} because not every engine supports
|
|
91
|
+
* RLS scoping; it is narrowed here, and left optional so a driver without it
|
|
92
|
+
* is a compile-time absence rather than a runtime surprise.
|
|
93
|
+
*/
|
|
94
|
+
driver?: DataDriver & {
|
|
95
|
+
withAuth?(user: {
|
|
96
|
+
uid: string;
|
|
97
|
+
roles?: string[];
|
|
98
|
+
}): Promise<DataDriver>;
|
|
99
|
+
};
|
|
43
100
|
/**
|
|
44
101
|
* Used storage implementation
|
|
45
102
|
*/
|
|
@@ -371,7 +371,22 @@ export interface RebaseClient<DB = unknown> {
|
|
|
371
371
|
setOnUnauthorized?(handler: () => Promise<boolean>): void;
|
|
372
372
|
/** Resolve the current auth token */
|
|
373
373
|
resolveToken?(): Promise<string | null>;
|
|
374
|
-
/**
|
|
374
|
+
/**
|
|
375
|
+
* POST to an arbitrary path on the backend — the escape hatch, not the way
|
|
376
|
+
* to call a function.
|
|
377
|
+
*
|
|
378
|
+
* For a custom function use {@link functions}`.invoke(name, payload)`: it
|
|
379
|
+
* targets `/functions/<name>`, takes a method and sub-path, and returns the
|
|
380
|
+
* response body as sent. This posts wherever you point it and **unwraps**:
|
|
381
|
+
* it returns `res.data` when the response has a `data` property and the
|
|
382
|
+
* whole envelope otherwise — so an endpoint that legitimately answers
|
|
383
|
+
* `{ data: null }` hands back the envelope rather than `null`. Two ways to
|
|
384
|
+
* reach a function with two different response contracts is a trap; this is
|
|
385
|
+
* the one that exists for paths `invoke` cannot express.
|
|
386
|
+
*
|
|
387
|
+
* @internal Prefer `functions.invoke()`. Kept public because a backend can
|
|
388
|
+
* mount routes outside `/functions`, and nothing else reaches those.
|
|
389
|
+
*/
|
|
375
390
|
call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
|
|
376
391
|
/**
|
|
377
392
|
* Execute raw SQL against the database.
|
|
@@ -419,64 +434,6 @@ export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>,
|
|
|
419
434
|
params?: unknown[];
|
|
420
435
|
}): Promise<Record<string, unknown>[]>;
|
|
421
436
|
}
|
|
422
|
-
/**
|
|
423
|
-
* The browser-side Rebase surface — the shape produced by
|
|
424
|
-
* `createRebaseClient()` in `@rebasepro/client`.
|
|
425
|
-
*
|
|
426
|
-
* Its {@link data} accessor is **user-scoped**: every call carries the signed-in
|
|
427
|
-
* user's token, so backend RLS policies apply. It deliberately omits the
|
|
428
|
-
* server-only members — there is no `sql`, no `email`, and no
|
|
429
|
-
* `dataAsAdmin`, so the RLS-bypassing accessor can never be reached from
|
|
430
|
-
* browser code.
|
|
431
|
-
*/
|
|
432
|
-
export interface RebaseBrowserClient<DB = unknown> {
|
|
433
|
-
/** User-scoped data access layer (carries the signed-in user's token). */
|
|
434
|
-
data: RebaseSdkData<DB>;
|
|
435
|
-
/** Unified Authentication layer */
|
|
436
|
-
auth: AuthClient;
|
|
437
|
-
/** Unified Storage layer (default storage source, backward-compatible) */
|
|
438
|
-
storage?: StorageSource;
|
|
439
|
-
/** Registry of all named storage sources for multi-backend support */
|
|
440
|
-
storageRegistry?: StorageSourceRegistry;
|
|
441
|
-
/** Build a server-backed {@link StorageSource} for a named storage source. */
|
|
442
|
-
createStorageSource?(storageId: string): StorageSource;
|
|
443
|
-
/** Discover the storage sources declared on the backend. */
|
|
444
|
-
fetchStorageSources?(): Promise<StorageSourceDefinition[]>;
|
|
445
|
-
/** Admin API for user management */
|
|
446
|
-
admin?: AdminAPI;
|
|
447
|
-
/** Cron job management API */
|
|
448
|
-
cron?: CronAPI;
|
|
449
|
-
/** Database backup management API */
|
|
450
|
-
backups?: BackupsAPI;
|
|
451
|
-
/** Custom backend functions API */
|
|
452
|
-
functions?: FunctionsAPI;
|
|
453
|
-
/** Service API keys management API */
|
|
454
|
-
apiKeys?: ApiKeysAPI;
|
|
455
|
-
/** Base HTTP URL of the backend server */
|
|
456
|
-
baseUrl?: string;
|
|
457
|
-
/**
|
|
458
|
-
* The path every API route is mounted under, appended to {@link baseUrl}.
|
|
459
|
-
*
|
|
460
|
-
* `"/api"` unless the backend was configured with a different `basePath`
|
|
461
|
-
* and the client told to match. Exposed because code that builds a URL by
|
|
462
|
-
* hand — rather than going through the client's own methods — otherwise has
|
|
463
|
-
* to guess, and guessing `/api` is wrong for exactly the projects that set
|
|
464
|
-
* the option.
|
|
465
|
-
*/
|
|
466
|
-
apiPath?: string;
|
|
467
|
-
/** WebSocket client for realtime subscriptions */
|
|
468
|
-
ws?: RebaseWebSocket;
|
|
469
|
-
/** Set the auth token for subsequent requests */
|
|
470
|
-
setToken?(token: string | null): void;
|
|
471
|
-
/** Set a function that lazily resolves the auth token */
|
|
472
|
-
setAuthTokenGetter?(getter: () => Promise<string | null>): void;
|
|
473
|
-
/** Set handler called when a request returns 401 */
|
|
474
|
-
setOnUnauthorized?(handler: () => Promise<boolean>): void;
|
|
475
|
-
/** Resolve the current auth token */
|
|
476
|
-
resolveToken?(): Promise<string | null>;
|
|
477
|
-
/** Make a raw HTTP call to the backend */
|
|
478
|
-
call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
|
|
479
|
-
}
|
|
480
437
|
/**
|
|
481
438
|
* Client-side registry for managing multiple storage sources.
|
|
482
439
|
*
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { VectorSearchParams } from "./data_driver";
|
|
2
|
+
import type { ComputedSortField, SearchMatch } from "../types/search";
|
|
1
3
|
import { Entity, EntityValues } from "../types/entities";
|
|
2
4
|
import { WhereFilterOp, FieldPath, FilterValues, OrderByTuple } from "../types/filter-operators";
|
|
3
5
|
export type WhereValue<T> = T | T[] | null;
|
|
@@ -34,13 +36,19 @@ export interface FilterCondition {
|
|
|
34
36
|
*
|
|
35
37
|
* `limit`/`offset` and `page` describe the same window two ways. If **both
|
|
36
38
|
* `offset` and `page` are provided, `page` wins** — the backend computes
|
|
37
|
-
* `offset = (page - 1) * (limit ??
|
|
38
|
-
* Pick one style per query.
|
|
39
|
+
* `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
|
|
40
|
+
* explicit `offset`. Pick one style per query.
|
|
39
41
|
*
|
|
40
42
|
* @group Data
|
|
41
43
|
*/
|
|
42
44
|
export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
43
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Maximum number of items to return.
|
|
47
|
+
*
|
|
48
|
+
* Defaults to {@link DEFAULT_LIST_LIMIT}, and is clamped to
|
|
49
|
+
* {@link MAX_LIST_LIMIT}. Both bounds are applied by the backend, so a
|
|
50
|
+
* read is never unbounded whether or not a limit was asked for.
|
|
51
|
+
*/
|
|
44
52
|
limit?: number;
|
|
45
53
|
/**
|
|
46
54
|
* Number of items to skip. Ignored when {@link FindParams.page} is also
|
|
@@ -49,7 +57,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
|
|
|
49
57
|
offset?: number;
|
|
50
58
|
/**
|
|
51
59
|
* Page number (1-indexed), alternative to {@link FindParams.offset}.
|
|
52
|
-
* When set, overrides `offset` as `(page - 1) * (limit ??
|
|
60
|
+
* When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
|
|
53
61
|
*/
|
|
54
62
|
page?: number;
|
|
55
63
|
/**
|
|
@@ -76,7 +84,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
|
|
|
76
84
|
* Sort order as a `[field, direction]` tuple.
|
|
77
85
|
* @example orderBy: ["created_at", "desc"]
|
|
78
86
|
*/
|
|
79
|
-
orderBy?: OrderByTuple<FieldPath<M
|
|
87
|
+
orderBy?: OrderByTuple<FieldPath<M> | ComputedSortField>;
|
|
80
88
|
/**
|
|
81
89
|
* Relations to include in the response.
|
|
82
90
|
*
|
|
@@ -86,11 +94,45 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
|
|
|
86
94
|
*/
|
|
87
95
|
include?: string[];
|
|
88
96
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
97
|
+
* Text search string, AND-ed with `where`/`logical`. This is the value
|
|
98
|
+
* behind the query builder's `.search()` method.
|
|
99
|
+
*
|
|
100
|
+
* What it compiles to depends on the collection. By default — matching
|
|
101
|
+
* every collection that has not said otherwise — it is a case-insensitive
|
|
102
|
+
* substring match OR-ed across the collection's top-level `string`
|
|
103
|
+
* properties: it does not reach inside `map` or `array` properties, it does
|
|
104
|
+
* not stem or rank, and it cannot use an index.
|
|
105
|
+
*
|
|
106
|
+
* A Postgres collection that declares a `search` block instead gets a
|
|
107
|
+
* ranked full-text match over exactly the fields it named, and rows come
|
|
108
|
+
* back with a {@link FindParams.orderBy}-able `_score`.
|
|
92
109
|
*/
|
|
93
110
|
searchString?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Nearest-neighbour search over a `vector` property.
|
|
113
|
+
*
|
|
114
|
+
* Postgres only, and only for a collection that declares a property of
|
|
115
|
+
* type `vector`. Rows come back ordered by distance, closest first, each
|
|
116
|
+
* carrying a `_distance`. Combines with `where` and `logical`, which are
|
|
117
|
+
* applied as filters before the ordering — so this is "the nearest rows
|
|
118
|
+
* that also match", not "the nearest rows, then filtered".
|
|
119
|
+
*
|
|
120
|
+
* Supplying the query vector is the caller's job: rebase stores and
|
|
121
|
+
* searches embeddings, it does not compute them.
|
|
122
|
+
*/
|
|
123
|
+
vectorSearch?: VectorSearchParams;
|
|
124
|
+
/**
|
|
125
|
+
* Ask each returned row to explain itself: which declared search fields
|
|
126
|
+
* matched, with a highlighted snippet from each. Populates `_matches`.
|
|
127
|
+
*
|
|
128
|
+
* Off by default because it is not free — one `ts_headline` per declared
|
|
129
|
+
* field per returned row, and `ts_headline` re-parses the document rather
|
|
130
|
+
* than reading the index. Fine for a page of results, not for an export.
|
|
131
|
+
*
|
|
132
|
+
* Ignored unless the collection declares a `search` block and the query
|
|
133
|
+
* carries a `searchString`; there is nothing to explain otherwise.
|
|
134
|
+
*/
|
|
135
|
+
searchExplain?: boolean;
|
|
94
136
|
}
|
|
95
137
|
/**
|
|
96
138
|
* Paginated response from a collection query.
|
|
@@ -108,34 +150,49 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
|
|
|
108
150
|
};
|
|
109
151
|
}
|
|
110
152
|
/**
|
|
111
|
-
* Fluent query builder for the **admin
|
|
153
|
+
* Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`
|
|
112
154
|
* (Snapshot-wrapped rows).
|
|
113
155
|
*
|
|
114
156
|
* @internal App developers should use {@link SDKQueryBuilderInterface}
|
|
115
157
|
* (flat rows, returned by `client.data.*` / `context.data.*`). This
|
|
116
|
-
* Snapshot-flavored variant backs the admin
|
|
158
|
+
* Snapshot-flavored variant backs the admin panel internals only.
|
|
117
159
|
*
|
|
118
160
|
* @group Data
|
|
119
161
|
*/
|
|
120
162
|
export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
121
163
|
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
|
|
122
164
|
where(logicalCondition: LogicalCondition): this;
|
|
123
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
|
|
165
|
+
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
|
|
124
166
|
limit(count: number): this;
|
|
125
167
|
offset(count: number): this;
|
|
126
|
-
search(searchString: string
|
|
168
|
+
search(searchString: string, options?: {
|
|
169
|
+
explain?: boolean;
|
|
170
|
+
}): this;
|
|
171
|
+
/**
|
|
172
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
173
|
+
*
|
|
174
|
+
* Postgres only, over a property declared as `type: "vector"`. Each row
|
|
175
|
+
* comes back with a `_distance`. Any `where` on the same query filters
|
|
176
|
+
* before the ordering; distance decides the order.
|
|
177
|
+
*
|
|
178
|
+
* The query embedding is the caller's to produce.
|
|
179
|
+
*/
|
|
180
|
+
vectorSearch(property: string, vector: number[], options?: {
|
|
181
|
+
distance?: "cosine" | "l2" | "inner_product";
|
|
182
|
+
threshold?: number;
|
|
183
|
+
}): this;
|
|
127
184
|
include(...relations: string[]): this;
|
|
128
185
|
find(): Promise<FindResponse<M>>;
|
|
129
186
|
listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
|
|
130
187
|
}
|
|
131
188
|
/**
|
|
132
|
-
* A single collection's CRUD accessor for the **admin
|
|
189
|
+
* A single collection's CRUD accessor for the **admin panel** — every method
|
|
133
190
|
* resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
|
|
134
191
|
*
|
|
135
192
|
* @internal App developers do **not** use this. The public, symmetric surface
|
|
136
193
|
* is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
|
|
137
194
|
* in the SDK and `context.data.products` in framework callbacks. This
|
|
138
|
-
* Snapshot-flavored accessor backs the admin
|
|
195
|
+
* Snapshot-flavored accessor backs the admin panel view-model only.
|
|
139
196
|
*
|
|
140
197
|
* @group Data
|
|
141
198
|
*/
|
|
@@ -169,6 +226,21 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
169
226
|
* @returns The updated entity
|
|
170
227
|
*/
|
|
171
228
|
update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
|
|
229
|
+
/**
|
|
230
|
+
* Update many records in a single transaction.
|
|
231
|
+
*
|
|
232
|
+
* See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.
|
|
233
|
+
*/
|
|
234
|
+
updateMany?(updates: {
|
|
235
|
+
id: string | number;
|
|
236
|
+
data: Partial<EntityValues<M>>;
|
|
237
|
+
}[]): Promise<Entity<M>[]>;
|
|
238
|
+
/**
|
|
239
|
+
* Delete many records in a single transaction.
|
|
240
|
+
*
|
|
241
|
+
* See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.
|
|
242
|
+
*/
|
|
243
|
+
deleteMany?(ids: (string | number)[]): Promise<void>;
|
|
172
244
|
/**
|
|
173
245
|
* Delete a record by ID.
|
|
174
246
|
*/
|
|
@@ -185,14 +257,35 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
185
257
|
listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void): () => void;
|
|
186
258
|
/**
|
|
187
259
|
* Count the number of records matching the given filter.
|
|
260
|
+
*
|
|
261
|
+
* Optional on this contract because a data source need not support it, and
|
|
262
|
+
* required on `CollectionClient` — the HTTP implementation always has it.
|
|
263
|
+
* So `client.data.posts.count()` compiles in the browser while the same
|
|
264
|
+
* call through a `context.data` accessor needs `count?.()`, which is the
|
|
265
|
+
* one place the two halves of this API are not interchangeable.
|
|
188
266
|
*/
|
|
189
267
|
count?(params?: FindParams<M>): Promise<number>;
|
|
190
268
|
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilderInterface<M>;
|
|
191
269
|
where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
|
|
192
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
|
|
270
|
+
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
|
|
193
271
|
limit(count: number): QueryBuilderInterface<M>;
|
|
194
272
|
offset(count: number): QueryBuilderInterface<M>;
|
|
195
|
-
search(searchString: string
|
|
273
|
+
search(searchString: string, options?: {
|
|
274
|
+
explain?: boolean;
|
|
275
|
+
}): QueryBuilderInterface<M>;
|
|
276
|
+
/**
|
|
277
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
278
|
+
*
|
|
279
|
+
* Postgres only, over a property declared as `type: "vector"`. Each row
|
|
280
|
+
* comes back with a `_distance`. Any `where` on the same query filters
|
|
281
|
+
* before the ordering; distance decides the order.
|
|
282
|
+
*
|
|
283
|
+
* The query embedding is the caller's to produce.
|
|
284
|
+
*/
|
|
285
|
+
vectorSearch(property: string, vector: number[], options?: {
|
|
286
|
+
distance?: "cosine" | "l2" | "inner_product";
|
|
287
|
+
threshold?: number;
|
|
288
|
+
}): QueryBuilderInterface<M>;
|
|
196
289
|
include(...relations: string[]): QueryBuilderInterface<M>;
|
|
197
290
|
}
|
|
198
291
|
/**
|
|
@@ -217,11 +310,53 @@ export interface PaginationMeta {
|
|
|
217
310
|
* @group Data
|
|
218
311
|
*/
|
|
219
312
|
export interface FindResult<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
220
|
-
/**
|
|
221
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Flat rows matching the query, each carrying whatever the query computed
|
|
315
|
+
* for it — see {@link QueryComputedFields}.
|
|
316
|
+
*/
|
|
317
|
+
data: (M & QueryComputedFields)[];
|
|
222
318
|
/** Pagination metadata */
|
|
223
319
|
meta: PaginationMeta;
|
|
224
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Values a query attaches to a row that are not columns of it.
|
|
323
|
+
*
|
|
324
|
+
* Both are absent unless the query asked for the thing that produces them, so
|
|
325
|
+
* both are optional — and reading one on a query that did not ask returns
|
|
326
|
+
* `undefined` rather than a wrong number.
|
|
327
|
+
*
|
|
328
|
+
* They live here rather than on the row type because a generated row type
|
|
329
|
+
* describes a *table*, and neither of these is in one. Without this, a caller
|
|
330
|
+
* who sorted by relevance could not then read the relevance.
|
|
331
|
+
*
|
|
332
|
+
* A `type` alias, deliberately, not an `interface`. TypeScript grants an
|
|
333
|
+
* implicit index signature to a type alias and withholds it from an interface,
|
|
334
|
+
* so `Row & QueryComputedFields` stops being assignable to
|
|
335
|
+
* `Record<string, unknown>` the moment this becomes an interface. Seven casts
|
|
336
|
+
* in one downstream app broke on exactly that.
|
|
337
|
+
*
|
|
338
|
+
* @group Data
|
|
339
|
+
*/
|
|
340
|
+
export type QueryComputedFields = {
|
|
341
|
+
/**
|
|
342
|
+
* Relevance, when the collection declares a {@link SearchConfig} and the
|
|
343
|
+
* query carried a search string. Higher is better; the scale is not
|
|
344
|
+
* comparable between two different search strings.
|
|
345
|
+
*/
|
|
346
|
+
_score?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Which declared fields matched, and the text around each hit. Present only
|
|
349
|
+
* when the query asked for it — `.search(term, { explain: true })` — because
|
|
350
|
+
* it costs a `ts_headline` per field per row.
|
|
351
|
+
*/
|
|
352
|
+
_matches?: SearchMatch[];
|
|
353
|
+
/**
|
|
354
|
+
* Distance to the query vector, when the query used
|
|
355
|
+
* {@link FindParams.vectorSearch}. Lower is closer, and the rows are
|
|
356
|
+
* already ordered by it.
|
|
357
|
+
*/
|
|
358
|
+
_distance?: number;
|
|
359
|
+
};
|
|
225
360
|
/**
|
|
226
361
|
* Which column an iteration seeks on, for keyset ("seek") pagination.
|
|
227
362
|
*
|
|
@@ -311,10 +446,25 @@ export type FindAllParams<M extends Record<string, unknown> = Record<string, unk
|
|
|
311
446
|
export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
312
447
|
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
|
|
313
448
|
where(logicalCondition: LogicalCondition): this;
|
|
314
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
|
|
449
|
+
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
|
|
315
450
|
limit(count: number): this;
|
|
316
451
|
offset(count: number): this;
|
|
317
|
-
search(searchString: string
|
|
452
|
+
search(searchString: string, options?: {
|
|
453
|
+
explain?: boolean;
|
|
454
|
+
}): this;
|
|
455
|
+
/**
|
|
456
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
457
|
+
*
|
|
458
|
+
* Postgres only, over a property declared as `type: "vector"`. Each row
|
|
459
|
+
* comes back with a `_distance`. Any `where` on the same query filters
|
|
460
|
+
* before the ordering; distance decides the order.
|
|
461
|
+
*
|
|
462
|
+
* The query embedding is the caller's to produce.
|
|
463
|
+
*/
|
|
464
|
+
vectorSearch(property: string, vector: number[], options?: {
|
|
465
|
+
distance?: "cosine" | "l2" | "inner_product";
|
|
466
|
+
threshold?: number;
|
|
467
|
+
}): this;
|
|
318
468
|
include(...relations: string[]): this;
|
|
319
469
|
find(): Promise<FindResult<M>>;
|
|
320
470
|
count(): Promise<number>;
|
|
@@ -461,6 +611,12 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
461
611
|
* Batches are capped server-side (1000 rows by default) because one batch
|
|
462
612
|
* holds its locks for the whole transaction — chunk larger jobs.
|
|
463
613
|
*
|
|
614
|
+
* Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
|
|
615
|
+
* A client that never sees the response cannot know whether the batch
|
|
616
|
+
* committed, and without a key the server cannot tell the retry from a
|
|
617
|
+
* second genuine import — so it performs it again, duplicating every row in
|
|
618
|
+
* the batch rather than just one.
|
|
619
|
+
*
|
|
464
620
|
* @returns The written rows, in the order given.
|
|
465
621
|
*
|
|
466
622
|
* @example
|
|
@@ -472,7 +628,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
472
628
|
*/
|
|
473
629
|
createMany(data: I[], options?: {
|
|
474
630
|
upsert?: boolean;
|
|
475
|
-
}): Promise<M[]>;
|
|
631
|
+
} & WriteOptions): Promise<M[]>;
|
|
476
632
|
/**
|
|
477
633
|
* Update an existing record by ID.
|
|
478
634
|
* @param data The fields to update (the collection's `Update` shape).
|
|
@@ -480,18 +636,94 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
480
636
|
* @throws {RebaseApiError} with status 404 when the record does not exist.
|
|
481
637
|
*/
|
|
482
638
|
update(id: string | number, data: U): Promise<M>;
|
|
639
|
+
/**
|
|
640
|
+
* Update many records in a single request and a single transaction.
|
|
641
|
+
*
|
|
642
|
+
* The counterpart to {@link createMany}, and the reason it exists is the
|
|
643
|
+
* same: one call per row means one HTTP round trip and one transaction per
|
|
644
|
+
* row. Every record still runs the normal pipeline — callbacks, relations,
|
|
645
|
+
* row-level security — and the batch is all-or-nothing, so a rejected
|
|
646
|
+
* record leaves none of them written and the error names the offending
|
|
647
|
+
* index.
|
|
648
|
+
*
|
|
649
|
+
* Each entry is `{ id, data }` rather than a flat row carrying its own key.
|
|
650
|
+
* That is deliberate: on a table keyed on something other than `id` — a
|
|
651
|
+
* `sku`, a composite key — a flat row cannot say whether a column is the
|
|
652
|
+
* address or a value to write. Naming the address separately mirrors
|
|
653
|
+
* single-row `update(id, data)` exactly and leaves nothing to infer.
|
|
654
|
+
*
|
|
655
|
+
* An id that matches no row fails the batch with a 404 rather than being
|
|
656
|
+
* skipped, for the same reason `update()` does: silently updating four of
|
|
657
|
+
* five rows is worse than updating none.
|
|
658
|
+
*
|
|
659
|
+
* Batches share `createMany`'s server-side cap (1000 rows by default),
|
|
660
|
+
* because one batch holds its locks for the whole transaction.
|
|
661
|
+
*
|
|
662
|
+
* Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
|
|
663
|
+
* An update replayed in full is naturally idempotent, but one interleaved
|
|
664
|
+
* with another writer's is not — the key is what stops a lost ACK from
|
|
665
|
+
* re-applying a stale batch over newer data.
|
|
666
|
+
*
|
|
667
|
+
* @returns The updated rows, in the order given.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* ```ts
|
|
671
|
+
* await client.data.orders.updateMany([
|
|
672
|
+
* { id: "o-1", data: { status: "shipped" } },
|
|
673
|
+
* { id: "o-2", data: { status: "shipped" } }
|
|
674
|
+
* ]);
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
updateMany(updates: {
|
|
678
|
+
id: string | number;
|
|
679
|
+
data: U;
|
|
680
|
+
}[], options?: WriteOptions): Promise<M[]>;
|
|
483
681
|
/**
|
|
484
682
|
* Delete a record by ID.
|
|
485
683
|
* @throws {RebaseApiError} with status 404 when the record does not exist.
|
|
486
684
|
*/
|
|
487
685
|
delete(id: string | number): Promise<void>;
|
|
488
686
|
/**
|
|
489
|
-
*
|
|
687
|
+
* Delete many records in a single request and a single transaction.
|
|
688
|
+
*
|
|
689
|
+
* Takes ids, not a filter. A filter-shaped bulk delete is a different and
|
|
690
|
+
* far more dangerous operation — the failure mode is an omitted or
|
|
691
|
+
* mistyped condition emptying a table, and it cannot be reviewed at the
|
|
692
|
+
* call site the way an explicit list can. Read first, then pass the ids you
|
|
693
|
+
* meant.
|
|
694
|
+
*
|
|
695
|
+
* `beforeDelete` and `afterDelete` fire per row, exactly as they do for
|
|
696
|
+
* single deletes, and returning `false` from `beforeDelete` fails the batch
|
|
697
|
+
* rather than quietly dropping one row from it. All-or-nothing, so an id
|
|
698
|
+
* that matches no row 404s the whole call.
|
|
699
|
+
*
|
|
700
|
+
* Shares `createMany`'s row cap.
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```ts
|
|
704
|
+
* const stale = await client.data.sessions.findAll({
|
|
705
|
+
* where: { expires_at: ["<", cutoff] }
|
|
706
|
+
* });
|
|
707
|
+
* await client.data.sessions.deleteMany(stale.map(s => s.id as string));
|
|
708
|
+
* ```
|
|
490
709
|
*/
|
|
491
|
-
|
|
710
|
+
deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
|
|
492
711
|
/**
|
|
493
|
-
*
|
|
712
|
+
* The low-level realtime subscription: raw server pushes, nothing else.
|
|
713
|
+
*
|
|
714
|
+
* **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps
|
|
715
|
+
* this one and is what a UI actually wants — it emits from the local
|
|
716
|
+
* database first when offline is enabled, re-emits on local writes and
|
|
717
|
+
* rollbacks, and de-duplicates emissions so a refresh that changes nothing
|
|
718
|
+
* does not call back. `listen` does none of that; it forwards what the
|
|
719
|
+
* socket sends.
|
|
720
|
+
*
|
|
721
|
+
* Optional because it is only present when realtime is enabled. `observe()`
|
|
722
|
+
* is not — it degrades to a single fetch — which is the other reason to
|
|
723
|
+
* reach for it instead.
|
|
494
724
|
*/
|
|
725
|
+
listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
|
|
726
|
+
/** {@link listen} for a single row. Prefer `observeById()`. */
|
|
495
727
|
listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
|
|
496
728
|
/**
|
|
497
729
|
* Count the number of records matching the given filter.
|
|
@@ -499,14 +731,25 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
499
731
|
count?(params?: FindParams<M>): Promise<number>;
|
|
500
732
|
where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): SDKQueryBuilderInterface<M>;
|
|
501
733
|
where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
|
|
502
|
-
orderBy(column: keyof M & string, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
|
|
734
|
+
orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
|
|
503
735
|
limit(count: number): SDKQueryBuilderInterface<M>;
|
|
504
736
|
offset(count: number): SDKQueryBuilderInterface<M>;
|
|
505
|
-
search(searchString: string
|
|
737
|
+
search(searchString: string, options?: {
|
|
738
|
+
explain?: boolean;
|
|
739
|
+
}): SDKQueryBuilderInterface<M>;
|
|
740
|
+
/**
|
|
741
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
742
|
+
* Postgres only, over a `type: "vector"` property. See
|
|
743
|
+
* {@link SDKQueryBuilderInterface.vectorSearch}.
|
|
744
|
+
*/
|
|
745
|
+
vectorSearch(property: string, vector: number[], options?: {
|
|
746
|
+
distance?: "cosine" | "l2" | "inner_product";
|
|
747
|
+
threshold?: number;
|
|
748
|
+
}): SDKQueryBuilderInterface<M>;
|
|
506
749
|
include(...relations: string[]): SDKQueryBuilderInterface<M>;
|
|
507
750
|
}
|
|
508
751
|
/**
|
|
509
|
-
* The unified data access object for the **admin
|
|
752
|
+
* The unified data access object for the **admin panel** (Entity-shaped).
|
|
510
753
|
*
|
|
511
754
|
* Access collections as dynamic properties: `data.products.find(...)`. Each
|
|
512
755
|
* accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
|
|
@@ -515,7 +758,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
|
|
|
515
758
|
*
|
|
516
759
|
* @internal App developers do **not** use this — they use
|
|
517
760
|
* {@link RebaseSdkData} (flat rows), which is what the SDK client and backend
|
|
518
|
-
* `context.data` expose. This Entity-shaped map backs the admin
|
|
761
|
+
* `context.data` expose. This Entity-shaped map backs the admin panel only.
|
|
519
762
|
*
|
|
520
763
|
* @group Data
|
|
521
764
|
*/
|
|
@@ -539,10 +782,15 @@ export type RebaseData<DB = unknown> = {
|
|
|
539
782
|
* Dynamic collection accessor.
|
|
540
783
|
* Access any collection by its slug as a property.
|
|
541
784
|
*
|
|
785
|
+
* The index signature is `CollectionAccessor` alone, for the reason
|
|
786
|
+
* spelled out on {@link RebaseSdkData}: unioning in the `collection`
|
|
787
|
+
* method's own signature is unnecessary across an intersection, and it
|
|
788
|
+
* costs `data.products.find()` — the access this `@example` documents.
|
|
789
|
+
*
|
|
542
790
|
* @example
|
|
543
791
|
* data.products.find({ where: { status: ["==", "published"] } })
|
|
544
792
|
*/
|
|
545
|
-
[collectionSlug: string]: CollectionAccessor
|
|
793
|
+
[collectionSlug: string]: CollectionAccessor;
|
|
546
794
|
});
|
|
547
795
|
/**
|
|
548
796
|
* The unified data access object for the **SDK** — flat rows, no Entity wrapper.
|
|
@@ -595,6 +843,26 @@ export type InsertOf<T> = T extends {
|
|
|
595
843
|
export type UpdateOf<T> = T extends {
|
|
596
844
|
Update: infer U extends Record<string, unknown>;
|
|
597
845
|
} ? U : Partial<RowOf<T>>;
|
|
846
|
+
/**
|
|
847
|
+
* Note on the untyped branch below: its index signature is
|
|
848
|
+
* `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
|
|
849
|
+
*
|
|
850
|
+
* The union looks like it is needed so `collection` — a method on this same
|
|
851
|
+
* object — satisfies the index signature. It is not, because `collection` is
|
|
852
|
+
* declared in a *separate* member of the intersection, and TypeScript only
|
|
853
|
+
* requires named properties to be assignable to an index signature declared
|
|
854
|
+
* alongside them. Including the function arm cost the documented accessor:
|
|
855
|
+
*
|
|
856
|
+
* rebase.dataAsAdmin.projects.find()
|
|
857
|
+
* // ^ Property 'find' does not exist on type
|
|
858
|
+
* // 'SDKCollectionClient | ((slug: string) => …)'
|
|
859
|
+
*
|
|
860
|
+
* Every project without a generated `Database` type lands on this branch, so
|
|
861
|
+
* property-style access — the form used by the `@example` below, by the
|
|
862
|
+
* scaffolded function template, and by the 0.13 migration note — did not
|
|
863
|
+
* compile for any of them. Do not restore the arm; use `collection(slug)` if a
|
|
864
|
+
* caller genuinely needs the by-slug function.
|
|
865
|
+
*/
|
|
598
866
|
export type RebaseSdkData<DB = unknown> = {
|
|
599
867
|
/**
|
|
600
868
|
* Get a flat collection accessor by slug.
|
|
@@ -614,5 +882,5 @@ export type RebaseSdkData<DB = unknown> = {
|
|
|
614
882
|
* @example
|
|
615
883
|
* data.products.find({ where: { status: ["==", "published"] } })
|
|
616
884
|
*/
|
|
617
|
-
[collectionSlug: string]: SDKCollectionClient
|
|
885
|
+
[collectionSlug: string]: SDKCollectionClient;
|
|
618
886
|
});
|