@rebasepro/server 0.13.1-canary.gef9608c → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/{admin_block-EmIHae6X.js → admin_block-H86dCPsj.js} +61 -2
  2. package/dist/admin_block-H86dCPsj.js.map +1 -0
  3. package/dist/api/ast-schema-editor.d.ts +74 -6
  4. package/dist/api/errors.d.ts +12 -0
  5. package/dist/api/rest/api-generator.d.ts +26 -1
  6. package/dist/api/rest/idempotency.d.ts +25 -4
  7. package/dist/api/rest/query-parser.d.ts +15 -1
  8. package/dist/api/rest/write-validation.d.ts +40 -4
  9. package/dist/auth/adapter-middleware.d.ts +3 -1
  10. package/dist/auth/admin-roles.d.ts +24 -0
  11. package/dist/auth/apple-oauth.d.ts +16 -11
  12. package/dist/auth/bitbucket-oauth.d.ts +2 -4
  13. package/dist/auth/builtin-auth-adapter.d.ts +11 -0
  14. package/dist/auth/discord-oauth.d.ts +5 -6
  15. package/dist/auth/facebook-oauth.d.ts +2 -4
  16. package/dist/auth/github-oauth.d.ts +2 -4
  17. package/dist/auth/gitlab-oauth.d.ts +6 -4
  18. package/dist/auth/google-oauth.d.ts +1 -0
  19. package/dist/auth/index.d.ts +7 -0
  20. package/dist/auth/interfaces.d.ts +81 -1
  21. package/dist/auth/jwt.d.ts +40 -0
  22. package/dist/auth/linkedin-oauth.d.ts +2 -4
  23. package/dist/auth/mfa-crypto.d.ts +54 -3
  24. package/dist/auth/mfa-gate.d.ts +42 -0
  25. package/dist/auth/mfa-routes.d.ts +26 -1
  26. package/dist/auth/mfa.d.ts +16 -0
  27. package/dist/auth/microsoft-oauth.d.ts +19 -4
  28. package/dist/auth/middleware.d.ts +26 -0
  29. package/dist/auth/oauth-code-flow.d.ts +66 -0
  30. package/dist/auth/oauth-signin-policy.d.ts +61 -0
  31. package/dist/auth/oidc-id-token.d.ts +61 -0
  32. package/dist/auth/rate-limiter.d.ts +62 -5
  33. package/dist/auth/registration-policy.d.ts +31 -0
  34. package/dist/auth/rls-scope.d.ts +25 -0
  35. package/dist/auth/routes.d.ts +15 -0
  36. package/dist/auth/slack-oauth.d.ts +2 -4
  37. package/dist/auth/spotify-oauth.d.ts +2 -4
  38. package/dist/auth/token-revocation.d.ts +38 -0
  39. package/dist/auth/twitter-oauth.d.ts +2 -5
  40. package/dist/{auth-DU4ShERo.js → auth-CYoPVf-E.js} +1985 -395
  41. package/dist/auth-CYoPVf-E.js.map +1 -0
  42. package/dist/{backup-BJiY_mM-.js → backup-C6ljYVTp.js} +2 -2
  43. package/dist/{backup-BJiY_mM-.js.map → backup-C6ljYVTp.js.map} +1 -1
  44. package/dist/boot/ddl-bootstrap.d.ts +70 -0
  45. package/dist/{contract-routes-Dj8i5AiM.js → contract-routes-Bet-eCNJ.js} +3 -3
  46. package/dist/{contract-routes-Dj8i5AiM.js.map → contract-routes-Bet-eCNJ.js.map} +1 -1
  47. package/dist/cron/cron-loader.d.ts +26 -1
  48. package/dist/cron/cron-routes.d.ts +1 -1
  49. package/dist/cron/cron-scheduler.d.ts +13 -3
  50. package/dist/cron/define-cron.d.ts +17 -3
  51. package/dist/cron/index.d.ts +2 -2
  52. package/dist/{cron-loader-B1S2MCSl.js → cron-loader-YhhQeVBM.js} +43 -8
  53. package/dist/cron-loader-YhhQeVBM.js.map +1 -0
  54. package/dist/{cron-routes-D5a9v1HY.js → cron-routes-maM_RlUu.js} +10 -4
  55. package/dist/cron-routes-maM_RlUu.js.map +1 -0
  56. package/dist/{cron-scheduler-BhFZWR0T.js → cron-scheduler-DIpYBmZP.js} +20 -8
  57. package/dist/cron-scheduler-DIpYBmZP.js.map +1 -0
  58. package/dist/{cron-store-Cu4sOseb.js → cron-store-Dvr4Y1sZ.js} +60 -51
  59. package/dist/cron-store-Dvr4Y1sZ.js.map +1 -0
  60. package/dist/ddl-bootstrap-BhXbTnBl.js +183 -0
  61. package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -0
  62. package/dist/email/html.d.ts +54 -0
  63. package/dist/email/index.d.ts +3 -0
  64. package/dist/email/link-base.d.ts +39 -0
  65. package/dist/email/smtp-email-service.d.ts +7 -1
  66. package/dist/email/templates.d.ts +9 -1
  67. package/dist/email/types.d.ts +11 -1
  68. package/dist/env.d.ts +5 -0
  69. package/dist/{errors-BpudWAVU.js → errors-EBYiaJ2E.js} +14 -2
  70. package/dist/errors-EBYiaJ2E.js.map +1 -0
  71. package/dist/function-loader-DDS1v7YX.js +139 -0
  72. package/dist/function-loader-DDS1v7YX.js.map +1 -0
  73. package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
  74. package/dist/function-routes-Btcez1T-.js.map +1 -0
  75. package/dist/functions/define-function.d.ts +25 -6
  76. package/dist/functions/function-loader.d.ts +23 -0
  77. package/dist/functions/function-routes.d.ts +7 -1
  78. package/dist/functions/request-timeout.d.ts +34 -0
  79. package/dist/history/history-routes.d.ts +6 -0
  80. package/dist/index.d.ts +3 -2
  81. package/dist/index.es.js +1893 -714
  82. package/dist/index.es.js.map +1 -1
  83. package/dist/init/docs.d.ts +10 -1
  84. package/dist/init/process-safety.d.ts +25 -0
  85. package/dist/init.d.ts +38 -0
  86. package/dist/{jwt-CzeviDcB.js → jwt-_IFqfTOg.js} +57 -9
  87. package/dist/jwt-_IFqfTOg.js.map +1 -0
  88. package/dist/logger-DfvF_8r-.js +190 -0
  89. package/dist/logger-DfvF_8r-.js.map +1 -0
  90. package/dist/{openapi-generator-D5xsfVJY.js → openapi-generator-DPKtUC9X.js} +326 -55
  91. package/dist/openapi-generator-DPKtUC9X.js.map +1 -0
  92. package/dist/request-timeout-RivJsME0.js +65 -0
  93. package/dist/request-timeout-RivJsME0.js.map +1 -0
  94. package/dist/schema-editor-routes-CRcS3ArS.js +437 -0
  95. package/dist/schema-editor-routes-CRcS3ArS.js.map +1 -0
  96. package/dist/serve-spa.d.ts +5 -4
  97. package/dist/services/outbound-url-guard.d.ts +53 -0
  98. package/dist/services/routed-realtime-service.d.ts +10 -2
  99. package/dist/services/webhook-service.d.ts +76 -1
  100. package/dist/singleton.d.ts +25 -8
  101. package/dist/{src-CKOZBpeJ.js → src-C7rkDGxA.js} +619 -70
  102. package/dist/src-C7rkDGxA.js.map +1 -0
  103. package/dist/{src-_qQ3RNCK.js → src-Cz9nMgUR.js} +93 -3
  104. package/dist/src-Cz9nMgUR.js.map +1 -0
  105. package/dist/storage/LocalStorageController.d.ts +12 -1
  106. package/dist/storage/image-transform.d.ts +57 -0
  107. package/dist/storage/keys.d.ts +98 -0
  108. package/dist/storage/routes.d.ts +11 -0
  109. package/dist/storage/tus-handler.d.ts +22 -2
  110. package/dist/utils/logger.d.ts +18 -0
  111. package/dist/utils/sql.d.ts +11 -6
  112. package/package.json +6 -5
  113. package/dist/admin_block-EmIHae6X.js.map +0 -1
  114. package/dist/auth-DU4ShERo.js.map +0 -1
  115. package/dist/backend-CIxN4FVm.js +0 -15
  116. package/dist/backend-CIxN4FVm.js.map +0 -1
  117. package/dist/cron-loader-B1S2MCSl.js.map +0 -1
  118. package/dist/cron-routes-D5a9v1HY.js.map +0 -1
  119. package/dist/cron-scheduler-BhFZWR0T.js.map +0 -1
  120. package/dist/cron-store-Cu4sOseb.js.map +0 -1
  121. package/dist/errors-BpudWAVU.js.map +0 -1
  122. package/dist/function-loader-B_1fYfUY.js +0 -86
  123. package/dist/function-loader-B_1fYfUY.js.map +0 -1
  124. package/dist/function-routes-C0cLIy3N.js.map +0 -1
  125. package/dist/jwt-CzeviDcB.js.map +0 -1
  126. package/dist/logger-BYU66ENZ.js +0 -94
  127. package/dist/logger-BYU66ENZ.js.map +0 -1
  128. package/dist/openapi-generator-D5xsfVJY.js.map +0 -1
  129. package/dist/schema-editor-routes-C1DxDnDR.js +0 -248
  130. package/dist/schema-editor-routes-C1DxDnDR.js.map +0 -1
  131. package/dist/src-CKOZBpeJ.js.map +0 -1
  132. 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 { c as NULL_OPS, i as isRelationalCollectionConfig, l as REST_TO_CANONICAL, n as getDeclaredSubcollections, o as getDataSourceCapabilities, r as isPostgresCollectionConfig, s as CANONICAL_TO_REST, u as toCanonicalOp } from "./src-_qQ3RNCK.js";
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 `auth.uid()`.
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 **`auth.uid() IS NOT NULL` is a
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
- * `auth.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
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,24 +292,51 @@ var policy = {
235
292
  authUid: () => ({ kind: "authUid" }),
236
293
  authRoles: () => ({ kind: "authRoles" })
237
294
  };
238
- /** Hard ceiling clamped onto any client-supplied `limit`, on every surface. */
295
+ /** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
239
296
  var MAX_LIST_LIMIT = 1e3;
240
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
+ /**
241
318
  * Resolve a client-supplied list `limit` into a safe, always-defined value.
242
319
  *
243
- * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
244
- * so `0`, negatives, and absurd values can never bypass the cap.
245
- * - An absent / blank / non-numeric limit falls back to the mode default:
320
+ * - An absent / blank limit falls back to the mode default:
246
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.
247
327
  *
248
328
  * The return is never `undefined` — no ingress that routes its client limit
249
329
  * through this can produce an unbounded read.
330
+ *
331
+ * @throws {ListLimitError} when a present `limit` is not an integer in range.
250
332
  */
251
333
  function resolveClientListLimit(rawLimit, opts = {}) {
252
334
  const maxLimit = opts.maxLimit ?? 1e3;
253
335
  if (rawLimit != null && String(rawLimit).trim() !== "") {
254
- const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
255
- if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
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;
256
340
  }
257
341
  return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
258
342
  }
@@ -1392,6 +1476,47 @@ function singularizeForKey(name) {
1392
1476
  const result = singular(name);
1393
1477
  return result.length > 0 ? result : name;
1394
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
+ }
1395
1520
  //#endregion
1396
1521
  //#region ../common/src/util/entities.ts
1397
1522
  /**
@@ -1403,10 +1528,21 @@ function singularizeForKey(name) {
1403
1528
  * have `id` and `path` fields — these are relation-shaped objects from
1404
1529
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1405
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
+ *
1406
1538
  * Returns null if the value cannot be coerced.
1407
1539
  */
1408
- function normalizeToEntityRelation(value, propertyType) {
1540
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1409
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
+ }
1410
1546
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1411
1547
  const obj = value;
1412
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;
@@ -1678,6 +1814,48 @@ function getTableName(collection) {
1678
1814
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1679
1815
  }
1680
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
+ /**
1681
1859
  * Look up a relation by key with forgiving normalization.
1682
1860
  *
1683
1861
  * `resolveCollectionRelations` stores each relation under a single canonical
@@ -1770,6 +1948,37 @@ function getSubcollections(collection) {
1770
1948
  return getEntityChildViews(collection).map((view) => view.collection);
1771
1949
  }
1772
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
1773
1982
  //#region ../common/src/util/auth-default-policies.ts
1774
1983
  /**
1775
1984
  * Default RLS policies injected by the schema generator.
@@ -1777,8 +1986,14 @@ function getSubcollections(collection) {
1777
1986
  * Rebase's enforcement model is unified: authenticated (user-context) requests
1778
1987
  * run under the restricted `rebase_user` role, so Postgres RLS binds *every*
1779
1988
  * statement — reads and writes. A collection's `securityRules` are the whole
1780
- * authorization model. The server context (auth flows, migrations,
1781
- * `dataAsAdmin`) runs as the owner and bypasses RLS.
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.
1782
1997
  *
1783
1998
  * Because RLS default-denies, every collection is **locked by default**: with
1784
1999
  * no rules, only the server context and admins can touch it. The generator
@@ -1836,11 +2051,28 @@ function getIdPropertyName(collection) {
1836
2051
  *
1837
2052
  * Collections that opt out via `disableDefaultPolicies` are returned unchanged.
1838
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
+ }
1839
2071
  function getEffectiveSecurityRules(collection) {
1840
2072
  const explicit = [...collection.securityRules ?? []];
1841
- if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return explicit;
1842
2073
  const tableName = getTableName(collection);
1843
2074
  const injected = [];
2075
+ if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return isAuthCollection(collection) ? [...explicit, adminWriteGate(tableName)] : explicit;
1844
2076
  injected.push({
1845
2077
  name: `${tableName}_default_admin_read`,
1846
2078
  operations: ["select"],
@@ -1858,13 +2090,7 @@ function getEffectiveSecurityRules(collection) {
1858
2090
  operations: ["select"],
1859
2091
  condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
1860
2092
  });
1861
- injected.push({
1862
- name: `${tableName}_require_admin_write`,
1863
- mode: "restrictive",
1864
- operations: [...DEFAULT_GUARDED_OPS],
1865
- condition: SERVER_OR_ADMIN_EXPR$1,
1866
- check: SERVER_OR_ADMIN_EXPR$1
1867
- });
2093
+ injected.push(adminWriteGate(tableName));
1868
2094
  }
1869
2095
  return [...explicit, ...injected];
1870
2096
  }
@@ -2904,8 +3130,24 @@ var QueryBuilder = class {
2904
3130
  /**
2905
3131
  * Set a free-text search string if supported by the backend.
2906
3132
  */
2907
- search(searchString) {
3133
+ search(searchString, options) {
2908
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
+ };
2909
3151
  return this;
2910
3152
  }
2911
3153
  /**
@@ -3105,8 +3347,10 @@ async function collectAllPages(find, params, label = "collection") {
3105
3347
  * metadata, so type coercion is the responsibility of the server-side data
3106
3348
  * driver which has access to the collection schema.
3107
3349
  *
3108
- * Commas inside list values are backslash-escaped (`\,`), and literal
3109
- * backslashes are escaped as `\\`.
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.
3110
3354
  *
3111
3355
  * @module
3112
3356
  */
@@ -3124,22 +3368,55 @@ function stringifyValue(value) {
3124
3368
  return String(value);
3125
3369
  }
3126
3370
  /**
3127
- * Escape a single list item for the wire format.
3128
- * `\` → `\\`, `,` → `\,`
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.
3129
3378
  */
3130
- function escapeListItem(value) {
3131
- return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
3379
+ var WIRE_SPECIALS = /[\\,()]/g;
3380
+ /**
3381
+ * Escape a value for the wire format: `\` → `\\`, `,` → `\,`, `(` → `\(`,
3382
+ * `)` → `\)`.
3383
+ */
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}`);
3132
3393
  }
