@rebasepro/types 0.13.0 → 0.13.1-canary.g06dbe5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { RebaseApiError } from "../errors";
1
2
  import type { EntityStatus, EntityValues } from "../types/entities";
2
3
  import type { CollectionConfig, FilterValues } from "../types/collections";
3
4
  import type { RebaseCallContext } from "../call_context";
@@ -37,7 +38,7 @@ export interface VectorSearchParams {
37
38
  export declare const DEFAULT_LIST_LIMIT = 50;
38
39
  /** Rows returned for a vector-search list read when the client sends no `limit`. */
39
40
  export declare const DEFAULT_VECTOR_LIST_LIMIT = 10;
40
- /** Hard ceiling clamped onto any client-supplied `limit`, on every surface. */
41
+ /** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
41
42
  export declare const MAX_LIST_LIMIT = 1000;
42
43
  /** Overridable bounds for {@link resolveClientListLimit}. */
43
44
  export interface ListLimitBounds {
@@ -45,19 +46,36 @@ export interface ListLimitBounds {
45
46
  defaultLimit?: number;
46
47
  /** Default page size for vector-search reads. */
47
48
  vectorDefaultLimit?: number;
48
- /** Upper bound clamped onto any client-supplied limit. */
49
+ /** Largest limit a client may ask for. A larger one is rejected, not clamped. */
49
50
  maxLimit?: number;
50
51
  }
52
+ /**
53
+ * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
54
+ * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
55
+ * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
56
+ *
57
+ * @group Errors
58
+ */
59
+ export declare class ListLimitError extends RebaseApiError {
60
+ /** The ceiling that was exceeded — what the caller should page by instead. */
61
+ readonly maxLimit: number;
62
+ constructor(message: string, maxLimit: number);
63
+ }
51
64
  /**
52
65
  * Resolve a client-supplied list `limit` into a safe, always-defined value.
53
66
  *
54
- * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
55
- * so `0`, negatives, and absurd values can never bypass the cap.
56
- * - An absent / blank / non-numeric limit falls back to the mode default:
67
+ * - An absent / blank limit falls back to the mode default:
57
68
  * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
69
+ * - A limit that is present must be an integer in `[1, maxLimit]`. Anything
70
+ * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
71
+ * {@link ListLimitError} rather than being coerced into range, because every
72
+ * coercion answers a question the caller did not ask with a page it cannot
73
+ * tell apart from the whole collection.
58
74
  *
59
75
  * The return is never `undefined` — no ingress that routes its client limit
60
76
  * through this can produce an unbounded read.
77
+ *
78
+ * @throws {ListLimitError} when a present `limit` is not an integer in range.
61
79
  */
62
80
  export declare function resolveClientListLimit(rawLimit: number | string | null | undefined, opts?: ListLimitBounds & {
63
81
  vectorSearch?: boolean;
@@ -82,6 +100,8 @@ export interface FetchCollectionProps<M extends Record<string, unknown> = Record
82
100
  startAfter?: unknown;
83
101
  orderBy?: string;
84
102
  searchString?: string;
103
+ /** Ask each row which declared search field matched — populates `_matches`. */
104
+ searchExplain?: boolean;
85
105
  order?: "desc" | "asc";
86
106
  /** Vector similarity search configuration */
87
107
  vectorSearch?: VectorSearchParams;
@@ -128,6 +148,25 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
128
148
  /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */
129
149
  upsert?: boolean;
130
150
  }
151
+ /**
152
+ * @internal
153
+ */
154
+ export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
155
+ path: string;
156
+ /**
157
+ * The rows to update, each named by its address.
158
+ *
159
+ * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
160
+ * values and is insert-shaped — `saveMany` passes `status: "new"` and no
161
+ * `id`, so it cannot express "update exactly this row". This can, and it is
162
+ * why bulk update is a separate driver method rather than a flag on that one.
163
+ */
164
+ updates: {
165
+ id: string | number;
166
+ values: Partial<EntityValues<M>>;
167
+ }[];
168
+ collection?: CollectionConfig<M>;
169
+ }
131
170
  /**
132
171
  * @internal
133
172
  */
@@ -139,6 +178,14 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
139
178
  };
140
179
  collection?: CollectionConfig<M>;
141
180
  }
