@rebasepro/server 0.13.0 → 0.13.1-canary.g1822133
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/{admin_block-CcSGdH7M.js → admin_block-H86dCPsj.js} +62 -2
- package/dist/admin_block-H86dCPsj.js.map +1 -0
- package/dist/api/ast-schema-editor.d.ts +74 -6
- package/dist/api/errors.d.ts +12 -6
- package/dist/api/openapi-generator.d.ts +14 -0
- package/dist/api/rest/api-generator.d.ts +26 -1
- package/dist/api/rest/idempotency.d.ts +62 -4
- package/dist/api/rest/query-parser.d.ts +15 -1
- package/dist/api/rest/write-validation.d.ts +52 -9
- package/dist/auth/adapter-middleware.d.ts +3 -1
- package/dist/auth/admin-roles.d.ts +24 -0
- package/dist/auth/apple-oauth.d.ts +16 -11
- package/dist/auth/bitbucket-oauth.d.ts +2 -4
- package/dist/auth/builtin-auth-adapter.d.ts +11 -0
- package/dist/auth/discord-oauth.d.ts +5 -6
- package/dist/auth/facebook-oauth.d.ts +2 -4
- package/dist/auth/github-oauth.d.ts +2 -4
- package/dist/auth/gitlab-oauth.d.ts +6 -4
- package/dist/auth/google-oauth.d.ts +1 -0
- package/dist/auth/index.d.ts +8 -1
- package/dist/auth/interfaces.d.ts +81 -1
- package/dist/auth/jwt.d.ts +52 -0
- package/dist/auth/linkedin-oauth.d.ts +2 -4
- package/dist/auth/mfa-crypto.d.ts +54 -3
- package/dist/auth/mfa-gate.d.ts +42 -0
- package/dist/auth/mfa-routes.d.ts +26 -1
- package/dist/auth/mfa.d.ts +16 -0
- package/dist/auth/microsoft-oauth.d.ts +19 -4
- package/dist/auth/middleware.d.ts +26 -0
- package/dist/auth/oauth-code-flow.d.ts +66 -0
- package/dist/auth/oauth-signin-policy.d.ts +61 -0
- package/dist/auth/oidc-id-token.d.ts +61 -0
- package/dist/auth/rate-limiter.d.ts +62 -5
- package/dist/auth/registration-policy.d.ts +31 -0
- package/dist/auth/rls-scope.d.ts +25 -0
- package/dist/auth/routes.d.ts +15 -0
- package/dist/auth/slack-oauth.d.ts +2 -4
- package/dist/auth/spotify-oauth.d.ts +2 -4
- package/dist/auth/token-revocation.d.ts +38 -0
- package/dist/auth/twitter-oauth.d.ts +2 -5
- package/dist/{auth-CuC9M2x6.js → auth-CYoPVf-E.js} +1996 -426
- package/dist/auth-CYoPVf-E.js.map +1 -0
- package/dist/{backup-CVggVhR2.js → backup-C6ljYVTp.js} +2 -2
- package/dist/{backup-CVggVhR2.js.map → backup-C6ljYVTp.js.map} +1 -1
- package/dist/boot/boot.d.ts +19 -0
- package/dist/boot/ddl-bootstrap.d.ts +70 -0
- package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-Bet-eCNJ.js} +3 -3
- package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-Bet-eCNJ.js.map} +1 -1
- package/dist/cron/cron-loader.d.ts +26 -1
- package/dist/cron/cron-routes.d.ts +1 -1
- package/dist/cron/cron-scheduler.d.ts +13 -3
- package/dist/cron/define-cron.d.ts +17 -3
- package/dist/cron/index.d.ts +2 -2
- package/dist/{cron-loader-B1S2MCSl.js → cron-loader-YhhQeVBM.js} +43 -8
- package/dist/cron-loader-YhhQeVBM.js.map +1 -0
- package/dist/{cron-routes-CrQ0tK-_.js → cron-routes-maM_RlUu.js} +10 -4
- package/dist/cron-routes-maM_RlUu.js.map +1 -0
- package/dist/{cron-scheduler-B3RFt0HS.js → cron-scheduler-DIpYBmZP.js} +40 -9
- package/dist/cron-scheduler-DIpYBmZP.js.map +1 -0
- package/dist/{cron-store-BywZsyfZ.js → cron-store-Dvr4Y1sZ.js} +66 -47
- package/dist/cron-store-Dvr4Y1sZ.js.map +1 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js +183 -0
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -0
- package/dist/email/html.d.ts +54 -0
- package/dist/email/index.d.ts +3 -0
- package/dist/email/link-base.d.ts +39 -0
- package/dist/email/smtp-email-service.d.ts +7 -1
- package/dist/email/templates.d.ts +9 -1
- package/dist/email/types.d.ts +11 -1
- package/dist/env.d.ts +5 -0
- package/dist/{errors-CgkCzoj7.js → errors-EBYiaJ2E.js} +15 -11
- package/dist/errors-EBYiaJ2E.js.map +1 -0
- package/dist/function-loader-DDS1v7YX.js +139 -0
- package/dist/function-loader-DDS1v7YX.js.map +1 -0
- package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
- package/dist/function-routes-Btcez1T-.js.map +1 -0
- package/dist/functions/define-function.d.ts +25 -6
- package/dist/functions/function-loader.d.ts +23 -0
- package/dist/functions/function-routes.d.ts +7 -1
- package/dist/functions/request-timeout.d.ts +34 -0
- package/dist/history/history-routes.d.ts +6 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.es.js +2147 -845
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +10 -1
- package/dist/init/process-safety.d.ts +25 -0
- package/dist/init.d.ts +38 -0
- package/dist/{jwt-DD6EtpGj.js → jwt-_IFqfTOg.js} +72 -9
- package/dist/jwt-_IFqfTOg.js.map +1 -0
- package/dist/logger-DfvF_8r-.js +190 -0
- package/dist/logger-DfvF_8r-.js.map +1 -0
- package/dist/openapi-generator-DPKtUC9X.js +865 -0
- package/dist/openapi-generator-DPKtUC9X.js.map +1 -0
- package/dist/request-timeout-RivJsME0.js +65 -0
- package/dist/request-timeout-RivJsME0.js.map +1 -0
- package/dist/schema-editor-routes-CRcS3ArS.js +437 -0
- package/dist/schema-editor-routes-CRcS3ArS.js.map +1 -0
- package/dist/serve-spa.d.ts +5 -4
- package/dist/services/outbound-url-guard.d.ts +53 -0
- package/dist/services/routed-realtime-service.d.ts +10 -2
- package/dist/services/webhook-service.d.ts +76 -1
- package/dist/singleton.d.ts +25 -8
- package/dist/{src-Cum9kox5.js → src-C7rkDGxA.js} +678 -89
- package/dist/src-C7rkDGxA.js.map +1 -0
- package/dist/{src-_qQ3RNCK.js → src-Cz9nMgUR.js} +93 -3
- package/dist/src-Cz9nMgUR.js.map +1 -0
- package/dist/storage/LocalStorageController.d.ts +12 -1
- package/dist/storage/image-transform.d.ts +57 -0
- package/dist/storage/keys.d.ts +98 -0
- package/dist/storage/routes.d.ts +11 -0
- package/dist/storage/tus-handler.d.ts +22 -2
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logging.d.ts +0 -4
- package/dist/utils/sql.d.ts +11 -6
- package/package.json +7 -6
- package/dist/admin_block-CcSGdH7M.js.map +0 -1
- package/dist/auth-CuC9M2x6.js.map +0 -1
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/cron-loader-B1S2MCSl.js.map +0 -1
- package/dist/cron-routes-CrQ0tK-_.js.map +0 -1
- package/dist/cron-scheduler-B3RFt0HS.js.map +0 -1
- package/dist/cron-store-BywZsyfZ.js.map +0 -1
- package/dist/errors-CgkCzoj7.js.map +0 -1
- package/dist/function-loader-B_1fYfUY.js +0 -86
- package/dist/function-loader-B_1fYfUY.js.map +0 -1
- package/dist/function-routes-C0cLIy3N.js.map +0 -1
- package/dist/jwt-DD6EtpGj.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/openapi-generator-BEwyiaAr.js +0 -597
- package/dist/openapi-generator-BEwyiaAr.js.map +0 -1
- package/dist/schema-editor-routes-CbF20Mf1.js +0 -248
- package/dist/schema-editor-routes-CbF20Mf1.js.map +0 -1
- package/dist/src-Cum9kox5.js.map +0 -1
- package/dist/src-_qQ3RNCK.js.map +0 -1
|
@@ -2,7 +2,64 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { r as __require, t as __commonJSMin } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
-
import {
|
|
5
|
+
import { a as getDeclaredSubcollections, d as CANONICAL_TO_REST, f as NULL_OPS, l as getDataSourceCapabilities, m as toCanonicalOp, o as isPostgresCollectionConfig, p as REST_TO_CANONICAL, s as isRelationalCollectionConfig, u as ALL_WHERE_FILTER_OPS } from "./src-Cz9nMgUR.js";
|
|
6
|
+
//#region ../types/src/errors.ts
|
|
7
|
+
/**
|
|
8
|
+
* The single error type thrown across the entire Rebase client surface —
|
|
9
|
+
* HTTP data/control-plane calls, realtime/WebSocket operations, and
|
|
10
|
+
* client-side logic errors (e.g. an unknown collection accessor). A `catch`
|
|
11
|
+
* block only ever needs to check for this one class:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { RebaseApiError } from "@rebasepro/client"; // re-exported
|
|
15
|
+
*
|
|
16
|
+
* try {
|
|
17
|
+
* await client.data.products.update(id, { price: 9 });
|
|
18
|
+
* } catch (e) {
|
|
19
|
+
* if (e instanceof RebaseApiError) {
|
|
20
|
+
* if (e.status === 404) { ... } // HTTP failures carry a status
|
|
21
|
+
* console.error(e.code, e.details);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `status` is present for HTTP failures and `undefined` otherwise, so its
|
|
27
|
+
* presence distinguishes transport-level errors from realtime/logic errors.
|
|
28
|
+
*
|
|
29
|
+
* @group Errors
|
|
30
|
+
*/
|
|
31
|
+
var RebaseApiError = class extends Error {
|
|
32
|
+
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
33
|
+
status;
|
|
34
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
35
|
+
code;
|
|
36
|
+
/** Structured error payload from the server, when present. */
|
|
37
|
+
details;
|
|
38
|
+
constructor(message, init = {}) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "RebaseApiError";
|
|
41
|
+
this.status = init.status;
|
|
42
|
+
this.code = init.code;
|
|
43
|
+
this.details = init.details;
|
|
44
|
+
if (init.cause !== void 0) this.cause = init.cause;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Client-side logic error — raised before any request is made (e.g. accessing
|
|
49
|
+
* an unknown collection accessor when a typed dictionary is configured).
|
|
50
|
+
*
|
|
51
|
+
* A subclass of {@link RebaseApiError} (with no `status`), so a single
|
|
52
|
+
* `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.
|
|
53
|
+
*
|
|
54
|
+
* @group Errors
|
|
55
|
+
*/
|
|
56
|
+
var RebaseClientError = class extends RebaseApiError {
|
|
57
|
+
constructor(message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "RebaseClientError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
6
63
|
//#region ../types/src/types/entities.ts
|
|
7
64
|
/**
|
|
8
65
|
* Class used to create a reference to a entity in a different path.
|
|
@@ -129,7 +186,7 @@ var Vector = class {
|
|
|
129
186
|
//#endregion
|
|
130
187
|
//#region ../types/src/types/policy.ts
|
|
131
188
|
/**
|
|
132
|
-
* The id a request without a logged-in user reports as `
|
|
189
|
+
* The id a request without a logged-in user reports as `rebase.uid()`.
|
|
133
190
|
*
|
|
134
191
|
* A user-context request always sets `app.uid`: blank would read back as
|
|
135
192
|
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
@@ -137,7 +194,7 @@ var Vector = class {
|
|
|
137
194
|
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
138
195
|
* is set.
|
|
139
196
|
*
|
|
140
|
-
* The consequence for policy authors is that **`
|
|
197
|
+
* The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
|
|
141
198
|
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
142
199
|
* {@link policy.authenticated} to mean "signed in", and
|
|
143
200
|
* {@link policy.serverContext} to mean "the trusted server context". Do not
|
|
@@ -154,7 +211,7 @@ var ANONYMOUS_USER_ID = "anonymous";
|
|
|
154
211
|
* JavaScript evaluator and the linter were all built on
|
|
155
212
|
* {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
|
|
156
213
|
* callers as `'anon'` — so `policy.authenticated()`, which compiled to
|
|
157
|
-
* `
|
|
214
|
+
* `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
|
|
158
215
|
* sanctioned way to write "signed in" granted to everyone, and the linter
|
|
159
216
|
* flagged the spelling that actually worked as a foreign convention.
|
|
160
217
|
*
|
|
@@ -235,6 +292,54 @@ var policy = {
|
|
|
235
292
|
authUid: () => ({ kind: "authUid" }),
|
|
236
293
|
authRoles: () => ({ kind: "authRoles" })
|
|
237
294
|
};
|
|
295
|
+
/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
|
|
296
|
+
var MAX_LIST_LIMIT = 1e3;
|
|
297
|
+
/**
|
|
298
|
+
* Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
|
|
299
|
+
* serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
|
|
300
|
+
* verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
|
|
301
|
+
*
|
|
302
|
+
* @group Errors
|
|
303
|
+
*/
|
|
304
|
+
var ListLimitError = class ListLimitError extends RebaseApiError {
|
|
305
|
+
/** The ceiling that was exceeded — what the caller should page by instead. */
|
|
306
|
+
maxLimit;
|
|
307
|
+
constructor(message, maxLimit) {
|
|
308
|
+
super(message, {
|
|
309
|
+
status: 400,
|
|
310
|
+
code: "INVALID_LIMIT"
|
|
311
|
+
});
|
|
312
|
+
this.name = "ListLimitError";
|
|
313
|
+
this.maxLimit = maxLimit;
|
|
314
|
+
Object.setPrototypeOf(this, ListLimitError.prototype);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
319
|
+
*
|
|
320
|
+
* - An absent / blank limit falls back to the mode default:
|
|
321
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
322
|
+
* - A limit that is present must be an integer in `[1, maxLimit]`. Anything
|
|
323
|
+
* else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
|
|
324
|
+
* {@link ListLimitError} rather than being coerced into range, because every
|
|
325
|
+
* coercion answers a question the caller did not ask with a page it cannot
|
|
326
|
+
* tell apart from the whole collection.
|
|
327
|
+
*
|
|
328
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
329
|
+
* through this can produce an unbounded read.
|
|
330
|
+
*
|
|
331
|
+
* @throws {ListLimitError} when a present `limit` is not an integer in range.
|
|
332
|
+
*/
|
|
333
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
334
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
335
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
336
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
|
|
337
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
|
|
338
|
+
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);
|
|
339
|
+
return parsed;
|
|
340
|
+
}
|
|
341
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
342
|
+
}
|
|
238
343
|
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
239
344
|
(function(e) {
|
|
240
345
|
var t;
|
|
@@ -1371,6 +1476,47 @@ function singularizeForKey(name) {
|
|
|
1371
1476
|
const result = singular(name);
|
|
1372
1477
|
return result.length > 0 ? result : name;
|
|
1373
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* The API name a database column is served under.
|
|
1481
|
+
*
|
|
1482
|
+
* The wire name of a field is its property key, and Rebase's property keys are
|
|
1483
|
+
* camelCase — `displayName`, `createdAt`, `photoURL`. Columns are snake_case,
|
|
1484
|
+
* because an unquoted Postgres identifier folds to lower case and a camelCase
|
|
1485
|
+
* column is therefore reachable only as `"authorId"` forever: in hand-written
|
|
1486
|
+
* SQL, in psql, in an RLS policy body, in a dump, and in every third-party tool
|
|
1487
|
+
* that ever touches the database. So the two conventions are both right, and
|
|
1488
|
+
* this is the function that crosses between them.
|
|
1489
|
+
*
|
|
1490
|
+
* It exists because two sources of field names never crossed: a foreign key
|
|
1491
|
+
* derived from a relation (`author_id`) and a column read back by introspection
|
|
1492
|
+
* (`user_id`) both landed on the wire under their column name, while every
|
|
1493
|
+
* hand-authored collection next to them used camelCase. One API, two
|
|
1494
|
+
* conventions, and no rule a caller could infer from outside — those names are
|
|
1495
|
+
* also the `where` and `orderBy` keys, so it was not a matter of taste.
|
|
1496
|
+
*
|
|
1497
|
+
* Rules, in the order they matter:
|
|
1498
|
+
*
|
|
1499
|
+
* - **A name with no separator is returned unchanged.** `photoURL` stays
|
|
1500
|
+
* `photoURL` and `id` stays `id`. Lower-casing a single token is what makes
|
|
1501
|
+
* a "camelCase" helper destructive — `camelCase("photoURL")` is `photourl` —
|
|
1502
|
+
* and this function is applied to names that are *already* keys.
|
|
1503
|
+
* - **Each following segment keeps its own casing** apart from an upper-cased
|
|
1504
|
+
* first letter, so `photo_URL` → `photoURL` rather than `photoUrl`.
|
|
1505
|
+
* - **The result may still not be a JavaScript identifier.** `2fa_enabled`
|
|
1506
|
+
* becomes `2faEnabled`, which is a perfectly good object key and still needs
|
|
1507
|
+
* quoting where one is written into generated source.
|
|
1508
|
+
*
|
|
1509
|
+
* Not the inverse of {@link toSnakeCase}: `toSnakeCase` tokenises on case
|
|
1510
|
+
* boundaries and would turn `photoURL` into `photo_url`. Round-tripping is not
|
|
1511
|
+
* a property either function promises, which is why a column name that a
|
|
1512
|
+
* property maps explicitly is always read off `columnName` rather than derived.
|
|
1513
|
+
*/
|
|
1514
|
+
function toWireKey(columnName) {
|
|
1515
|
+
if (!columnName) return columnName;
|
|
1516
|
+
const segments = columnName.split(/[-_ ]+/).filter(Boolean);
|
|
1517
|
+
if (segments.length <= 1) return columnName;
|
|
1518
|
+
return segments.map((segment, index) => index === 0 ? segment.charAt(0).toLowerCase() + segment.slice(1) : segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
|
|
1519
|
+
}
|
|
1374
1520
|
//#endregion
|
|
1375
1521
|
//#region ../common/src/util/entities.ts
|
|
1376
1522
|
/**
|
|
@@ -1382,10 +1528,21 @@ function singularizeForKey(name) {
|
|
|
1382
1528
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
1383
1529
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
1384
1530
|
*
|
|
1531
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
1532
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
1533
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
1534
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
1535
|
+
* reported half of its own data as a type error. The declared target is the
|
|
1536
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
1537
|
+
*
|
|
1385
1538
|
* Returns null if the value cannot be coerced.
|
|
1386
1539
|
*/
|
|
1387
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
1540
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
1388
1541
|
if (value instanceof EntityRelation) return value;
|
|
1542
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
1543
|
+
if (value === "") return null;
|
|
1544
|
+
return new EntityRelation(value, targetPath);
|
|
1545
|
+
}
|
|
1389
1546
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1390
1547
|
const obj = value;
|
|
1391
1548
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -1657,6 +1814,48 @@ function getTableName(collection) {
|
|
|
1657
1814
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
1658
1815
|
}
|
|
1659
1816
|
/**
|
|
1817
|
+
* The field key a database column is served and addressed under.
|
|
1818
|
+
*
|
|
1819
|
+
* A column has two names and they are not the same name. `author_id` is what
|
|
1820
|
+
* Postgres stores; `authorId` is the key on the JSON row, the key in the
|
|
1821
|
+
* generated Drizzle table, and the key a caller writes in `where` and
|
|
1822
|
+
* `orderBy`. Every place that starts from a column and has to reach a row, a
|
|
1823
|
+
* Drizzle table or a payload goes through here, so there is one answer rather
|
|
1824
|
+
* than one per call site — the two that disagreed put `displayName` and
|
|
1825
|
+
* `author_id` on the same API.
|
|
1826
|
+
*
|
|
1827
|
+
* A declared property is the authority when there is one, because its key *is*
|
|
1828
|
+
* the wire name and `columnName` is the only thing that ever renamed the
|
|
1829
|
+
* column:
|
|
1830
|
+
*
|
|
1831
|
+
* 1. an explicit `columnName` equal to this column;
|
|
1832
|
+
* 2. a property whose key is literally the column (an author who wrote
|
|
1833
|
+
* `author_id:` meant `author_id` on the wire, and gets it);
|
|
1834
|
+
* 3. a property whose key snake-cases to the column, which is the default
|
|
1835
|
+
* mapping — `authorId` → `author_id`.
|
|
1836
|
+
*
|
|
1837
|
+
* With no property in the way — a foreign key derived from a relation, which
|
|
1838
|
+
* usually has none — the name is derived: {@link toWireKey}.
|
|
1839
|
+
*
|
|
1840
|
+
* Note the fallback is *not* the column verbatim. That was the old behaviour
|
|
1841
|
+
* and it is precisely the defect: a derived foreign key reached the wire under
|
|
1842
|
+
* its column name while every hand-authored field beside it was camelCase.
|
|
1843
|
+
*/
|
|
1844
|
+
function fieldKeyForColumn(collection, column) {
|
|
1845
|
+
const properties = collection?.properties;
|
|
1846
|
+
if (properties) {
|
|
1847
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
1848
|
+
const columnName = prop?.columnName;
|
|
1849
|
+
if (typeof columnName === "string" && columnName === column) return key;
|
|
1850
|
+
}
|
|
1851
|
+
for (const key of Object.keys(properties)) {
|
|
1852
|
+
if (key === column) return key;
|
|
1853
|
+
if (toSnakeCase(key) === column) return key;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return toWireKey(column);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1660
1859
|
* Look up a relation by key with forgiving normalization.
|
|
1661
1860
|
*
|
|
1662
1861
|
* `resolveCollectionRelations` stores each relation under a single canonical
|
|
@@ -1749,6 +1948,37 @@ function getSubcollections(collection) {
|
|
|
1749
1948
|
return getEntityChildViews(collection).map((view) => view.collection);
|
|
1750
1949
|
}
|
|
1751
1950
|
//#endregion
|
|
1951
|
+
//#region ../common/src/util/policy/sqlToPolicy.ts
|
|
1952
|
+
/**
|
|
1953
|
+
* Literals from other BaaS platforms that people compare `rebase.uid()` against
|
|
1954
|
+
* out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
|
|
1955
|
+
* `pgRoles`, one surface over: the same muscle memory inside a `using:` string
|
|
1956
|
+
* is the more dangerous spelling, because it inverts a rule instead of
|
|
1957
|
+
* emptying a table.
|
|
1958
|
+
*/
|
|
1959
|
+
/**
|
|
1960
|
+
* A `Map`, not an object literal.
|
|
1961
|
+
*
|
|
1962
|
+
* As `Record<string, string>` this was indexed with a literal taken straight
|
|
1963
|
+
* out of a policy, so every key on `Object.prototype` answered: a rule
|
|
1964
|
+
* comparing `rebase.uid()` to `"valueOf"`, `"toString"`, `"constructor"` or
|
|
1965
|
+
* `"hasOwnProperty"` found a truthy "platform" and reported an anonymous-grant
|
|
1966
|
+
* risk that does not exist — with the matched function interpolated into the
|
|
1967
|
+
* explanation as the platform's name. A security warning that fires on
|
|
1968
|
+
* innocent input is worse than none: it is what teaches people to skip the
|
|
1969
|
+
* warnings that are real.
|
|
1970
|
+
*
|
|
1971
|
+
* Same shape as the prototype-pollution class swept out of `setIn`, `getIn`,
|
|
1972
|
+
* `mergeDeep` and `unflattenObject` — a data-derived key reaching a plain
|
|
1973
|
+
* object. Found by a property test, on the input `"valueOf"`.
|
|
1974
|
+
*/
|
|
1975
|
+
var FOREIGN_CONVENTION_UIDS = /* @__PURE__ */ new Map([
|
|
1976
|
+
["anon", "Supabase"],
|
|
1977
|
+
["authenticated", "Supabase"],
|
|
1978
|
+
["service_role", "Supabase"]
|
|
1979
|
+
]);
|
|
1980
|
+
new RegExp(String.raw`rebase\.uid\(\)\s*=\s*'(${[...FOREIGN_CONVENTION_UIDS.keys()].join("|")})'`, "i");
|
|
1981
|
+
//#endregion
|
|
1752
1982
|
//#region ../common/src/util/auth-default-policies.ts
|
|
1753
1983
|
/**
|
|
1754
1984
|
* Default RLS policies injected by the schema generator.
|
|
@@ -1756,8 +1986,14 @@ function getSubcollections(collection) {
|
|
|
1756
1986
|
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
1757
1987
|
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
1758
1988
|
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
1759
|
-
* authorization model. The server context (auth flows, migrations,
|
|
1760
|
-
* `
|
|
1989
|
+
* authorization model. The server context (auth flows, migrations, raw
|
|
1990
|
+
* `rebase.sql`) runs as the owner and bypasses RLS.
|
|
1991
|
+
*
|
|
1992
|
+
* `rebase.dataAsAdmin` is **not** in that set, despite the name: it is scoped as
|
|
1993
|
+
* `{ uid: "service", roles: ["admin"] }`, so it runs as `rebase_user` like any
|
|
1994
|
+
* other caller and clears the baseline below through the *admin* arm, not the
|
|
1995
|
+
* server arm. Which is why `disableDefaultPolicies` plus a lone
|
|
1996
|
+
* `policy.serverContext()` rule locks it out too.
|
|
1761
1997
|
*
|
|
1762
1998
|
* Because RLS default-denies, every collection is **locked by default**: with
|
|
1763
1999
|
* no rules, only the server context and admins can touch it. The generator
|
|
@@ -1815,11 +2051,28 @@ function getIdPropertyName(collection) {
|
|
|
1815
2051
|
*
|
|
1816
2052
|
* Collections that opt out via `disableDefaultPolicies` are returned unchanged.
|
|
1817
2053
|
*/
|
|
2054
|
+
/**
|
|
2055
|
+
* The restrictive write gate for an auth collection.
|
|
2056
|
+
*
|
|
2057
|
+
* Restrictive, so it is ANDed with everything else: whatever an author's
|
|
2058
|
+
* permissive rules allow, a write to this table still has to satisfy this too.
|
|
2059
|
+
* It is the only thing standing between "users may edit their own row" and
|
|
2060
|
+
* "users may grant themselves any role".
|
|
2061
|
+
*/
|
|
2062
|
+
function adminWriteGate(tableName) {
|
|
2063
|
+
return {
|
|
2064
|
+
name: `${tableName}_require_admin_write`,
|
|
2065
|
+
mode: "restrictive",
|
|
2066
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
2067
|
+
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2068
|
+
check: SERVER_OR_ADMIN_EXPR$1
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
1818
2071
|
function getEffectiveSecurityRules(collection) {
|
|
1819
2072
|
const explicit = [...collection.securityRules ?? []];
|
|
1820
|
-
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return explicit;
|
|
1821
2073
|
const tableName = getTableName(collection);
|
|
1822
2074
|
const injected = [];
|
|
2075
|
+
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection(collection) ? [...explicit, adminWriteGate(tableName)] : explicit;
|
|
1823
2076
|
injected.push({
|
|
1824
2077
|
name: `${tableName}_default_admin_read`,
|
|
1825
2078
|
operations: ["select"],
|
|
@@ -1837,13 +2090,7 @@ function getEffectiveSecurityRules(collection) {
|
|
|
1837
2090
|
operations: ["select"],
|
|
1838
2091
|
condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
|
|
1839
2092
|
});
|
|
1840
|
-
injected.push(
|
|
1841
|
-
name: `${tableName}_require_admin_write`,
|
|
1842
|
-
mode: "restrictive",
|
|
1843
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
1844
|
-
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
1845
|
-
check: SERVER_OR_ADMIN_EXPR$1
|
|
1846
|
-
});
|
|
2093
|
+
injected.push(adminWriteGate(tableName));
|
|
1847
2094
|
}
|
|
1848
2095
|
return [...explicit, ...injected];
|
|
1849
2096
|
}
|
|
@@ -2883,8 +3130,24 @@ var QueryBuilder = class {
|
|
|
2883
3130
|
/**
|
|
2884
3131
|
* Set a free-text search string if supported by the backend.
|
|
2885
3132
|
*/
|
|
2886
|
-
search(searchString) {
|
|
3133
|
+
search(searchString, options) {
|
|
2887
3134
|
this.params.searchString = searchString;
|
|
3135
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
3136
|
+
return this;
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Order rows by nearest-neighbour distance to `vector`, closest first.
|
|
3140
|
+
*
|
|
3141
|
+
* Postgres only, over a property declared as `type: "vector"`. Rows come
|
|
3142
|
+
* back with a `_distance`; `where` filters before the ordering.
|
|
3143
|
+
*/
|
|
3144
|
+
vectorSearch(property, vector, options) {
|
|
3145
|
+
this.params.vectorSearch = {
|
|
3146
|
+
property,
|
|
3147
|
+
vector,
|
|
3148
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
3149
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
3150
|
+
};
|
|
2888
3151
|
return this;
|
|
2889
3152
|
}
|
|
2890
3153
|
/**
|
|
@@ -2941,6 +3204,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
2941
3204
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
2942
3205
|
}
|
|
2943
3206
|
};
|
|
3207
|
+
/**
|
|
3208
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3209
|
+
*
|
|
3210
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3211
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3212
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3213
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3214
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3215
|
+
* rows are the mildest of those outcomes.
|
|
3216
|
+
*
|
|
3217
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3218
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3219
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3220
|
+
* look like it is paging by offset.
|
|
3221
|
+
*/
|
|
3222
|
+
function resolveFindWindow(params) {
|
|
3223
|
+
const limit = params?.limit ?? 50;
|
|
3224
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3225
|
+
return {
|
|
3226
|
+
limit,
|
|
3227
|
+
offset,
|
|
3228
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
2944
3231
|
function normalizePageSize(raw) {
|
|
2945
3232
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
2946
3233
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3060,8 +3347,10 @@ async function collectAllPages(find, params, label = "collection") {
|
|
|
3060
3347
|
* metadata, so type coercion is the responsibility of the server-side data
|
|
3061
3348
|
* driver which has access to the collection schema.
|
|
3062
3349
|
*
|
|
3063
|
-
*
|
|
3064
|
-
*
|
|
3350
|
+
* Structural characters inside a value are backslash-escaped: `,` → `\,`,
|
|
3351
|
+
* `(` → `\(`, `)` → `\)`, and a literal backslash as `\\`. Decoding is
|
|
3352
|
+
* deliberately conservative — only those four sequences are decoded, so a
|
|
3353
|
+
* backslash that arrives unescaped from an older client survives intact.
|
|
3065
3354
|
*
|
|
3066
3355
|
* @module
|
|
3067
3356
|
*/
|
|
@@ -3079,22 +3368,55 @@ function stringifyValue(value) {
|
|
|
3079
3368
|
return String(value);
|
|
3080
3369
|
}
|
|
3081
3370
|
/**
|
|
3082
|
-
*
|
|
3083
|
-
*
|
|
3371
|
+
* Characters that carry structure in the wire format and must therefore be
|
|
3372
|
+
* escaped inside a value: the separator, the group delimiters, and the escape
|
|
3373
|
+
* character itself.
|
|
3374
|
+
*
|
|
3375
|
+
* Parentheses are here because `and(...)`/`or(...)` groups are parsed by
|
|
3376
|
+
* tracking paren depth. A value containing one is not merely ambiguous, it
|
|
3377
|
+
* moves where the parser thinks the group ends.
|
|
3378
|
+
*/
|
|
3379
|
+
var WIRE_SPECIALS = /[\\,()]/g;
|
|
3380
|
+
/**
|
|
3381
|
+
* Escape a value for the wire format: `\` → `\\`, `,` → `\,`, `(` → `\(`,
|
|
3382
|
+
* `)` → `\)`.
|
|
3084
3383
|
*/
|
|
3085
|
-
|
|
3086
|
-
|
|
3384
|
+
/**
|
|
3385
|
+
* The wire spelling of an empty list.
|
|
3386
|
+
*
|
|
3387
|
+
* A lone backslash: unproducible by {@link escapeWireValue}, which doubles
|
|
3388
|
+
* every backslash it emits, so it cannot collide with any real item.
|
|
3389
|
+
*/
|
|
3390
|
+
var EMPTY_LIST_TOKEN = "\\";
|
|
3391
|
+
function escapeWireValue(value) {
|
|
3392
|
+
return value.replace(WIRE_SPECIALS, (ch) => `\\${ch}`);
|
|
3087
3393
|
}
|
|
3088
3394
|
/**
|
|
3089
|
-
* Unescape a
|
|
3090
|
-
*
|
|
3395
|
+
* Unescape a wire-format value.
|
|
3396
|
+
*
|
|
3397
|
+
* **Conservative**, and deliberately so: only the four sequences
|
|
3398
|
+
* {@link escapeWireValue} actually produces are decoded. A backslash followed
|
|
3399
|
+
* by anything else is left exactly as it is.
|
|
3400
|
+
*
|
|
3401
|
+
* This used to consume the backslash before *any* character, which is
|
|
3402
|
+
* indistinguishable for anything this codec emitted — it only ever emits those
|
|
3403
|
+
* four — but not for input arriving from elsewhere. A client on an older
|
|
3404
|
+
* release sends a Windows path or a LIKE pattern with a literal `C:\x`
|
|
3405
|
+
* unescaped, and greedy unescaping silently turned it into `C:x`, changing
|
|
3406
|
+
* which rows matched. Decoding only what the encoder can produce makes the two
|
|
3407
|
+
* directions agree across versions.
|
|
3091
3408
|
*/
|
|
3092
|
-
function
|
|
3409
|
+
function unescapeWireValue(value) {
|
|
3093
3410
|
let result = "";
|
|
3094
|
-
for (let i = 0; i < value.length; i++)
|
|
3095
|
-
|
|
3096
|
-
i
|
|
3097
|
-
|
|
3411
|
+
for (let i = 0; i < value.length; i++) {
|
|
3412
|
+
const next = value[i + 1];
|
|
3413
|
+
if (value[i] === "\\" && (next === "\\" || next === "," || next === "(" || next === ")")) {
|
|
3414
|
+
result += next;
|
|
3415
|
+
i++;
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
result += value[i];
|
|
3419
|
+
}
|
|
3098
3420
|
return result;
|
|
3099
3421
|
}
|
|
3100
3422
|
/**
|
|
@@ -3112,14 +3434,235 @@ function splitListItems(inner) {
|
|
|
3112
3434
|
current += inner[i] + inner[i + 1];
|
|
3113
3435
|
i++;
|
|
3114
3436
|
} else if (inner[i] === ",") {
|
|
3115
|
-
items.push(
|
|
3437
|
+
items.push(unescapeWireValue(current));
|
|
3116
3438
|
current = "";
|
|
3117
3439
|
} else current += inner[i];
|
|
3118
|
-
items.push(
|
|
3440
|
+
items.push(unescapeWireValue(current));
|
|
3119
3441
|
return items;
|
|
3120
3442
|
}
|
|
3121
|
-
|
|
3122
|
-
|
|
3443
|
+
/**
|
|
3444
|
+
* Split a group body on commas at paren depth 0, honouring escapes.
|
|
3445
|
+
*
|
|
3446
|
+
* The escape-awareness is the point. The splitter used to track only paren
|
|
3447
|
+
* depth, so a comma inside a scalar value ended a condition:
|
|
3448
|
+
* `or(name.eq.Doe, John,age.gte.18)` parsed as *three* conditions, the middle
|
|
3449
|
+
* one a fabricated `" John" == true`. On an `or` that widens the result set,
|
|
3450
|
+
* and nothing anywhere reports an error — the query simply stops meaning what
|
|
3451
|
+
* the caller wrote.
|
|
3452
|
+
*/
|
|
3453
|
+
function splitGroupItems(inner) {
|
|
3454
|
+
const parts = [];
|
|
3455
|
+
let depth = 0;
|
|
3456
|
+
let start = 0;
|
|
3457
|
+
for (let i = 0; i < inner.length; i++) {
|
|
3458
|
+
const ch = inner[i];
|
|
3459
|
+
if (ch === "\\" && i + 1 < inner.length) {
|
|
3460
|
+
i++;
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
if (ch === "(") depth++;
|
|
3464
|
+
else if (ch === ")") depth--;
|
|
3465
|
+
else if (ch === "," && depth === 0) {
|
|
3466
|
+
parts.push(inner.slice(start, i));
|
|
3467
|
+
start = i + 1;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
parts.push(inner.slice(start));
|
|
3471
|
+
return parts;
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Operator tables as `Map`s, because the key comes off the wire.
|
|
3475
|
+
*
|
|
3476
|
+
* Indexed as plain objects, every `Object.prototype` member answered: a query
|
|
3477
|
+
* string of `?f=valueOf.x` found a truthy "operator" — the inherited function —
|
|
3478
|
+
* and `deserializeTuple` returned it *as the operator*, so a function object
|
|
3479
|
+
* travelled on into the compilers in place of a `WhereFilterOp`. The guard one
|
|
3480
|
+
* line below (`if (!canonicalOp)`) reads as though it rejects anything unknown,
|
|
3481
|
+
* and does not: `Object.prototype` is not unknown to a plain object.
|
|
3482
|
+
*
|
|
3483
|
+
* Same shape as the prototype-key defects swept out of `setIn`, `getIn`,
|
|
3484
|
+
* `mergeDeep`, `unflattenObject` and `FOREIGN_CONVENTION_UIDS`.
|
|
3485
|
+
*/
|
|
3486
|
+
var REST_OP_LOOKUP = new Map(Object.entries(REST_TO_CANONICAL));
|
|
3487
|
+
var CANONICAL_OP_LOOKUP = new Map(Object.entries(CANONICAL_TO_REST));
|
|
3488
|
+
/** The operator spellings a rejection lists back to the caller. */
|
|
3489
|
+
var VALID_OPERATOR_LIST = ALL_WHERE_FILTER_OPS.join(", ");
|
|
3490
|
+
/**
|
|
3491
|
+
* A filter condition named an operator this dialect does not have.
|
|
3492
|
+
*
|
|
3493
|
+
* ## Why this throws, rather than returning a typed rejection
|
|
3494
|
+
*
|
|
3495
|
+
* `deserializeFilter` is the *shared* codec: the REST ingress
|
|
3496
|
+
* (`packages/server/src/api/rest/query-parser.ts`), the browser SDK and the
|
|
3497
|
+
* admin panel (`buildRebaseData.ts`) all decode through it. Two constraints
|
|
3498
|
+
* follow.
|
|
3499
|
+
*
|
|
3500
|
+
* - It cannot throw the server's `ApiError`. `@rebasepro/common` does not
|
|
3501
|
+
* depend on `@rebasepro/server` (the dependency runs the other way), and a
|
|
3502
|
+
* browser client has no error handler to render an `ApiError` with. So the
|
|
3503
|
+
* rejection is this plain `Error` subclass, whose `message` reads correctly
|
|
3504
|
+
* wherever it surfaces — a rejected promise in an app, a 400 body over HTTP.
|
|
3505
|
+
* - It cannot be a returned rejection *value*. Every caller assigns the result
|
|
3506
|
+
* straight into a query it is about to run; a sentinel that none of them
|
|
3507
|
+
* check would be ignored, which is exactly the silently-wrong-filter failure
|
|
3508
|
+
* this exists to stop. Throwing is also what this file already does for the
|
|
3509
|
+
* sibling cases — `serializeTuple` on an unknown canonical operator,
|
|
3510
|
+
* `deserializeLogicalCondition` past the nesting bound — and the REST parser
|
|
3511
|
+
* already converts the latter into a 400.
|
|
3512
|
+
*
|
|
3513
|
+
* `statusCode`, `code` and `details` are carried as fields because the server's
|
|
3514
|
+
* Hono error handler duck-types those off any thrown error: a decode path that
|
|
3515
|
+
* forgets to convert still answers 400 with the canonical envelope instead of a
|
|
3516
|
+
* 500 that says "An unexpected error occurred". `query-parser.ts` converts
|
|
3517
|
+
* explicitly all the same — that is the path the contract is stated on, and an
|
|
3518
|
+
* incidental 400 is not a contract.
|
|
3519
|
+
*/
|
|
3520
|
+
var UnknownFilterOperatorError = class extends Error {
|
|
3521
|
+
/** The field the condition was written against. */
|
|
3522
|
+
field;
|
|
3523
|
+
/** The operator string as it arrived, verbatim. */
|
|
3524
|
+
operator;
|
|
3525
|
+
/** Every operator this dialect accepts, in canonical spelling. */
|
|
3526
|
+
validOperators = ALL_WHERE_FILTER_OPS;
|
|
3527
|
+
/** See the class docblock: read by the server's error handler. */
|
|
3528
|
+
statusCode = 400;
|
|
3529
|
+
code = "UNKNOWN_FILTER_OPERATOR";
|
|
3530
|
+
details;
|
|
3531
|
+
constructor(field, operator) {
|
|
3532
|
+
super(`Unknown filter operator '${operator}' on field '${field}'. Valid operators: ${VALID_OPERATOR_LIST}`);
|
|
3533
|
+
this.name = "UnknownFilterOperatorError";
|
|
3534
|
+
this.field = field;
|
|
3535
|
+
this.operator = operator;
|
|
3536
|
+
this.details = {
|
|
3537
|
+
field,
|
|
3538
|
+
operator,
|
|
3539
|
+
validOperators: ALL_WHERE_FILTER_OPS
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
/**
|
|
3544
|
+
* Two to three characters of ASCII punctuation and nothing else — the shape
|
|
3545
|
+
* every symbolic operator has (`==`, `>=`, `<>`, `~~`, `!!`, `>>`, `===`), and
|
|
3546
|
+
* one a column value effectively never has.
|
|
3547
|
+
*
|
|
3548
|
+
* Two characters minimum on purpose. A *single* punctuation character is a
|
|
3549
|
+
* perfectly ordinary value — `{ grade: ["-", "+"] }` is a two-item list, not a
|
|
3550
|
+
* condition — and the only single-character operator anyone actually mistypes
|
|
3551
|
+
* is `=`, which is named separately below. `<` and `>` need no special case:
|
|
3552
|
+
* they are real operators and resolve.
|
|
3553
|
+
*/
|
|
3554
|
+
var SYMBOLIC_OPERATOR = /^[^\p{L}\p{N}\s]{2,3}$/u;
|
|
3555
|
+
/** Lowercase, strip everything that is not a letter or digit. */
|
|
3556
|
+
function normalizeOperatorName(op) {
|
|
3557
|
+
return op.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* Every real operator name with its case and separators removed, so a
|
|
3561
|
+
* respelling of one — `arrayContains`, `not_in`, `NOT-LIKE`, `isNull` — is
|
|
3562
|
+
* recognised as an attempt at an operator rather than read as a value.
|
|
3563
|
+
*
|
|
3564
|
+
* These are rejected rather than accepted: admitting a second spelling of an
|
|
3565
|
+
* operator would leave two wire spellings of one thing, and the rejection
|
|
3566
|
+
* message names the one that works.
|
|
3567
|
+
*/
|
|
3568
|
+
var RESPELLED_OPERATORS = new Set([...ALL_WHERE_FILTER_OPS, ...Object.keys(REST_TO_CANONICAL)].map(normalizeOperatorName));
|
|
3569
|
+
/**
|
|
3570
|
+
* Operator names *other* query dialects use, which this one does not have.
|
|
3571
|
+
*
|
|
3572
|
+
* This list is curated, and deliberately so. For a word-shaped string there is
|
|
3573
|
+
* no rule that separates "an operator the caller guessed" from "a value that
|
|
3574
|
+
* happens to be a word": `{ tags: ["a", "b"] }` has to keep meaning a two-item
|
|
3575
|
+
* `in` list, so the codec cannot simply refuse every unrecognised word in
|
|
3576
|
+
* position 0. The line is therefore drawn by name, and only around names whose
|
|
3577
|
+
* use as an operator is far more likely than their use as one of two sibling
|
|
3578
|
+
* values. `contains` is the motivating case — the first thing a developer
|
|
3579
|
+
* reaches for, and until now it compiled to `title IN ('contains', 'Hell')`.
|
|
3580
|
+
*
|
|
3581
|
+
* Genuinely ambiguous single words (`any`, `all`, `exists`, `search`, `not`)
|
|
3582
|
+
* are left off: as operators they are rare, and as enum values they are common.
|
|
3583
|
+
* Everywhere else the tie goes to *rejecting*, because a 400 naming the
|
|
3584
|
+
* supported set costs the caller one round trip, and the alternative — which is
|
|
3585
|
+
* what every name on this list used to produce — is a query that runs, returns
|
|
3586
|
+
* rows, and is wrong.
|
|
3587
|
+
*/
|
|
3588
|
+
var NEAR_MISS_OPERATORS = /* @__PURE__ */ new Set([
|
|
3589
|
+
"contains",
|
|
3590
|
+
"notcontains",
|
|
3591
|
+
"doesnotcontain",
|
|
3592
|
+
"doesnotcontains",
|
|
3593
|
+
"includes",
|
|
3594
|
+
"notincludes",
|
|
3595
|
+
"startswith",
|
|
3596
|
+
"notstartswith",
|
|
3597
|
+
"beginswith",
|
|
3598
|
+
"startingwith",
|
|
3599
|
+
"endswith",
|
|
3600
|
+
"notendswith",
|
|
3601
|
+
"matches",
|
|
3602
|
+
"notmatches",
|
|
3603
|
+
"regex",
|
|
3604
|
+
"regexp",
|
|
3605
|
+
"between",
|
|
3606
|
+
"notbetween",
|
|
3607
|
+
"equals",
|
|
3608
|
+
"notequals",
|
|
3609
|
+
"equalto",
|
|
3610
|
+
"isequalto",
|
|
3611
|
+
"isnotequalto",
|
|
3612
|
+
"greaterthan",
|
|
3613
|
+
"greaterthanorequal",
|
|
3614
|
+
"greaterthanorequalto",
|
|
3615
|
+
"lessthan",
|
|
3616
|
+
"lessthanorequal",
|
|
3617
|
+
"lessthanorequalto",
|
|
3618
|
+
"isempty",
|
|
3619
|
+
"isnotempty",
|
|
3620
|
+
"oneof",
|
|
3621
|
+
"noneof",
|
|
3622
|
+
"anyof",
|
|
3623
|
+
"allof",
|
|
3624
|
+
"null",
|
|
3625
|
+
"isnullorempty"
|
|
3626
|
+
]);
|
|
3627
|
+
/**
|
|
3628
|
+
* Was this string *meant* as an operator?
|
|
3629
|
+
*
|
|
3630
|
+
* Only consulted after {@link toCanonicalOp} has already failed to resolve it,
|
|
3631
|
+
* so a `true` here is always a rejection.
|
|
3632
|
+
*/
|
|
3633
|
+
function isOperatorShaped(op) {
|
|
3634
|
+
if (op === "=") return true;
|
|
3635
|
+
if (SYMBOLIC_OPERATOR.test(op)) return true;
|
|
3636
|
+
const normalized = normalizeOperatorName(op);
|
|
3637
|
+
if (!normalized) return false;
|
|
3638
|
+
return RESPELLED_OPERATORS.has(normalized) || NEAR_MISS_OPERATORS.has(normalized);
|
|
3639
|
+
}
|
|
3640
|
+
/**
|
|
3641
|
+
* Read a `[op, value]` tuple, if that is what this is.
|
|
3642
|
+
*
|
|
3643
|
+
* Three outcomes, and the middle one is the defect this function exists for:
|
|
3644
|
+
*
|
|
3645
|
+
* - the operator resolves (canonical *or* REST spelling) → the canonical tuple;
|
|
3646
|
+
* - the operator does not resolve but was plainly meant as one → throw;
|
|
3647
|
+
* - it does not look like an operator at all → `undefined`, and the caller
|
|
3648
|
+
* falls back to reading the array as a list of values.
|
|
3649
|
+
*
|
|
3650
|
+
* The old test was `toCanonicalOp(raw[0]) === raw[0]`, i.e. canonical spelling
|
|
3651
|
+
* only, with *everything else* — including every REST short-code — dropping
|
|
3652
|
+
* through to `["in", raw]`. So the operator string itself became a value in a
|
|
3653
|
+
* membership test: `["!!", "Hello"]` compiled to `title IN ('!!','Hello')`,
|
|
3654
|
+
* which matches, and the caller got back rows their filter was written to
|
|
3655
|
+
* exclude. `["eq", "active"]` had the same shape of failure.
|
|
3656
|
+
*/
|
|
3657
|
+
function readTuple(field, raw) {
|
|
3658
|
+
if (!Array.isArray(raw) || raw.length !== 2) return void 0;
|
|
3659
|
+
const [op, value] = raw;
|
|
3660
|
+
if (typeof op !== "string") return void 0;
|
|
3661
|
+
const canonical = toCanonicalOp(op);
|
|
3662
|
+
if (canonical) return [canonical, value];
|
|
3663
|
+
if (op.includes(".")) return void 0;
|
|
3664
|
+
if (isOperatorShaped(op)) throw new UnknownFilterOperatorError(field, op);
|
|
3665
|
+
}
|
|
3123
3666
|
/**
|
|
3124
3667
|
* Serialize a single canonical condition tuple to a PostgREST dot-string.
|
|
3125
3668
|
*
|
|
@@ -3134,9 +3677,13 @@ function serializeTuple(tuple) {
|
|
|
3134
3677
|
if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
|
|
3135
3678
|
const [op, value] = tuple;
|
|
3136
3679
|
if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
|
|
3137
|
-
const restOp = CANONICAL_OP_LOOKUP
|
|
3680
|
+
const restOp = CANONICAL_OP_LOOKUP.get(op);
|
|
3138
3681
|
if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`);
|
|
3139
|
-
if (
|
|
3682
|
+
if (value === null && (op === "==" || op === "!=")) return op === "==" ? "isnull.null" : "notnull.null";
|
|
3683
|
+
if (Array.isArray(value)) {
|
|
3684
|
+
if (value.length === 0) return `${restOp}.(${EMPTY_LIST_TOKEN})`;
|
|
3685
|
+
return `${restOp}.(${value.map((v) => escapeWireValue(stringifyValue(v))).join(",")})`;
|
|
3686
|
+
}
|
|
3140
3687
|
return `${restOp}.${stringifyValue(value)}`;
|
|
3141
3688
|
}
|
|
3142
3689
|
/**
|
|
@@ -3188,10 +3735,13 @@ function deserializeSingle(raw) {
|
|
|
3188
3735
|
if (dotIndex === -1) return ["==", raw];
|
|
3189
3736
|
const prefix = raw.substring(0, dotIndex);
|
|
3190
3737
|
const rest = raw.substring(dotIndex + 1);
|
|
3191
|
-
const canonicalOp = REST_OP_LOOKUP
|
|
3738
|
+
const canonicalOp = REST_OP_LOOKUP.get(prefix);
|
|
3192
3739
|
if (!canonicalOp) return ["==", raw];
|
|
3193
3740
|
if (NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
|
|
3194
|
-
if (rest.startsWith("(") && rest.endsWith(")"))
|
|
3741
|
+
if (rest.startsWith("(") && rest.endsWith(")")) {
|
|
3742
|
+
const inner = rest.slice(1, -1);
|
|
3743
|
+
return [canonicalOp, inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)];
|
|
3744
|
+
}
|
|
3195
3745
|
return [canonicalOp, rest];
|
|
3196
3746
|
}
|
|
3197
3747
|
/**
|
|
@@ -3206,20 +3756,27 @@ function deserializeSingle(raw) {
|
|
|
3206
3756
|
*
|
|
3207
3757
|
* deserializeFilter({ age: ["gte.18", "lt.65"] })
|
|
3208
3758
|
* // → { age: [[">=", "18"], ["<", "65"]] }
|
|
3759
|
+
*
|
|
3760
|
+
* @throws {UnknownFilterOperatorError} when a condition names an operator this
|
|
3761
|
+
* dialect does not have. See that class for why a rejection here is a throw.
|
|
3209
3762
|
*/
|
|
3210
3763
|
function deserializeFilter(query) {
|
|
3211
3764
|
const result = {};
|
|
3212
3765
|
for (const [field, raw] of Object.entries(query)) {
|
|
3213
3766
|
if (raw === void 0) continue;
|
|
3214
|
-
|
|
3215
|
-
|
|
3767
|
+
const tuple = readTuple(field, raw);
|
|
3768
|
+
if (tuple) {
|
|
3769
|
+
result[field] = tuple;
|
|
3216
3770
|
continue;
|
|
3217
3771
|
}
|
|
3218
3772
|
if (Array.isArray(raw)) {
|
|
3219
3773
|
if (raw.length === 0) continue;
|
|
3220
|
-
if (Array.isArray(raw[0])
|
|
3221
|
-
|
|
3222
|
-
|
|
3774
|
+
if (Array.isArray(raw[0])) {
|
|
3775
|
+
const tuples = raw.map((item) => readTuple(field, item));
|
|
3776
|
+
if (tuples.every((t) => t !== void 0)) {
|
|
3777
|
+
result[field] = tuples;
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3223
3780
|
}
|
|
3224
3781
|
if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
|
|
3225
3782
|
else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
|
|
@@ -3244,42 +3801,22 @@ function serializeLogicalCondition(cond) {
|
|
|
3244
3801
|
const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
|
|
3245
3802
|
return `${cond.type}(${inner})`;
|
|
3246
3803
|
}
|
|
3247
|
-
const restOp = CANONICAL_OP_LOOKUP
|
|
3804
|
+
const restOp = CANONICAL_OP_LOOKUP.get(cond.operator) ?? "eq";
|
|
3248
3805
|
if (Array.isArray(cond.value)) {
|
|
3249
|
-
const items = cond.value.map((v) =>
|
|
3806
|
+
const items = cond.value.map((v) => escapeWireValue(stringifyValue(v))).join(",");
|
|
3250
3807
|
return `${cond.column}.${restOp}.(${items})`;
|
|
3251
3808
|
}
|
|
3252
|
-
return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
|
|
3809
|
+
return `${cond.column}.${restOp}.${escapeWireValue(stringifyValue(cond.value))}`;
|
|
3253
3810
|
}
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
* `LogicalCondition` or `FilterCondition`.
|
|
3257
|
-
*
|
|
3258
|
-
* @example
|
|
3259
|
-
* deserializeLogicalCondition("status.eq.active")
|
|
3260
|
-
* // → { column: "status", operator: "==", value: "active" }
|
|
3261
|
-
*
|
|
3262
|
-
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
3263
|
-
* // → { type: "or", conditions: [...] }
|
|
3264
|
-
*/
|
|
3265
|
-
function deserializeLogicalCondition(str) {
|
|
3811
|
+
function deserializeLogicalCondition(str, nesting = 0) {
|
|
3812
|
+
if (nesting > 32) throw new Error(`Filter groups nest more than 32 levels deep. Flatten the condition — \`or(a,or(b,c))\` is \`or(a,b,c)\`.`);
|
|
3266
3813
|
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
3267
3814
|
if (logicalMatch) {
|
|
3268
3815
|
const type = logicalMatch[1];
|
|
3269
3816
|
const innerStr = logicalMatch[2];
|
|
3270
|
-
const conditions = [];
|
|
3271
|
-
let depth = 0;
|
|
3272
|
-
let start = 0;
|
|
3273
|
-
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
3274
|
-
else if (innerStr[i] === ")") depth--;
|
|
3275
|
-
else if (innerStr[i] === "," && depth === 0) {
|
|
3276
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
3277
|
-
start = i + 1;
|
|
3278
|
-
}
|
|
3279
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
3280
3817
|
return {
|
|
3281
3818
|
type,
|
|
3282
|
-
conditions
|
|
3819
|
+
conditions: splitGroupItems(innerStr).map((part) => deserializeLogicalCondition(part, nesting + 1))
|
|
3283
3820
|
};
|
|
3284
3821
|
}
|
|
3285
3822
|
const firstDot = str.indexOf(".");
|
|
@@ -3294,7 +3831,7 @@ function deserializeLogicalCondition(str) {
|
|
|
3294
3831
|
if (secondDot === -1) return {
|
|
3295
3832
|
column,
|
|
3296
3833
|
operator: "==",
|
|
3297
|
-
value: rest
|
|
3834
|
+
value: unescapeWireValue(rest)
|
|
3298
3835
|
};
|
|
3299
3836
|
const opStr = rest.substring(0, secondDot);
|
|
3300
3837
|
const valueStr = rest.substring(secondDot + 1);
|
|
@@ -3307,7 +3844,7 @@ function deserializeLogicalCondition(str) {
|
|
|
3307
3844
|
return {
|
|
3308
3845
|
column,
|
|
3309
3846
|
operator,
|
|
3310
|
-
value: valueStr
|
|
3847
|
+
value: unescapeWireValue(valueStr)
|
|
3311
3848
|
};
|
|
3312
3849
|
}
|
|
3313
3850
|
//#endregion
|
|
@@ -3344,10 +3881,12 @@ function createPrimaryKeyResolver(options) {
|
|
|
3344
3881
|
* than postgres still serve rows with one, and this keeps them working.
|
|
3345
3882
|
*/
|
|
3346
3883
|
function rowToEntity(row, slug, primaryKeys = []) {
|
|
3884
|
+
const { _matches, ...values } = row;
|
|
3347
3885
|
return {
|
|
3348
3886
|
id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
|
|
3349
3887
|
path: slug,
|
|
3350
|
-
values
|
|
3888
|
+
values,
|
|
3889
|
+
..._matches ? { searchMatches: _matches } : {}
|
|
3351
3890
|
};
|
|
3352
3891
|
}
|
|
3353
3892
|
/**
|
|
@@ -3390,21 +3929,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3390
3929
|
const accessor = {
|
|
3391
3930
|
async find(params) {
|
|
3392
3931
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3393
|
-
const limit = params
|
|
3394
|
-
const offset = params?.offset ?? 0;
|
|
3932
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3395
3933
|
const fetchService = driver.restFetchService;
|
|
3396
3934
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3397
3935
|
filter,
|
|
3398
|
-
|
|
3399
|
-
|
|
3936
|
+
logical: params?.logical,
|
|
3937
|
+
limit,
|
|
3938
|
+
offset: driverOffset,
|
|
3400
3939
|
orderBy: params?.orderBy?.[0],
|
|
3401
3940
|
order: params?.orderBy?.[1],
|
|
3402
3941
|
searchString: params?.searchString
|
|
3403
3942
|
}, params?.include) : await driver.fetchCollection({
|
|
3404
3943
|
path: slug,
|
|
3405
|
-
limit
|
|
3406
|
-
offset:
|
|
3944
|
+
limit,
|
|
3945
|
+
offset: driverOffset,
|
|
3407
3946
|
filter,
|
|
3947
|
+
logical: params?.logical,
|
|
3408
3948
|
orderBy: params?.orderBy?.[0],
|
|
3409
3949
|
order: params?.orderBy?.[1],
|
|
3410
3950
|
searchString: params?.searchString
|
|
@@ -3414,7 +3954,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3414
3954
|
if (driver.count) {
|
|
3415
3955
|
total = await driver.count({
|
|
3416
3956
|
path: slug,
|
|
3417
|
-
filter
|
|
3957
|
+
filter,
|
|
3958
|
+
logical: params?.logical,
|
|
3959
|
+
searchString: params?.searchString
|
|
3418
3960
|
});
|
|
3419
3961
|
hasMore = offset + rows.length < total;
|
|
3420
3962
|
}
|
|
@@ -3466,30 +4008,48 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3466
4008
|
values: {}
|
|
3467
4009
|
} });
|
|
3468
4010
|
},
|
|
4011
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
4012
|
+
return (await driver.updateMany({
|
|
4013
|
+
path: slug,
|
|
4014
|
+
updates: updates.map((u) => ({
|
|
4015
|
+
id: u.id,
|
|
4016
|
+
values: u.data
|
|
4017
|
+
}))
|
|
4018
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
4019
|
+
} : void 0,
|
|
4020
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
4021
|
+
await driver.deleteMany({
|
|
4022
|
+
path: slug,
|
|
4023
|
+
ids
|
|
4024
|
+
});
|
|
4025
|
+
} : void 0,
|
|
3469
4026
|
count: driver.count ? async (params) => {
|
|
3470
4027
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3471
4028
|
return driver.count({
|
|
3472
4029
|
path: slug,
|
|
3473
|
-
filter
|
|
4030
|
+
filter,
|
|
4031
|
+
logical: params?.logical,
|
|
4032
|
+
searchString: params?.searchString
|
|
3474
4033
|
});
|
|
3475
4034
|
} : void 0,
|
|
3476
4035
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
3477
|
-
const limit = params
|
|
3478
|
-
const offset = params?.offset ?? 0;
|
|
4036
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3479
4037
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
3480
4038
|
return driver.listenCollection({
|
|
3481
4039
|
path: slug,
|
|
3482
|
-
limit
|
|
3483
|
-
offset:
|
|
4040
|
+
limit,
|
|
4041
|
+
offset: driverOffset,
|
|
3484
4042
|
filter: params?.where,
|
|
4043
|
+
logical: params?.logical,
|
|
3485
4044
|
orderBy: params?.orderBy?.[0],
|
|
3486
4045
|
order: params?.orderBy?.[1],
|
|
3487
4046
|
searchString: params?.searchString,
|
|
4047
|
+
searchExplain: params?.searchExplain,
|
|
3488
4048
|
onUpdate: (entities) => {
|
|
3489
4049
|
onUpdate({
|
|
3490
4050
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
3491
4051
|
meta: {
|
|
3492
|
-
total: entities.length,
|
|
4052
|
+
total: offset + entities.length,
|
|
3493
4053
|
limit,
|
|
3494
4054
|
offset,
|
|
3495
4055
|
hasMore: entities.length >= limit
|
|
@@ -3522,8 +4082,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3522
4082
|
offset(count) {
|
|
3523
4083
|
return new QueryBuilder(accessor).offset(count);
|
|
3524
4084
|
},
|
|
3525
|
-
search(searchString) {
|
|
3526
|
-
return new QueryBuilder(accessor).search(searchString);
|
|
4085
|
+
search(searchString, options) {
|
|
4086
|
+
return new QueryBuilder(accessor).search(searchString, options);
|
|
4087
|
+
},
|
|
4088
|
+
vectorSearch(property, vector, options) {
|
|
4089
|
+
return new QueryBuilder(accessor).vectorSearch(property, vector, options);
|
|
3527
4090
|
},
|
|
3528
4091
|
include(...relations) {
|
|
3529
4092
|
return new QueryBuilder(accessor).include(...relations);
|
|
@@ -3611,8 +4174,18 @@ var SdkQueryBuilder = class {
|
|
|
3611
4174
|
this.params.offset = count;
|
|
3612
4175
|
return this;
|
|
3613
4176
|
}
|
|
3614
|
-
search(searchString) {
|
|
4177
|
+
search(searchString, options) {
|
|
3615
4178
|
this.params.searchString = searchString;
|
|
4179
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
4180
|
+
return this;
|
|
4181
|
+
}
|
|
4182
|
+
vectorSearch(property, vector, options) {
|
|
4183
|
+
this.params.vectorSearch = {
|
|
4184
|
+
property,
|
|
4185
|
+
vector,
|
|
4186
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
4187
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
4188
|
+
};
|
|
3616
4189
|
return this;
|
|
3617
4190
|
}
|
|
3618
4191
|
include(...relations) {
|
|
@@ -3666,9 +4239,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3666
4239
|
async update(id, data) {
|
|
3667
4240
|
return entityToRow(await snap.update(id, data));
|
|
3668
4241
|
},
|
|
4242
|
+
async updateMany(updates) {
|
|
4243
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4244
|
+
if (updates.length === 0) return [];
|
|
4245
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
4246
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
4247
|
+
id: u.id,
|
|
4248
|
+
data: u.data
|
|
4249
|
+
})))).map(entityToRow);
|
|
4250
|
+
},
|
|
3669
4251
|
delete(id) {
|
|
3670
4252
|
return snap.delete(id);
|
|
3671
4253
|
},
|
|
4254
|
+
async deleteMany(ids) {
|
|
4255
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4256
|
+
if (ids.length === 0) return;
|
|
4257
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
4258
|
+
await snap.deleteMany(ids);
|
|
4259
|
+
},
|
|
3672
4260
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
3673
4261
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
3674
4262
|
data: res.data.map(entityToRow),
|
|
@@ -3684,6 +4272,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3684
4272
|
limit: (count) => new SdkQueryBuilder(client).limit(count),
|
|
3685
4273
|
offset: (count) => new SdkQueryBuilder(client).offset(count),
|
|
3686
4274
|
search: (searchString) => new SdkQueryBuilder(client).search(searchString),
|
|
4275
|
+
vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
|
|
3687
4276
|
include: (...relations) => new SdkQueryBuilder(client).include(...relations)
|
|
3688
4277
|
};
|
|
3689
4278
|
return client;
|
|
@@ -3772,6 +4361,6 @@ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
|
|
|
3772
4361
|
} });
|
|
3773
4362
|
}
|
|
3774
4363
|
//#endregion
|
|
3775
|
-
export {
|
|
4364
|
+
export { RebaseApiError as A, resolveClientListLimit as C, EntityRelation as D, EntityReference as E, GeoPoint as O, MAX_LIST_LIMIT as S, isAnonymousUid as T, resolveCollectionRelations as _, deserializeLogicalCondition as a, toSnakeCase as b, collectAllPages as c, CollectionRegistry as d, createDataSourceRegistry as f, findRelation as g, fieldKeyForColumn as h, deserializeFilter as i, RebaseClientError as j, Vector as k, paginateFind as l, getEffectiveSecurityRules as m, buildSdkData as n, serializeFilter as o, resolveDataSource as p, UnknownFilterOperatorError as r, serializeLogicalCondition as s, buildRoutedRebaseData as t, resolveFindWindow as u, buildCompositeId as v, ANONYMOUS_USER_ID as w, ListLimitError as x, resolvePrimaryKeys as y };
|
|
3776
4365
|
|
|
3777
|
-
//# sourceMappingURL=src-
|
|
4366
|
+
//# sourceMappingURL=src-C7rkDGxA.js.map
|