3133
3394
  /**
3134
- * Unescape a single list item from the wire format.
3135
- * `\\` → `\`, `\,` → `,`
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.
3136
3408
  */
3137
- function unescapeListItem(value) {
3409
+ function unescapeWireValue(value) {
3138
3410
  let result = "";
3139
- for (let i = 0; i < value.length; i++) if (value[i] === "\\" && i + 1 < value.length) {
3140
- result += value[i + 1];
3141
- i++;
3142
- } else result += value[i];
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
+ }
3143
3420
  return result;
3144
3421
  }
3145
3422
  /**
@@ -3157,14 +3434,235 @@ function splitListItems(inner) {
3157
3434
  current += inner[i] + inner[i + 1];
3158
3435
  i++;
3159
3436
  } else if (inner[i] === ",") {
3160
- items.push(unescapeListItem(current));
3437
+ items.push(unescapeWireValue(current));
3161
3438
  current = "";
3162
3439
  } else current += inner[i];
3163
- items.push(unescapeListItem(current));
3440
+ items.push(unescapeWireValue(current));
3164
3441
  return items;
3165
3442
  }
3166
- var REST_OP_LOOKUP = REST_TO_CANONICAL;
3167
- var CANONICAL_OP_LOOKUP = CANONICAL_TO_REST;
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
+ }
3168
3666
  /**
3169
3667
  * Serialize a single canonical condition tuple to a PostgREST dot-string.
3170
3668
  *
@@ -3179,9 +3677,13 @@ function serializeTuple(tuple) {
3179
3677
  if (!Array.isArray(tuple) || tuple.length !== 2) throw new TypeError(`serializeTuple: expected a [WhereFilterOp, value] tuple, got ${JSON.stringify(tuple)}`);
3180
3678
  const [op, value] = tuple;
3181
3679
  if (typeof op !== "string") throw new TypeError(`serializeTuple: operator must be a string, got ${typeof op}`);
3182
- const restOp = CANONICAL_OP_LOOKUP[op];
3680
+ const restOp = CANONICAL_OP_LOOKUP.get(op);
3183
3681
  if (!restOp) throw new TypeError(`serializeTuple: unknown operator "${op}". Valid operators: ${Object.keys(CANONICAL_TO_REST).join(", ")}`);
3184
- if (Array.isArray(value)) return `${restOp}.(${value.map((v) => escapeListItem(stringifyValue(v))).join(",")})`;
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
+ }
3185
3687
  return `${restOp}.${stringifyValue(value)}`;
3186
3688
  }
3187
3689
  /**
@@ -3233,10 +3735,13 @@ function deserializeSingle(raw) {
3233
3735
  if (dotIndex === -1) return ["==", raw];
3234
3736
  const prefix = raw.substring(0, dotIndex);
3235
3737
  const rest = raw.substring(dotIndex + 1);
3236
- const canonicalOp = REST_OP_LOOKUP[prefix];
3738
+ const canonicalOp = REST_OP_LOOKUP.get(prefix);
3237
3739
  if (!canonicalOp) return ["==", raw];
3238
3740
  if (NULL_OPS.has(canonicalOp)) return [canonicalOp, null];
3239
- if (rest.startsWith("(") && rest.endsWith(")")) return [canonicalOp, splitListItems(rest.slice(1, -1))];
3741
+ if (rest.startsWith("(") && rest.endsWith(")")) {
3742
+ const inner = rest.slice(1, -1);
3743
+ return [canonicalOp, inner === EMPTY_LIST_TOKEN ? [] : splitListItems(inner)];
3744
+ }
3240
3745
  return [canonicalOp, rest];
3241
3746
  }
3242
3747
  /**
@@ -3251,20 +3756,27 @@ function deserializeSingle(raw) {
3251
3756
  *
3252
3757
  * deserializeFilter({ age: ["gte.18", "lt.65"] })
3253
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.
3254
3762
  */