181
+ /**
182
+ * @internal
183
+ */
184
+ export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
185
+ path: string;
186
+ ids: (string | number)[];
187
+ collection?: CollectionConfig<M>;
188
+ }
142
189
  export type FilterCombinationValidProps = {
143
190
  path: string;
144
191
  databaseId?: string;
@@ -210,6 +257,16 @@ export interface DataDriver {
210
257
  * back to `save` per row.
211
258
  */
212
259
  saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
260
+ /**
261
+ * Update many rows in one transaction, each addressed by id.
262
+ *
263
+ * Optional for the same reason `saveMany` is: a driver that cannot make the
264
+ * batch atomic should not pretend to. The REST layer reports
265
+ * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
266
+ * writes, which would be neither atomic nor one round trip — the two things
267
+ * a caller reaches for a batch to get.
268
+ */
269
+ updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
213
270
  /**
214
271
  * Delete a entity
215
272
  * @param props
@@ -221,6 +278,13 @@ export interface DataDriver {
221
278
  * @param path Collection path
222
279
  */
223
280
  deleteAll?(path: string): Promise<void>;
281
+ /**
282
+ * Delete many rows in one transaction, addressed by id.
283
+ *
284
+ * Ids rather than a filter, deliberately — see
285
+ * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
286
+ */
287
+ deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
224
288
  /**
225
289
  * Check if the given property is unique in the given collection
226
290
  * @param path Collection path
@@ -303,6 +367,8 @@ export interface RestFetchService {
303
367
  offset?: number;
304
368
  startAfter?: Record<string, unknown>;
305
369
  searchString?: string;
370
+ /** Ask each row which declared search fields matched — populates `_matches`. */
371
+ searchExplain?: boolean;
306
372
  databaseId?: string;
307
373
  vectorSearch?: VectorSearchParams;
308
374
  }, include?: string[]): Promise<Record<string, unknown>[]>;
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,29 @@
1
+ /**
2
+ * The error codes every route can produce, as `RebaseApiError.code`.
3
+ *
4
+ * These are the defaults on `ApiError`'s static constructors server-side, so
5
+ * any endpoint can answer with one. They are **not** the complete set: routes
6
+ * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,
7
+ * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.
8
+ *
9
+ * Hence the union is deliberately open rather than closed. It exists to give
10
+ * autocomplete and to catch a typo in the common cases — `code` was a bare
11
+ * `string`, so `e.code === "NOT_FOUND"` and `e.code === "NOTFOUND"` were
12
+ * equally valid and only one of them worked. Closing it would be a lie that
13
+ * broke the moment a route added a code.
14
+ *
15
+ * @example
16
+ * if (e instanceof RebaseApiError) {
17
+ * switch (e.code) {
18
+ * case "NOT_FOUND": return null; // completed
19
+ * case "FORBIDDEN": return redirect();
20
+ * default: throw e; // routes' own codes land here
21
+ * }
22
+ * }
23
+ *
24
+ * @group Errors
25
+ */
26
+ export type RebaseErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "DB_PERMISSION_DENIED" | "SCHEMA_DRIFT" | (string & {});
1
27
  /**
2
28
  * Structured initializer for {@link RebaseApiError}.
3
29
  *
@@ -10,8 +36,8 @@ export interface RebaseErrorInit {
10
36
  * logic errors that have no HTTP status.
11
37
  */
12
38
  status?: number;
13
- /** Stable, machine-readable error code (e.g. `"NOT_FOUND"`, `"BAD_REQUEST"`). */
14
- code?: string;
39
+ /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
40
+ code?: RebaseErrorCode;
15
41
  /** Structured error payload returned by the server, when present. */
