@rebasepro/types 0.13.0 → 0.13.1-canary.g06dbe5b
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 +379 -36
- package/dist/controllers/data_driver.d.ts +71 -5
- package/dist/errors.d.ts +30 -4
- package/dist/index.es.js +204 -11
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +38 -1
- 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 +2 -2
- package/src/call_context.ts +68 -4
- package/src/controllers/client.ts +47 -95
- package/src/controllers/data.ts +390 -36
- package/src/controllers/data_driver.ts +109 -8
- package/src/errors.ts +43 -4
- package/src/types/admin_block.ts +85 -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/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 { CollectionRegistryController } from "./collection_registry";
|
|
2
3
|
import type { EntityStatus, EntityValues } from "../types/entities";
|
|
3
4
|
import type { CollectionConfig, FilterValues } from "../types/collections";
|
|
@@ -52,12 +53,23 @@ export interface VectorSearchParams {
|
|
|
52
53
|
// server-side callers build fetch options directly and are intentionally NOT
|
|
53
54
|
// bounded here (migrations, admin exports, and CDC refetches may need the full
|
|
54
55
|
// set).
|
|
56
|
+
//
|
|
57
|
+
// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping
|
|
58
|
+
// answers a request for 100 000 rows with 1 000 of them, and a short page is
|
|
59
|
+
// indistinguishable from "that is all the data there is" — which is how a CSV
|
|
60
|
+
// export shipped 50 rows of a 100 000-row collection under a filename that read
|
|
61
|
+
// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*
|
|
62
|
+
// on the REST list response, but only for a caller who thinks to compare what it
|
|
63
|
+
// asked for against what it got, and the WebSocket `collection_update` frame
|
|
64
|
+
// carries neither — so signalling cannot be the answer on every surface and
|
|
65
|
+
// rejecting is. An ABSENT limit still defaults: naming no window is not the same
|
|
66
|
+
// as asking for one that cannot be served.
|
|
55
67
|
|
|
56
68
|
/** Rows returned for a plain / text-search list read when the client sends no `limit`. */
|
|
57
69
|
export const DEFAULT_LIST_LIMIT = 50;
|
|
58
70
|
/** Rows returned for a vector-search list read when the client sends no `limit`. */
|
|
59
71
|
export const DEFAULT_VECTOR_LIST_LIMIT = 10;
|
|
60
|
-
/**
|
|
72
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
61
73
|
export const MAX_LIST_LIMIT = 1000;
|
|
62
74
|
|
|
63
75
|
/** Overridable bounds for {@link resolveClientListLimit}. */
|
|
@@ -66,20 +78,46 @@ export interface ListLimitBounds {
|
|
|
66
78
|
defaultLimit?: number;
|
|
67
79
|
/** Default page size for vector-search reads. */
|
|
68
80
|
vectorDefaultLimit?: number;
|
|
69
|
-
/**
|
|
81
|
+
/** Largest limit a client may ask for. A larger one is rejected, not clamped. */
|
|
70
82
|
maxLimit?: number;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
87
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
88
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
89
|
+
*
|
|
90
|
+
* @group Errors
|
|
91
|
+
*/
|
|
92
|
+
export class ListLimitError extends RebaseApiError {
|
|
93
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
94
|
+
readonly maxLimit: number;
|
|
95
|
+
|
|
96
|
+
constructor(message: string, maxLimit: number) {
|
|
97
|
+
super(message, { status: 400, code: "INVALID_LIMIT" });
|
|
98
|
+
this.name = "ListLimitError";
|
|
99
|
+
this.maxLimit = maxLimit;
|
|
100
|
+
// Keeps `instanceof` working when this is compiled down for an older
|
|
101
|
+
// target, where extending a builtin otherwise loses the prototype.
|
|
102
|
+
Object.setPrototypeOf(this, ListLimitError.prototype);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
/**
|
|
74
107
|
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
75
108
|
*
|
|
76
|
-
* -
|
|
77
|
-
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
78
|
-
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
109
|
+
* - An absent / blank limit falls back to the mode default:
|
|
79
110
|
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
111
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
112
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
113
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
114
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
115
|
+
* tell apart from the whole collection.
|
|
80
116
|
*
|
|
81
117
|
* The return is never `undefined` — no ingress that routes its client limit
|
|
82
118
|
* through this can produce an unbounded read.
|
|
119
|
+
*
|
|
120
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
83
121
|
*/
|
|
84
122
|
export function resolveClientListLimit(
|
|
85
123
|
rawLimit: number | string | null | undefined,
|
|
@@ -87,10 +125,24 @@ export function resolveClientListLimit(
|
|
|
87
125
|
): number {
|
|
88
126
|
const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;
|
|
89
127
|
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
// `Number`, not `parseInt`: `parseInt("50rows")` is 50, which silently
|
|
129
|
+
// reads a typo as a window the caller never wrote.
|
|
130
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
|
|
131
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
132
|
+
throw new ListLimitError(
|
|
133
|
+
`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,
|
|
134
|
+
maxLimit
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (parsed > maxLimit) {
|
|
138
|
+
throw new ListLimitError(
|
|
139
|
+
`\`limit\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +
|
|
140
|
+
"per read and page through the rest with `offset` — answering with a smaller page would be " +
|
|
141
|
+
"indistinguishable from there being no more rows.",
|
|
142
|
+
maxLimit
|
|
143
|
+
);
|
|
93
144
|
}
|
|
145
|
+
return parsed;
|
|
94
146
|
}
|
|
95
147
|
return opts.vectorSearch
|
|
96
148
|
? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)
|
|
@@ -117,6 +169,8 @@ export interface FetchCollectionProps<M extends Record<string, unknown> = Record
|
|
|
117
169
|
startAfter?: unknown;
|
|
118
170
|
orderBy?: string;
|
|
119
171
|
searchString?: string;
|
|
172
|
+
/** Ask each row which declared search field matched — populates `_matches`. */
|
|
173
|
+
searchExplain?: boolean;
|
|
120
174
|
order?: "desc" | "asc";
|
|
121
175
|
/** Vector similarity search configuration */
|
|
122
176
|
vectorSearch?: VectorSearchParams;
|
|
@@ -169,6 +223,23 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
|
|
|
169
223
|
upsert?: boolean;
|
|
170
224
|
}
|
|
171
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @internal
|
|
228
|
+
*/
|
|
229
|
+
export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
230
|
+
path: string;
|
|
231
|
+
/**
|
|
232
|
+
* The rows to update, each named by its address.
|
|
233
|
+
*
|
|
234
|
+
* Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
|
|
235
|
+
* values and is insert-shaped — `saveMany` passes `status: "new"` and no
|
|
236
|
+
* `id`, so it cannot express "update exactly this row". This can, and it is
|
|
237
|
+
* why bulk update is a separate driver method rather than a flag on that one.
|
|
238
|
+
*/
|
|
239
|
+
updates: { id: string | number; values: Partial<EntityValues<M>> }[];
|
|
240
|
+
collection?: CollectionConfig<M>;
|
|
241
|
+
}
|
|
242
|
+
|
|
172
243
|
/**
|
|
173
244
|
* @internal
|
|
174
245
|
*/
|
|
@@ -177,6 +248,15 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
|
|
|
177
248
|
collection?: CollectionConfig<M>;
|
|
178
249
|
}
|
|
179
250
|
|
|
251
|
+
/**
|
|
252
|
+
* @internal
|
|
253
|
+
*/
|
|
254
|
+
export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
255
|
+
path: string;
|
|
256
|
+
ids: (string | number)[];
|
|
257
|
+
collection?: CollectionConfig<M>;
|
|
258
|
+
}
|
|
259
|
+
|
|
180
260
|
export type FilterCombinationValidProps = {
|
|
181
261
|
path: string;
|
|
182
262
|
databaseId?: string;
|
|
@@ -258,6 +338,17 @@ export interface DataDriver {
|
|
|
258
338
|
*/
|
|
259
339
|
saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
260
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Update many rows in one transaction, each addressed by id.
|
|
343
|
+
*
|
|
344
|
+
* Optional for the same reason `saveMany` is: a driver that cannot make the
|
|
345
|
+
* batch atomic should not pretend to. The REST layer reports
|
|
346
|
+
* `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
|
|
347
|
+
* writes, which would be neither atomic nor one round trip — the two things
|
|
348
|
+
* a caller reaches for a batch to get.
|
|
349
|
+
*/
|
|
350
|
+
updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
351
|
+
|
|
261
352
|
/**
|
|
262
353
|
* Delete a entity
|
|
263
354
|
* @param props
|
|
@@ -271,6 +362,14 @@ export interface DataDriver {
|
|
|
271
362
|
*/
|
|
272
363
|
deleteAll?(path: string): Promise<void>;
|
|
273
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Delete many rows in one transaction, addressed by id.
|
|
367
|
+
*
|
|
368
|
+
* Ids rather than a filter, deliberately — see
|
|
369
|
+
* {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
|
|
370
|
+
*/
|
|
371
|
+
deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
|
|
372
|
+
|
|
274
373
|
/**
|
|
275
374
|
* Check if the given property is unique in the given collection
|
|
276
375
|
* @param path Collection path
|
|
@@ -383,6 +482,8 @@ export interface RestFetchService {
|
|
|
383
482
|
offset?: number;
|
|
384
483
|
startAfter?: Record<string, unknown>;
|
|
385
484
|
searchString?: string;
|
|
485
|
+
/** Ask each row which declared search fields matched — populates `_matches`. */
|
|
486
|
+
searchExplain?: boolean;
|
|
386
487
|
databaseId?: string;
|
|
387
488
|
vectorSearch?: VectorSearchParams;
|
|
388
489
|
},
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
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 =
|
|
27
|
+
| "BAD_REQUEST"
|
|
28
|
+
| "UNAUTHORIZED"
|
|
29
|
+
| "FORBIDDEN"
|
|
30
|
+
| "NOT_FOUND"
|
|
31
|
+
| "CONFLICT"
|
|
32
|
+
| "INTERNAL_ERROR"
|
|
33
|
+
| "SERVICE_UNAVAILABLE"
|
|
34
|
+
| "DB_PERMISSION_DENIED"
|
|
35
|
+
| "SCHEMA_DRIFT"
|
|
36
|
+
// `string & {}` keeps the union open while preserving completion on the
|
|
37
|
+
// literals above — a bare `| string` would collapse them and offer nothing.
|
|
38
|
+
| (string & {});
|
|
39
|
+
|
|
1
40
|
/**
|
|
2
41
|
* Structured initializer for {@link RebaseApiError}.
|
|
3
42
|
*
|
|
@@ -10,8 +49,8 @@ export interface RebaseErrorInit {
|
|
|
10
49
|
* logic errors that have no HTTP status.
|
|
11
50
|
*/
|
|
12
51
|
status?: number;
|
|
13
|
-
/** Stable, machine-readable error code
|
|
14
|
-
code?:
|
|
52
|
+
/** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
|
|
53
|
+
code?: RebaseErrorCode;
|
|
15
54
|
/** Structured error payload returned by the server, when present. */
|
|
16
55
|
details?: unknown;
|
|
17
56
|
/** The underlying error this one wraps, if any. */
|
|
@@ -45,8 +84,8 @@ export interface RebaseErrorInit {
|
|
|
45
84
|
export class RebaseApiError extends Error {
|
|
46
85
|
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
47
86
|
readonly status?: number;
|
|
48
|
-
/** Stable machine-readable error code, when the server supplied one. */
|
|
49
|
-
readonly code?:
|
|
87
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
88
|
+
readonly code?: RebaseErrorCode;
|
|
50
89
|
/** Structured error payload from the server, when present. */
|
|
51
90
|
readonly details?: unknown;
|
|
52
91
|
|
package/src/types/admin_block.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const ADMIN_COLLECTION_KEYS = [
|
|
|
41
41
|
"defaultSize",
|
|
42
42
|
"defaultViewMode",
|
|
43
43
|
"disableDefaultActions",
|
|
44
|
+
"display",
|
|
44
45
|
"enabledViews",
|
|
45
46
|
"entityActions",
|
|
46
47
|
"entityViews",
|
|
@@ -51,6 +52,7 @@ export const ADMIN_COLLECTION_KEYS = [
|
|
|
51
52
|
"formAutoSave",
|
|
52
53
|
"formView",
|
|
53
54
|
"group",
|
|
55
|
+
"hideFromEntityViews",
|
|
54
56
|
"hideFromNavigation",
|
|
55
57
|
"hideIdFromCollection",
|
|
56
58
|
"hideIdFromForm",
|
|
@@ -122,3 +124,86 @@ export const ADMIN_PROPERTY_KEYS = [
|
|
|
122
124
|
|
|
123
125
|
/** A key of a property's `admin` block. @group Models */
|
|
124
126
|
export type AdminPropertyKey = typeof ADMIN_PROPERTY_KEYS[number];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Move flattened admin keys back down into the `admin` block.
|
|
130
|
+
*
|
|
131
|
+
* The admin panel works with a *flat* view model — the block merged onto the
|
|
132
|
+
* collection — so what comes back from a form has `icon` and `defaultViewMode`
|
|
133
|
+
* at the top level while `admin` still holds whatever the file was loaded with.
|
|
134
|
+
* This is the way back.
|
|
135
|
+
*
|
|
136
|
+
* **The top-level value wins.** It is the one the form just wrote; the block is
|
|
137
|
+
* the copy the collection was loaded with, and preferring it resolves every edit
|
|
138
|
+
* in favour of the value the user changed away from.
|
|
139
|
+
*
|
|
140
|
+
* This lives here, next to the key lists, because it had two implementations —
|
|
141
|
+
* `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in
|
|
142
|
+
* `@rebasepro/server`'s schema editor — that agreed on everything except that
|
|
143
|
+
* precedence, which is the only part that decides whether a save is visible.
|
|
144
|
+
*
|
|
145
|
+
* @group Models
|
|
146
|
+
*/
|
|
147
|
+
export function nestAdminKeysOf(
|
|
148
|
+
source: Record<string, unknown>,
|
|
149
|
+
adminKeys: readonly string[]
|
|
150
|
+
): Record<string, unknown> {
|
|
151
|
+
const keys = new Set<string>(adminKeys);
|
|
152
|
+
const top: Record<string, unknown> = {};
|
|
153
|
+
const block: Record<string, unknown> = { ...((source.admin as Record<string, unknown> | undefined) ?? {}) };
|
|
154
|
+
|
|
155
|
+
for (const [key, value] of Object.entries(source)) {
|
|
156
|
+
if (key === "admin") continue;
|
|
157
|
+
if (keys.has(key)) block[key] = value;
|
|
158
|
+
else top[key] = value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Object.keys(block).length > 0) top.admin = block;
|
|
162
|
+
return top;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* {@link nestAdminKeysOf} for a collection.
|
|
167
|
+
*
|
|
168
|
+
* @group Models
|
|
169
|
+
*/
|
|
170
|
+
export function nestAdminCollectionKeys(collection: Record<string, unknown>): Record<string, unknown> {
|
|
171
|
+
return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* {@link nestAdminKeysOf} for a property, applied to its children too.
|
|
176
|
+
*
|
|
177
|
+
* A map property carries `properties`, an array property carries `of`, and both
|
|
178
|
+
* hold properties with `admin` blocks of their own. A flat `readOnly` left on a
|
|
179
|
+
* child is as dead — and as fatal at the next boot — as one left on the parent,
|
|
180
|
+
* so the walk goes all the way down.
|
|
181
|
+
*
|
|
182
|
+
* @group Models
|
|
183
|
+
*/
|
|
184
|
+
export function nestAdminPropertyKeys(property: Record<string, unknown>): Record<string, unknown> {
|
|
185
|
+
const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);
|
|
186
|
+
|
|
187
|
+
const children = nested.properties;
|
|
188
|
+
if (children && typeof children === "object" && !Array.isArray(children)) {
|
|
189
|
+
nested.properties = Object.fromEntries(
|
|
190
|
+
Object.entries(children as Record<string, unknown>).map(([key, child]) => [
|
|
191
|
+
key,
|
|
192
|
+
child && typeof child === "object" && !Array.isArray(child)
|
|
193
|
+
? nestAdminPropertyKeys(child as Record<string, unknown>)
|
|
194
|
+
: child
|
|
195
|
+
])
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const of = nested.of;
|
|
200
|
+
if (Array.isArray(of)) {
|
|
201
|
+
nested.of = of.map(entry => entry && typeof entry === "object" && !Array.isArray(entry)
|
|
202
|
+
? nestAdminPropertyKeys(entry as Record<string, unknown>)
|
|
203
|
+
: entry);
|
|
204
|
+
} else if (of && typeof of === "object") {
|
|
205
|
+
nested.of = nestAdminPropertyKeys(of as Record<string, unknown>);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return nested;
|
|
209
|
+
}
|
package/src/types/backend.ts
CHANGED
package/src/types/collections.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Relation } from "./relations";
|
|
|
7
7
|
import type { SecurityRule } from "./security_rules";
|
|
8
8
|
import { getDataSourceCapabilities } from "./data_source";
|
|
9
9
|
import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operators";
|
|
10
|
+
import type { SearchConfig } from "./search";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Base interface containing all driver-agnostic collection properties.
|
|
@@ -19,9 +20,27 @@ import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operato
|
|
|
19
20
|
export interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The collection's identity. Required, and the value nearly everything else
|
|
24
|
+
* keys on:
|
|
25
|
+
*
|
|
26
|
+
* - the REST path — `/api/data/<slug>`
|
|
27
|
+
* - the SDK accessor — `client.data.<slug>` / `client.data.collection("<slug>")`
|
|
28
|
+
* - the admin panel's URL
|
|
29
|
+
* - the target of a `reference` or `relation` property
|
|
30
|
+
*
|
|
31
|
+
* Conventionally kebab-case and plural (`blog-posts`). It is independent of
|
|
32
|
+
* {@link table}: the slug is what callers say, the table is where the rows
|
|
33
|
+
* live, and renaming one does not rename the other.
|
|
34
|
+
*
|
|
35
|
+
* Treat it as frozen once anything has shipped against it — changing a slug
|
|
36
|
+
* changes every URL and every generated accessor at once.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* defineCollection({
|
|
40
|
+
* slug: "blog-posts", // /api/data/blog-posts, client.data.blogPosts
|
|
41
|
+
* table: "posts",
|
|
42
|
+
* properties: { … }
|
|
43
|
+
* })
|
|
25
44
|
*/
|
|
26
45
|
slug: string;
|
|
27
46
|
|
|
@@ -192,11 +211,16 @@ export interface BaseCollectionConfig<M extends Record<string, unknown> = Record
|
|
|
192
211
|
* Whether a write naming a field this collection does not declare is
|
|
193
212
|
* rejected with a 400. Defaults to `true`.
|
|
194
213
|
*
|
|
195
|
-
* Set to `false`
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
214
|
+
* Set to `false` where a column really does exist that the config never
|
|
215
|
+
* declared — populated by a trigger, or introspected rather than declared —
|
|
216
|
+
* and callers need to write it. The column still has to exist: the driver
|
|
217
|
+
* checks the key against the table's own columns whatever this is set to,
|
|
218
|
+
* because a key with no column behind it is not passed to the database and
|
|
219
|
+
* refused, it is dropped from the statement and answered 201.
|
|
220
|
+
*
|
|
221
|
+
* It does not let a typo through to Postgres for Postgres to judge. That is
|
|
222
|
+
* what this flag was documented as doing, and no such judgment ever
|
|
223
|
+
* happened.
|
|
200
224
|
*/
|
|
201
225
|
strictWrites?: boolean;
|
|
202
226
|
|
|
@@ -283,6 +307,21 @@ export interface PostgresCollectionConfig<M extends Record<string, unknown> = Re
|
|
|
283
307
|
* @default false
|
|
284
308
|
*/
|
|
285
309
|
disableDefaultPolicies?: boolean;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Opt in to Postgres full-text search for this collection.
|
|
313
|
+
*
|
|
314
|
+
* Omit it and `.search()` keeps its existing behaviour exactly — an
|
|
315
|
+
* `ILIKE '%term%'` across top-level string properties. Declare it and the
|
|
316
|
+
* collection gains one generated `tsvector` column and a GIN index, and
|
|
317
|
+
* `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.
|
|
318
|
+
*
|
|
319
|
+
* Postgres-only, like {@link VectorProperty}: the block is rejected at boot
|
|
320
|
+
* on other engines rather than silently ignored.
|
|
321
|
+
*
|
|
322
|
+
* @see SearchConfig
|
|
323
|
+
*/
|
|
324
|
+
search?: SearchConfig;
|
|
286
325
|
}
|
|
287
326
|
|
|
288
327
|
/**
|
package/src/types/cron.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RebaseServerClient } from "../controllers/client";
|
|
2
|
+
import type { RebaseSdkData } from "../controllers/data";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Cron Job type definitions for Rebase.
|
|
@@ -93,16 +94,57 @@ export interface CronJobContext {
|
|
|
93
94
|
log: (...args: unknown[]) => void;
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
|
-
* The server-side
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
* The server-side Rebase singleton — the **same object** `import { rebase }
|
|
98
|
+
* from "@rebasepro/server"` returns, and the same one `defineFunction`
|
|
99
|
+
* hands its callback. Spelled the same way here so that one thing has one
|
|
100
|
+
* name across every server-side authoring surface.
|
|
99
101
|
*
|
|
100
|
-
* Its data plane
|
|
101
|
-
* RLS** (`{ uid: "service", roles:
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* Its data plane is {@link RebaseServerClient.dataAsAdmin}, which runs with
|
|
103
|
+
* **admin privileges and bypasses RLS** (`{ uid: "service", roles:
|
|
104
|
+
* ["admin"] }`). A cron has no per-request user, so there is no user-scoped
|
|
105
|
+
* alternative here and no policy to fall back on: scope every query's
|
|
106
|
+
* filters yourself.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* export default defineCron({
|
|
110
|
+
* name: "Nightly cleanup",
|
|
111
|
+
* schedule: "0 3 * * *",
|
|
112
|
+
* async handler({ rebase, log }) {
|
|
113
|
+
* const expired = await rebase.dataAsAdmin.sessions.findAll({
|
|
114
|
+
* where: { expired: ["==", true] }
|
|
115
|
+
* });
|
|
116
|
+
* for (const session of expired) {
|
|
117
|
+
* await rebase.dataAsAdmin.sessions.delete(session.id as string);
|
|
118
|
+
* }
|
|
119
|
+
* log(`Deleted ${expired.length} expired sessions`);
|
|
120
|
+
* }
|
|
121
|
+
* });
|
|
122
|
+
*/
|
|
123
|
+
rebase: RebaseServerClient;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The same object as {@link rebase}, under the name this context used
|
|
127
|
+
* before.
|
|
128
|
+
*
|
|
129
|
+
* @deprecated Use `rebase` instead. Two things made the old name a problem,
|
|
130
|
+
* and neither was cosmetic. It contradicted every other server surface,
|
|
131
|
+
* where the singleton is `rebase` — the previous docstring had to end with
|
|
132
|
+
* *"it is only named `client` here"*. And typing it as `RebaseClient`
|
|
133
|
+
* re-exposed `client.data`, the alias that {@link RebaseServerClient}
|
|
134
|
+
* deliberately `Omit`s so the RLS-bypassing plane has exactly one name and
|
|
135
|
+
* the privilege is visible at the call site. A reader who learned
|
|
136
|
+
* `client.data` here carried it to a collection callback, where
|
|
137
|
+
* `context.data` is the *user-scoped* plane — same spelling, opposite
|
|
138
|
+
* privilege.
|
|
139
|
+
*
|
|
140
|
+
* Still the full server client at runtime, and `data` still resolves, so
|
|
141
|
+
* existing cron files keep working and keep compiling. It will be removed
|
|
142
|
+
* in the next major.
|
|
104
143
|
*/
|
|
105
|
-
client:
|
|
144
|
+
client: RebaseServerClient & {
|
|
145
|
+
/** @deprecated Use `rebase.dataAsAdmin` — the name states the privilege. */
|
|
146
|
+
data: RebaseSdkData;
|
|
147
|
+
};
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
// =============================================================================
|
package/src/types/entities.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SearchMatch } from "./search";
|
|
1
2
|
/**
|
|
2
3
|
* New or existing status
|
|
3
4
|
* @group Models
|
|
@@ -26,6 +27,17 @@ export interface Entity<M extends Record<string, unknown> = Record<string, unkno
|
|
|
26
27
|
*/
|
|
27
28
|
values: EntityValues<M>;
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Why this entity is in a search result: which declared fields matched, and
|
|
32
|
+
* the text around each hit.
|
|
33
|
+
*
|
|
34
|
+
* Present only on rows returned by a search that asked for it. A sibling of
|
|
35
|
+
* `values` rather than a key inside it, because it describes the *query*,
|
|
36
|
+
* not the record — nothing in the collection declares it, no form edits it,
|
|
37
|
+
* and a record fetched by id never has one.
|
|
38
|
+
*/
|
|
39
|
+
searchMatches?: SearchMatch[];
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* Which driver this entity belongs to (e.g., 'postgres', 'firestore').
|
|
31
43
|
* If not specified, the default driver is assumed.
|
|
@@ -8,7 +8,8 @@ import type { RebaseCallContext } from "../call_context";
|
|
|
8
8
|
*
|
|
9
9
|
* Register per-collection on the collection's `callbacks` field, or globally
|
|
10
10
|
* via `initializeRebaseBackend({ callbacks })`. Fires on **every** data path — REST API,
|
|
11
|
-
* WebSocket / realtime subscriptions, and server-side
|
|
11
|
+
* WebSocket / realtime subscriptions, and server-side writes through
|
|
12
|
+
* `rebase.dataAsAdmin`.
|
|
12
13
|
*
|
|
13
14
|
* When both global and per-collection callbacks are registered, execution
|
|
14
15
|
* order is: **global → collection → property callbacks**.
|
package/src/types/index.ts
CHANGED
|
@@ -5,8 +5,10 @@ export * from "./chips";
|
|
|
5
5
|
export * from "./properties";
|
|
6
6
|
export * from "./admin_block";
|
|
7
7
|
export * from "./collections";
|
|
8
|
+
export * from "./search";
|
|
8
9
|
export * from "./relations";
|
|
9
10
|
export * from "./policy";
|
|
11
|
+
export * from "./rls-functions";
|
|
10
12
|
export * from "./security_rules";
|
|
11
13
|
|
|
12
14
|
export * from "./entity_callbacks";
|