3255
3763
  function deserializeFilter(query) {
3256
3764
  const result = {};
3257
3765
  for (const [field, raw] of Object.entries(query)) {
3258
3766
  if (raw === void 0) continue;
3259
- if (Array.isArray(raw) && raw.length === 2 && typeof raw[0] === "string" && toCanonicalOp(raw[0]) === raw[0]) {
3260
- result[field] = raw;
3767
+ const tuple = readTuple(field, raw);
3768
+ if (tuple) {
3769
+ result[field] = tuple;
3261
3770
  continue;
3262
3771
  }
3263
3772
  if (Array.isArray(raw)) {
3264
3773
  if (raw.length === 0) continue;
3265
- if (Array.isArray(raw[0]) && raw[0].length === 2 && typeof raw[0][0] === "string" && toCanonicalOp(raw[0][0]) === raw[0][0]) {
3266
- result[field] = raw;
3267
- continue;
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
+ }
3268
3780
  }
3269
3781
  if (raw.length === 1) result[field] = typeof raw[0] === "string" ? deserializeSingle(raw[0]) : ["==", raw[0]];
3270
3782
  else if (typeof raw[0] === "string" && raw[0].includes(".")) result[field] = raw.map((r) => typeof r === "string" ? deserializeSingle(r) : ["==", r]);
@@ -3289,12 +3801,12 @@ function serializeLogicalCondition(cond) {
3289
3801
  const inner = (cond.conditions ?? []).map(serializeLogicalCondition).join(",");
3290
3802
  return `${cond.type}(${inner})`;
3291
3803
  }
3292
- const restOp = CANONICAL_OP_LOOKUP[cond.operator] ?? "eq";
3804
+ const restOp = CANONICAL_OP_LOOKUP.get(cond.operator) ?? "eq";
3293
3805
  if (Array.isArray(cond.value)) {
3294
- const items = cond.value.map((v) => escapeListItem(stringifyValue(v))).join(",");
3806
+ const items = cond.value.map((v) => escapeWireValue(stringifyValue(v))).join(",");
3295
3807
  return `${cond.column}.${restOp}.(${items})`;
3296
3808
  }
3297
- return `${cond.column}.${restOp}.${stringifyValue(cond.value)}`;
3809
+ return `${cond.column}.${restOp}.${escapeWireValue(stringifyValue(cond.value))}`;
3298
3810
  }
3299
3811
  function deserializeLogicalCondition(str, nesting = 0) {
3300
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)\`.`);
@@ -3302,19 +3814,9 @@ function deserializeLogicalCondition(str, nesting = 0) {
3302
3814
  if (logicalMatch) {
3303
3815
  const type = logicalMatch[1];
3304
3816
  const innerStr = logicalMatch[2];
3305
- const conditions = [];
3306
- let depth = 0;
3307
- let start = 0;
3308
- for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
3309
- else if (innerStr[i] === ")") depth--;
3310
- else if (innerStr[i] === "," && depth === 0) {
3311
- conditions.push(deserializeLogicalCondition(innerStr.slice(start, i), nesting + 1));
3312
- start = i + 1;
3313
- }
3314
- conditions.push(deserializeLogicalCondition(innerStr.slice(start), nesting + 1));
3315
3817
  return {
3316
3818
  type,
3317
- conditions
3819
+ conditions: splitGroupItems(innerStr).map((part) => deserializeLogicalCondition(part, nesting + 1))
3318
3820
  };
3319
3821
  }
3320
3822
  const firstDot = str.indexOf(".");
@@ -3329,7 +3831,7 @@ function deserializeLogicalCondition(str, nesting = 0) {
3329
3831
  if (secondDot === -1) return {
3330
3832
  column,
3331
3833
  operator: "==",
3332
- value: rest
3834
+ value: unescapeWireValue(rest)
3333
3835
  };
3334
3836
  const opStr = rest.substring(0, secondDot);
3335
3837
  const valueStr = rest.substring(secondDot + 1);
@@ -3342,7 +3844,7 @@ function deserializeLogicalCondition(str, nesting = 0) {
3342
3844
  return {
3343
3845
  column,
3344
3846
  operator,
3345
- value: valueStr
3847
+ value: unescapeWireValue(valueStr)
3346
3848
  };
3347
3849
  }
3348
3850
  //#endregion
@@ -3379,10 +3881,12 @@ function createPrimaryKeyResolver(options) {
3379
3881
  * than postgres still serve rows with one, and this keeps them working.
3380
3882
  */
3381
3883
  function rowToEntity(row, slug, primaryKeys = []) {
3884
+ const { _matches, ...values } = row;
3382
3885
  return {
3383
3886
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
3384
3887
  path: slug,
3385
- values: row
3888
+ values,
3889
+ ..._matches ? { searchMatches: _matches } : {}
3386
3890
  };
3387
3891
  }
3388
3892
  /**
@@ -3504,6 +4008,21 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3504
4008
  values: {}
3505
4009
  } });
3506
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,
3507
4026
  count: driver.count ? async (params) => {
3508
4027
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3509
4028
  return driver.count({
@@ -3525,6 +4044,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3525
4044
  orderBy: params?.orderBy?.[0],
3526
4045
  order: params?.orderBy?.[1],
3527
4046
  searchString: params?.searchString,
4047
+ searchExplain: params?.searchExplain,
3528
4048
  onUpdate: (entities) => {
3529
4049
  onUpdate({
3530
4050
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
@@ -3562,8 +4082,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3562
4082
  offset(count) {
3563
4083
  return new QueryBuilder(accessor).offset(count);
3564
4084
  },
3565
- search(searchString) {
3566
- 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);
3567
4090
  },
3568
4091
  include(...relations) {
3569
4092
  return new QueryBuilder(accessor).include(...relations);
@@ -3651,8 +4174,18 @@ var SdkQueryBuilder = class {
3651
4174
  this.params.offset = count;
3652
4175
  return this;
3653
4176
  }
3654
- search(searchString) {
4177
+ search(searchString, options) {
3655
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
+ };
3656
4189
  return this;
3657
4190
  }
3658
4191
  include(...relations) {
@@ -3706,9 +4239,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
3706
4239
  async update(id, data) {
3707
4240
  return entityToRow(await snap.update(id, data));
3708
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
+ },
3709
4251
  delete(id) {
3710
4252
  return snap.delete(id);
3711
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
+ },
3712
4260
  count: snap.count ? (params) => snap.count(params) : void 0,
3713
4261
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
3714
4262
  data: res.data.map(entityToRow),
@@ -3724,6 +4272,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
3724
4272
  limit: (count) => new SdkQueryBuilder(client).limit(count),
3725
4273
  offset: (count) => new SdkQueryBuilder(client).offset(count),
3726
4274
  search: (searchString) => new SdkQueryBuilder(client).search(searchString),
4275
+ vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
3727
4276
  include: (...relations) => new SdkQueryBuilder(client).include(...relations)
3728
4277
  };
3729
4278
  return client;
@@ -3812,6 +4361,6 @@ function buildRoutedRebaseData({ defaultData, sources, resolveKey }) {
3812
4361
  } });
3813
4362
  }
3814
4363
  //#endregion
3815
- export { EntityReference as C, Vector as E, isAnonymousUid as S, GeoPoint as T, resolvePrimaryKeys as _, serializeFilter as a, resolveClientListLimit as b, paginateFind as c, createDataSourceRegistry as d, resolveDataSource as f, buildCompositeId as g, resolveCollectionRelations as h, deserializeLogicalCondition as i, resolveFindWindow as l, findRelation as m, buildSdkData as n, serializeLogicalCondition as o, getEffectiveSecurityRules as p, deserializeFilter as r, collectAllPages as s, buildRoutedRebaseData as t, CollectionRegistry as u, toSnakeCase as v, EntityRelation as w, ANONYMOUS_USER_ID as x, MAX_LIST_LIMIT as y };
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 };
3816
4365
 
3817
- //# sourceMappingURL=src-CKOZBpeJ.js.map
4366
+ //# sourceMappingURL=src-C7rkDGxA.js.map