16
42
  details?: unknown;
17
43
  /** The underlying error this one wraps, if any. */
@@ -44,8 +70,8 @@ export interface RebaseErrorInit {
44
70
  export declare class RebaseApiError extends Error {
45
71
  /** HTTP status code, or `undefined` for non-HTTP errors. */
46
72
  readonly status?: number;
47
- /** Stable machine-readable error code, when the server supplied one. */
48
- readonly code?: string;
73
+ /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
74
+ readonly code?: RebaseErrorCode;
49
75
  /** Structured error payload from the server, when present. */
50
76
  readonly details?: unknown;
51
77
  constructor(message: string, init?: RebaseErrorInit);
package/dist/index.es.js CHANGED
@@ -26,7 +26,7 @@
26
26
  var RebaseApiError = class extends Error {
27
27
  /** HTTP status code, or `undefined` for non-HTTP errors. */
28
28
  status;
29
- /** Stable machine-readable error code, when the server supplied one. */
29
+ /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
30
30
  code;
31
31
  /** Structured error payload from the server, when present. */
32
32
  details;
@@ -307,6 +307,7 @@ var ADMIN_COLLECTION_KEYS = [
307
307
  "defaultSize",
308
308
  "defaultViewMode",
309
309
  "disableDefaultActions",
310
+ "display",
310
311
  "enabledViews",
311
312
  "entityActions",
312
313
  "entityViews",
@@ -317,6 +318,7 @@ var ADMIN_COLLECTION_KEYS = [
317
318
  "formAutoSave",
318
319
  "formView",
319
320
  "group",
321
+ "hideFromEntityViews",
320
322
  "hideFromNavigation",
321
323
  "hideIdFromCollection",
322
324
  "hideIdFromForm",
@@ -381,6 +383,64 @@ var ADMIN_PROPERTY_KEYS = [
381
383
  "urlPreview",
382
384
  "widget"
383
385
  ];
386
+ /**
387
+ * Move flattened admin keys back down into the `admin` block.
388
+ *
389
+ * The admin panel works with a *flat* view model — the block merged onto the
390
+ * collection — so what comes back from a form has `icon` and `defaultViewMode`
391
+ * at the top level while `admin` still holds whatever the file was loaded with.
392
+ * This is the way back.
393
+ *
394
+ * **The top-level value wins.** It is the one the form just wrote; the block is
395
+ * the copy the collection was loaded with, and preferring it resolves every edit
396
+ * in favour of the value the user changed away from.
397
+ *
398
+ * This lives here, next to the key lists, because it had two implementations —
399
+ * `toAdminCollectionConfig` in `@rebasepro/admin-types` and `nestAdminKeys` in
400
+ * `@rebasepro/server`'s schema editor — that agreed on everything except that
401
+ * precedence, which is the only part that decides whether a save is visible.
402
+ *
403
+ * @group Models
404
+ */
405
+ function nestAdminKeysOf(source, adminKeys) {
406
+ const keys = new Set(adminKeys);
407
+ const top = {};
408
+ const block = { ...source.admin ?? {} };
409
+ for (const [key, value] of Object.entries(source)) {
410
+ if (key === "admin") continue;
411
+ if (keys.has(key)) block[key] = value;
412
+ else top[key] = value;
413
+ }
414
+ if (Object.keys(block).length > 0) top.admin = block;
415
+ return top;
416
+ }
417
+ /**
418
+ * {@link nestAdminKeysOf} for a collection.
419
+ *
420
+ * @group Models
421
+ */
422
+ function nestAdminCollectionKeys(collection) {
423
+ return nestAdminKeysOf(collection, ADMIN_COLLECTION_KEYS);
424
+ }
425
+ /**
426
+ * {@link nestAdminKeysOf} for a property, applied to its children too.
427
+ *
428
+ * A map property carries `properties`, an array property carries `of`, and both
429
+ * hold properties with `admin` blocks of their own. A flat `readOnly` left on a
430
+ * child is as dead — and as fatal at the next boot — as one left on the parent,
431
+ * so the walk goes all the way down.
432
+ *
433
+ * @group Models
434
+ */
435
+ function nestAdminPropertyKeys(property) {
436
+ const nested = nestAdminKeysOf(property, ADMIN_PROPERTY_KEYS);
437
+ const children = nested.properties;
438
+ if (children && typeof children === "object" && !Array.isArray(children)) nested.properties = Object.fromEntries(Object.entries(children).map(([key, child]) => [key, child && typeof child === "object" && !Array.isArray(child) ? nestAdminPropertyKeys(child) : child]));
439
+ const of = nested.of;
440
+ if (Array.isArray(of)) nested.of = of.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? nestAdminPropertyKeys(entry) : entry);
441
+ else if (of && typeof of === "object") nested.of = nestAdminPropertyKeys(of);
442
+ return nested;
443
+ }
384
444
  //#endregion
385
445
  //#region src/types/data_source.ts
386
446
  /**
@@ -577,6 +637,22 @@ function getDeclaredSubcollections(collection) {
577
637
  return collection.subcollections;
578
638
  }
579
639
  //#endregion
640
+ //#region src/types/search.ts
641
+ /** The column name used when {@link SearchConfig.column} is not given. */
642
+ var DEFAULT_SEARCH_COLUMN = "search_vector";
643
+ /** The text search configuration used when {@link SearchConfig.language} is not given. */
644
+ var DEFAULT_SEARCH_LANGUAGE = "simple";
645
+ /** The weight a field carries when it does not name one. */
646
+ var DEFAULT_SEARCH_WEIGHT = "B";
647
+ /** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */
648
+ var DEFAULT_FUZZY_THRESHOLD = .3;
649
+ /**
650
+ * The relevance sort key. Valid only on a collection that declares a
651
+ * {@link SearchConfig} *and* on a query that carries a search string; anywhere
652
+ * else it is an unknown field and the request is refused.
653
+ */
654
+ var RELEVANCE_SORT_FIELD = "_score";
655
+ //#endregion
580
656
  //#region src/types/relations.ts
581
657
  /** @group Models */
582
658
  function hasForeignKeyOnTarget(relation) {
@@ -593,7 +669,7 @@ function isToMany(relation) {
593
669
  //#endregion
594
670
  //#region src/types/policy.ts
595
671
  /**
596
- * The id a request without a logged-in user reports as `auth.uid()`.
672
+ * The id a request without a logged-in user reports as `rebase.uid()`.
597
673
  *
598
674
  * A user-context request always sets `app.uid`: blank would read back as
599
675
  * `NULL`, and `NULL` is how the trusted server context is recognised, so an
@@ -601,7 +677,7 @@ function isToMany(relation) {
601
677
  * therefore substitutes this sentinel at the single chokepoint where the GUC
602
678
  * is set.
603
679
  *
604
- * The consequence for policy authors is that **`auth.uid() IS NOT NULL` is a
680
+ * The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
605
681
  * tautology on the user path** — it is true for anonymous visitors too. Use
606
682
  * {@link policy.authenticated} to mean "signed in", and
607
683
  * {@link policy.serverContext} to mean "the trusted server context". Do not
@@ -618,7 +694,7 @@ var ANONYMOUS_USER_ID = "anonymous";
618
694
  * JavaScript evaluator and the linter were all built on
619
695
  * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
620
696
  * callers as `'anon'` — so `policy.authenticated()`, which compiled to
621
- * `auth.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
697
+ * `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
622
698
  * sanctioned way to write "signed in" granted to everyone, and the linter
623
699
  * flagged the spelling that actually worked as a foreign convention.
624
700
  *
@@ -700,6 +776,96 @@ var policy = {
700
776
  authRoles: () => ({ kind: "authRoles" })
701
777
  };
702
778
  //#endregion
779
+ //#region src/types/rls-functions.ts
780
+ /**
781
+ * The SQL helper functions RLS policies call, and the schema they live in.
782
+ *
783
+ * ## One schema, and it is ours
784
+ *
785
+ * Rebase creates exactly one schema in a project's database: `rebase`. These
786
+ * three functions live in it alongside the framework's own tables, and that is
787
+ * the whole contract — a reader can look at a database and know precisely which
788
+ * namespace belongs to the framework and that nothing else was touched.
789
+ *
790
+ * It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
791
+ * `auth`, which is Supabase's name, chosen so that a developer who had written
792
+ * Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
793
+ * name was not Rebase's to take, and taking it had a concrete cost: pointing
794
+ * Rebase at a database that already had a Supabase `auth` schema meant
795
+ * `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
796
+ * `RETURNS uuid`, which Postgres rejects outright —
797
+ *
798
+ * ERROR: cannot change return type of existing function
799
+ * HINT: Use DROP FUNCTION auth.uid() first.
800
+ *
801
+ * — and the failure landed inside a catch-all that logged a warning and carried
802
+ * on, leaving a database with auth tables, no helper functions, and policies
803
+ * calling functions that did not exist. Under `rebase db migrate` the same
804
+ * statements aborted the migration instead.
805
+ *
806
+ * `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
807
+ * schema untouched and gains a `rebase` one, which is what a gradual migration
808
+ * needs.
809
+ *
810
+ * ## Why functions at all, rather than inlining `current_setting`
811
+ *
812
+ * Because the indirection has already been spent once. `uid()` resolves
813
+ * `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
814
+ * rolling deploy — old and new pods serving one database — both eras resolve
815
+ * the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
816
+ * bodies it would have been a rewrite of every policy on every table.
817
+ *
818
+ * ## Why the name is not configurable
819
+ *
820
+ * A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
821
+ * these strings are written into every policy in every database Rebase has
822
+ * provisioned. Everything that reads policies back — the SQL-to-policy parser
823
+ * behind the admin UI, the drift checker, `rls-check` — would have to know the
824
+ * configured value to recognise its own output. One frozen name is the feature.
825
+ */
826
+ /** The schema Rebase owns. The only schema Rebase creates. */
827
+ var REBASE_SCHEMA = "rebase";
828
+ /**
829
+ * The principal of the current request, as text, or NULL in the server context.
830
+ *
831
+ * Never NULL for a user request — an anonymous one carries
832
+ * {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
833
+ * the trusted server plane and `IS NOT NULL` a tautology.
834
+ */
835
+ var RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
836
+ /** The request's roles as a comma-separated string, for `string_to_array`. */
837
+ var RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
838
+ /** The request's JWT claims as `jsonb`, or `{}`. */
839
+ var RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
840
+ /**
841
+ * The pre-1.0 spellings, for recognising policies and hand-written SQL that
842
+ * predate the move.
843
+ *
844
+ * Kept because policies outlive the server that wrote them: a database migrated
845
+ * by an older release still holds `auth.uid()` in its policy bodies until the
846
+ * next push or boot recompiles them, and anything that reads policies back has
847
+ * to recognise both eras or report the framework's own output as foreign drift.
848
+ * Also used to give a project whose `securityRules` contain raw `auth.uid()` a
849
+ * message naming the replacement, instead of a parse failure.
850
+ */
851
+ var LEGACY_RLS_SCHEMA = "auth";
852
+ var LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;
853
+ var LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;
854
+ var LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;
855
+ /**
856
+ * Rewrites the pre-1.0 function calls in a fragment of policy SQL.
857
+ *
858
+ * Deliberately anchored on a word boundary and the schema qualifier, so a column
859
+ * called `auth_uid` or a table named `auth` is left alone.
860
+ */
861
+ function rewriteLegacyRlsFunctions(sql) {
862
+ return sql.replace(/\bauth\.(uid|jwt|roles)\s*\(\s*\)/gi, (_match, fn) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`);
863
+ }
864
+ /** Whether a fragment of SQL still calls the pre-1.0 functions. */
865
+ function usesLegacyRlsFunctions(sql) {
866
+ return /\bauth\.(uid|jwt|roles)\s*\(\s*\)/i.test(sql);
867
+ }
868
+ //#endregion
703
869
  //#region src/types/backend.ts
704
870
  /**
705
871
  * Type guard: does this admin support SQL operations?
@@ -1131,24 +1297,51 @@ function computeSchemaVersion(collections) {
1131
1297
  var DEFAULT_LIST_LIMIT = 50;
1132
1298
  /** Rows returned for a vector-search list read when the client sends no `limit`. */
1133
1299
  var DEFAULT_VECTOR_LIST_LIMIT = 10;
1134
- /** Hard ceiling clamped onto any client-supplied `limit`, on every surface. */
1300
+ /** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */
1135
1301
  var MAX_LIST_LIMIT = 1e3;
1136
1302
  /**
1303
+ * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
1304
+ * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
1305
+ * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
1306
+ *
1307
+ * @group Errors
1308
+ */
1309
+ var ListLimitError = class ListLimitError extends RebaseApiError {
1310
+ /** The ceiling that was exceeded — what the caller should page by instead. */
1311
+ maxLimit;
1312
+ constructor(message, maxLimit) {
1313
+ super(message, {
1314
+ status: 400,
1315
+ code: "INVALID_LIMIT"
1316
+ });
1317
+ this.name = "ListLimitError";
1318
+ this.maxLimit = maxLimit;
1319
+ Object.setPrototypeOf(this, ListLimitError.prototype);
1320
+ }
1321
+ };
1322
+ /**
1137
1323
  * Resolve a client-supplied list `limit` into a safe, always-defined value.
1138
1324
  *
1139
- * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
1140
- * so `0`, negatives, and absurd values can never bypass the cap.
1141
- * - An absent / blank / non-numeric limit falls back to the mode default:
1325
+ * - An absent / blank limit falls back to the mode default:
1142
1326
  * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
1327
+ * - A limit that is present must be an integer in `[1, maxLimit]`. Anything
1328
+ * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
1329
+ * {@link ListLimitError} rather than being coerced into range, because every
1330
+ * coercion answers a question the caller did not ask with a page it cannot
1331
+ * tell apart from the whole collection.
1143
1332
  *
1144
1333
  * The return is never `undefined` — no ingress that routes its client limit
1145
1334
  * through this can produce an unbounded read.
1335
+ *
1336
+ * @throws {ListLimitError} when a present `limit` is not an integer in range.
1146
1337
  */
1147
1338
  function resolveClientListLimit(rawLimit, opts = {}) {
1148
1339
  const maxLimit = opts.maxLimit ?? 1e3;
1149
1340
  if (rawLimit != null && String(rawLimit).trim() !== "") {
1150
- const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
1151
- if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
1341
+ const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
1342
+ if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
1343
+ 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);
1344
+ return parsed;
1152
1345
  }
1153
1346
  return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
1154
1347
  }
@@ -1180,6 +1373,6 @@ function isPublicStoragePath(path) {
1180
1373
  return p.startsWith("public/") || p.startsWith(`default/public/`);
1181
1374
  }
1182
1375
  //#endregion
1183
- export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_LIST_LIMIT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REST_TO_CANONICAL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, serializeCollections, storageEnvSuffix, toCanonicalOp };
1376
+ export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIREBASE_CAPABILITIES, GeoPoint, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, ListLimitError, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, REST_TO_CANONICAL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, canonicalSchemaPayload, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationalCollectionConfig, isSQLAdmin, isSchemaAdmin, isSerializedCollectionRef, isToMany, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys, normalizeStorageSources, policy, registerDataSourceCapabilities, resolveClientListLimit, rewriteLegacyRlsFunctions, serializeCollections, storageEnvSuffix, toCanonicalOp, usesLegacyRlsFunctions };
1184
1377
 
1185
1378
  //# sourceMappingURL=index.es.js.map