@rebasepro/common 0.13.0 → 0.13.1-canary.g394d868

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.
@@ -72,4 +72,18 @@ export declare function serializeLogicalCondition(cond: LogicalCondition | Filte
72
72
  * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
73
73
  * // → { type: "or", conditions: [...] }
74
74
  */
75
- export declare function deserializeLogicalCondition(str: string): LogicalCondition | FilterCondition;
75
+ /**
76
+ * How deeply `or(...)`/`and(...)` groups may nest.
77
+ *
78
+ * This parser recurses once per level, on a value that arrives in a query
79
+ * string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
80
+ * stack size exceeded`, which a caller sees as a 500 about the call stack
81
+ * rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
82
+ * below that in practice, but "the HTTP layer happens to stop it" is not a
83
+ * bound this parser should rely on.
84
+ *
85
+ * Thirty-two is far past anything a real filter expresses; the deepest in this
86
+ * repository's own tests is three.
87
+ */
88
+ export declare const MAX_LOGICAL_NESTING_DEPTH = 32;
89
+ export declare function deserializeLogicalCondition(str: string, nesting?: number): LogicalCondition | FilterCondition;
@@ -45,6 +45,26 @@ export declare class RebasePaginationError extends Error {
45
45
  }
46
46
  /** The one thing a transport has to provide to be paginated. */
47
47
  export type PageFinder<M extends Record<string, unknown> = Record<string, unknown>> = (params: FindParams<M>) => Promise<FindResult<M>>;
48
+ /**
49
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
50
+ *
51
+ * Lives here, next to the walk, for the reason at the top of this file: every
52
+ * transport has to mean the same thing by "page two". Four of them did not —
53
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
54
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
55
+ * the published type documented a fourth number. Pages that overlap or skip
56
+ * rows are the mildest of those outcomes.
57
+ *
58
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
59
+ * is the value to hand a driver: it stays `undefined` when the caller named no
60
+ * offset, because keyset pagination seeks with a `where` clause and must not
61
+ * look like it is paging by offset.
62
+ */
63
+ export declare function resolveFindWindow(params?: Pick<FindParams, "limit" | "offset" | "page">): {
64
+ limit: number;
65
+ offset: number;
66
+ driverOffset: number | undefined;
67
+ };
48
68
  /**
49
69
  * Walk every row a query matches, yielding one row at a time and fetching the
50
70
  * next page only when the consumer asks for it.
@@ -41,3 +41,39 @@ export declare function createDataSourceRegistry(definitions?: DataSourceDefinit
41
41
  * @param registry optional registry of declared data sources
42
42
  */
43
43
  export declare function resolveDataSource(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): ResolvedDataSource;
44
+ /**
45
+ * Does a SQL toolchain own this collection's storage?
46
+ *
47
+ * "Owns the storage" means: something generates a table for it, pushes that
48
+ * table to a database, plans its RLS policies, and reports it as drifted when
49
+ * the two disagree. That is true of a Postgres collection and false of a
50
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
51
+ * migrates — and the two were never told apart. Every stage of the SQL
52
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
53
+ * collection declared next to the Postgres ones got a `pgTable` in the
54
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
55
+ * `db push` include list — where its name shielding a same-named real table
56
+ * from Atlas's exclude list is the one that can lose data.
57
+ *
58
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
59
+ * name check: an engine registered through `registerDataSourceCapabilities`
60
+ * gets the same treatment as the built-in ones.
61
+ *
62
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
63
+ * tooling (the CLI, the schema generator) has no data-source registry to
64
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
65
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
66
+ * including a collection generates a table nothing writes to; wrongly excluding
67
+ * one silently stops generating a table the app is serving from. Declare
68
+ * `engine` on a collection that is not SQL-backed and this is exact.
69
+ */
70
+ export declare function isRelationalCollection(collection: DataSourceResolvable | undefined, registry?: DataSourceRegistry): boolean;
71
+ /**
72
+ * The subset of `collections` a SQL toolchain owns — see
73
+ * {@link isRelationalCollection}.
74
+ *
75
+ * Every stage that generates SQL from collections starts by calling this, so
76
+ * the rule lives in one place rather than being re-decided per generator. It
77
+ * keeps the input order.
78
+ */
79
+ export declare function relationalCollections<C extends DataSourceResolvable>(collections: readonly C[], registry?: DataSourceRegistry): C[];
package/dist/index.es.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
1
+ import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, DEFAULT_LIST_LIMIT, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
2
2
  import { deepClone, generateForeignKeyName, getIn, getPolicyNamesForRules, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, prettifyIdentifier, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
3
3
  import jsonLogic from "json-logic-js";
4
4
  import { deepEqual } from "fast-equals";
@@ -89,10 +89,21 @@ function getRelationFrom(entity) {
89
89
  * have `id` and `path` fields — these are relation-shaped objects from
90
90
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
91
91
  *
92
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
93
+ * foreign key, and the REST layer returns it as the scalar it is; only some
94
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
95
+ * depends on how the row was loaded, and a caller that only accepted objects
96
+ * reported half of its own data as a type error. The declared target is the
97
+ * missing half: with it, an id is a relation that has not been fetched yet.
98
+ *
92
99
  * Returns null if the value cannot be coerced.
93
100
  */
94
- function normalizeToEntityRelation(value, propertyType) {
101
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
95
102
  if (value instanceof EntityRelation) return value;
103
+ if (targetPath && (typeof value === "string" || typeof value === "number")) {
104
+ if (value === "") return null;
105
+ return new EntityRelation(value, targetPath);
106
+ }
96
107
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
97
108
  const obj = value;
98
109
  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;
@@ -608,6 +619,33 @@ function resolveCollectionRelations(collection) {
608
619
  _resolvedRelationsCache.set(collection, relations);
609
620
  return relations;
610
621
  }
622
+ /**
623
+ * The path of the collection a relation property points at, derived from the
624
+ * property alone.
625
+ *
626
+ * A preview holds a property and a value and no collection, so it cannot call
627
+ * `resolveRelationProperty`. It does not need to: both forms that carry a
628
+ * target — the stamped `resolvedRelation` and the inline `relation` — name it
629
+ * directly. Only the third form, a relation declared by name in the
630
+ * collection's `relations` array, is out of reach, and that one has no target
631
+ * to read without the collection anyway.
632
+ *
633
+ * This is what lets a preview render a relation column that arrived as a bare
634
+ * foreign key: the id says *which* row, the declared target says *which
635
+ * collection*, and `RelationPreview` fetches the rest. Without it a scalar id
636
+ * is indistinguishable from a value of the wrong type.
637
+ */
638
+ function getRelationTargetPath(property) {
639
+ const stamped = property.resolvedRelation?.targetSlug;
640
+ if (stamped) return stamped;
641
+ const target = property.relation?.target;
642
+ if (typeof target !== "function") return void 0;
643
+ try {
644
+ return target()?.slug;
645
+ } catch (_e) {
646
+ return;
647
+ }
648
+ }
611
649
  function getTableName(collection) {
612
650
  if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
613
651
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
@@ -882,6 +920,59 @@ function getEntityChildViews(collection) {
882
920
  return views;
883
921
  }
884
922
  /**
923
+ * Each of `collection`'s tabs paired with the property that declared it, when a
924
+ * property declared it: child view key → property key.
925
+ *
926
+ * A many-relation can only be declared as a property — that is the documented
927
+ * and only mechanism — and {@link getEntityChildViews} promotes it to a tab. So
928
+ * one declaration reaches the panel twice, and neither surface knew about the
929
+ * other. The form rendered a relation picker beside the tab, and the collection
930
+ * table rendered *two* columns under one heading: the relation's own column,
931
+ * showing the child rows, and a jump-to-tab button carrying the same name.
932
+ *
933
+ * The pairing is what lets each surface decide which half is redundant, and it
934
+ * has to be a pairing rather than two sets because the two keys differ whenever
935
+ * a relation is named. The match is on the resolved `relationName` — the
936
+ * identity `getEntityChildViews` itself dedupes on — so a relation declared in
937
+ * `relations` and pointed at by a differently-named property is recognised too.
938
+ *
939
+ * A relation with no property of its own is absent here, which is the point: it
940
+ * has exactly one surface already, and nothing to weigh it against.
941
+ *
942
+ * Only top-level properties: a relation nested inside a `map` gets no tab.
943
+ */
944
+ function getChildViewDeclaringProperties(collection) {
945
+ const pairs = /* @__PURE__ */ new Map();
946
+ const relationProperties = Object.entries(collection.properties ?? {}).filter(([, property]) => property?.type === "relation");
947
+ if (relationProperties.length === 0) return pairs;
948
+ const relationViews = getEntityChildViews(collection).filter((view) => view.source.kind === "relation");
949
+ if (relationViews.length === 0) return pairs;
950
+ const resolvedRelations = resolveCollectionRelations(collection);
951
+ const identityOf = (relationKey) => resolvedRelations[relationKey]?.relationName ?? relationKey;
952
+ const declaringPropertyByIdentity = /* @__PURE__ */ new Map();
953
+ for (const [propertyKey, property] of relationProperties) {
954
+ const relation = property.resolvedRelation ?? resolvedRelations[propertyKey];
955
+ if (relation?.cardinality !== "many") continue;
956
+ const identity = relation.relationName ?? propertyKey;
957
+ if (!declaringPropertyByIdentity.has(identity)) declaringPropertyByIdentity.set(identity, propertyKey);
958
+ }
959
+ for (const view of relationViews) {
960
+ const propertyKey = declaringPropertyByIdentity.get(identityOf(view.source.relationKey));
961
+ if (propertyKey) pairs.set(view.key, propertyKey);
962
+ }
963
+ return pairs;
964
+ }
965
+ /**
966
+ * The property keys of `collection` whose relation is already one of its tabs.
967
+ *
968
+ * What a form asks: the tab is the treatment for a list of child rows, so the
969
+ * picker beside it is the redundant half. See
970
+ * {@link getChildViewDeclaringProperties}.
971
+ */
972
+ function getChildViewRelationPropertyKeys(collection) {
973
+ return new Set(getChildViewDeclaringProperties(collection).values());
974
+ }
975
+ /**
885
976
  * The child views of `collection` as bare collections.
886
977
  *
887
978
  * The flattened view of {@link getEntityChildViews}, for navigation code that
@@ -1996,9 +2087,14 @@ function registerConditionOperations() {
1996
2087
  operationsRegistered = true;
1997
2088
  }
1998
2089
  /**
1999
- * Evaluate a JSON Logic rule against the given context.
2090
+ * Evaluate a condition against the given context.
2091
+ *
2092
+ * A condition may be stated as a literal instead of a rule — `hidden: true`
2093
+ * rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
2094
+ * answer, so it is returned rather than handed to the evaluator.
2000
2095
  */
2001
2096
  function evaluateCondition(rule, context) {
2097
+ if (typeof rule === "boolean") return rule;
2002
2098
  registerConditionOperations();
2003
2099
  return jsonLogic.apply(rule, context);
2004
2100
  }
@@ -2359,6 +2455,46 @@ function resolveDataSource(collection, registry) {
2359
2455
  capabilities: getDataSourceCapabilities(engine)
2360
2456
  };
2361
2457
  }
2458
+ /**
2459
+ * Does a SQL toolchain own this collection's storage?
2460
+ *
2461
+ * "Owns the storage" means: something generates a table for it, pushes that
2462
+ * table to a database, plans its RLS policies, and reports it as drifted when
2463
+ * the two disagree. That is true of a Postgres collection and false of a
2464
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
2465
+ * migrates — and the two were never told apart. Every stage of the SQL
2466
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
2467
+ * collection declared next to the Postgres ones got a `pgTable` in the
2468
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
2469
+ * `db push` include list — where its name shielding a same-named real table
2470
+ * from Atlas's exclude list is the one that can lose data.
2471
+ *
2472
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
2473
+ * name check: an engine registered through `registerDataSourceCapabilities`
2474
+ * gets the same treatment as the built-in ones.
2475
+ *
2476
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
2477
+ * tooling (the CLI, the schema generator) has no data-source registry to
2478
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
2479
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
2480
+ * including a collection generates a table nothing writes to; wrongly excluding
2481
+ * one silently stops generating a table the app is serving from. Declare
2482
+ * `engine` on a collection that is not SQL-backed and this is exact.
2483
+ */
2484
+ function isRelationalCollection(collection, registry) {
2485
+ return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
2486
+ }
2487
+ /**
2488
+ * The subset of `collections` a SQL toolchain owns — see
2489
+ * {@link isRelationalCollection}.
2490
+ *
2491
+ * Every stage that generates SQL from collections starts by calling this, so
2492
+ * the rule lives in one place rather than being re-decided per generator. It
2493
+ * keeps the input order.
2494
+ */
2495
+ function relationalCollections(collections, registry) {
2496
+ return collections.filter((collection) => isRelationalCollection(collection, registry));
2497
+ }
2362
2498
  //#endregion
2363
2499
  //#region src/collections/CollectionRegistry.ts
2364
2500
  var CollectionRegistry = class {
@@ -2861,6 +2997,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
2861
2997
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
2862
2998
  }
2863
2999
  };
3000
+ /**
3001
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
3002
+ *
3003
+ * Lives here, next to the walk, for the reason at the top of this file: every
3004
+ * transport has to mean the same thing by "page two". Four of them did not —
3005
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
3006
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
3007
+ * the published type documented a fourth number. Pages that overlap or skip
3008
+ * rows are the mildest of those outcomes.
3009
+ *
3010
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
3011
+ * is the value to hand a driver: it stays `undefined` when the caller named no
3012
+ * offset, because keyset pagination seeks with a `where` clause and must not
3013
+ * look like it is paging by offset.
3014
+ */
3015
+ function resolveFindWindow(params) {
3016
+ const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
3017
+ const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
3018
+ return {
3019
+ limit,
3020
+ offset,
3021
+ driverOffset: params?.page != null ? offset : params?.offset
3022
+ };
3023
+ }
2864
3024
  function normalizePageSize(raw) {
2865
3025
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
2866
3026
  return Math.max(1, Math.floor(raw));
@@ -3194,7 +3354,22 @@ function serializeLogicalCondition(cond) {
3194
3354
  * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
3195
3355
  * // → { type: "or", conditions: [...] }
3196
3356
  */
3197
- function deserializeLogicalCondition(str) {
3357
+ /**
3358
+ * How deeply `or(...)`/`and(...)` groups may nest.
3359
+ *
3360
+ * This parser recurses once per level, on a value that arrives in a query
3361
+ * string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
3362
+ * stack size exceeded`, which a caller sees as a 500 about the call stack
3363
+ * rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
3364
+ * below that in practice, but "the HTTP layer happens to stop it" is not a
3365
+ * bound this parser should rely on.
3366
+ *
3367
+ * Thirty-two is far past anything a real filter expresses; the deepest in this
3368
+ * repository's own tests is three.
3369
+ */
3370
+ var MAX_LOGICAL_NESTING_DEPTH = 32;
3371
+ function deserializeLogicalCondition(str, nesting = 0) {
3372
+ 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)\`.`);
3198
3373
  const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
3199
3374
  if (logicalMatch) {
3200
3375
  const type = logicalMatch[1];
@@ -3205,10 +3380,10 @@ function deserializeLogicalCondition(str) {
3205
3380
  for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
3206
3381
  else if (innerStr[i] === ")") depth--;
3207
3382
  else if (innerStr[i] === "," && depth === 0) {
3208
- conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
3383
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start, i), nesting + 1));
3209
3384
  start = i + 1;
3210
3385
  }
3211
- conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
3386
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start), nesting + 1));
3212
3387
  return {
3213
3388
  type,
3214
3389
  conditions
@@ -3322,21 +3497,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3322
3497
  const accessor = {
3323
3498
  async find(params) {
3324
3499
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3325
- const limit = params?.limit ?? 20;
3326
- const offset = params?.offset ?? 0;
3500
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3327
3501
  const fetchService = driver.restFetchService;
3328
3502
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3329
3503
  filter,
3330
- limit: params?.limit,
3331
- offset: params?.offset,
3504
+ logical: params?.logical,
3505
+ limit,
3506
+ offset: driverOffset,
3332
3507
  orderBy: params?.orderBy?.[0],
3333
3508
  order: params?.orderBy?.[1],
3334
3509
  searchString: params?.searchString
3335
3510
  }, params?.include) : await driver.fetchCollection({
3336
3511
  path: slug,
3337
- limit: params?.limit,
3338
- offset: params?.offset,
3512
+ limit,
3513
+ offset: driverOffset,
3339
3514
  filter,
3515
+ logical: params?.logical,
3340
3516
  orderBy: params?.orderBy?.[0],
3341
3517
  order: params?.orderBy?.[1],
3342
3518
  searchString: params?.searchString
@@ -3346,7 +3522,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3346
3522
  if (driver.count) {
3347
3523
  total = await driver.count({
3348
3524
  path: slug,
3349
- filter
3525
+ filter,
3526
+ logical: params?.logical,
3527
+ searchString: params?.searchString
3350
3528
  });
3351
3529
  hasMore = offset + rows.length < total;
3352
3530
  }
@@ -3398,22 +3576,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3398
3576
  values: {}
3399
3577
  } });
3400
3578
  },
3579
+ updateMany: driver.updateMany ? async (updates) => {
3580
+ return (await driver.updateMany({
3581
+ path: slug,
3582
+ updates: updates.map((u) => ({
3583
+ id: u.id,
3584
+ values: u.data
3585
+ }))
3586
+ })).map((row) => rowToEntity(row, slug, getPks()));
3587
+ } : void 0,
3588
+ deleteMany: driver.deleteMany ? async (ids) => {
3589
+ await driver.deleteMany({
3590
+ path: slug,
3591
+ ids
3592
+ });
3593
+ } : void 0,
3401
3594
  count: driver.count ? async (params) => {
3402
3595
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3403
3596
  return driver.count({
3404
3597
  path: slug,
3405
- filter
3598
+ filter,
3599
+ logical: params?.logical,
3600
+ searchString: params?.searchString
3406
3601
  });
3407
3602
  } : void 0,
3408
3603
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
3409
- const limit = params?.limit ?? 20;
3410
- const offset = params?.offset ?? 0;
3604
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3411
3605
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
3412
3606
  return driver.listenCollection({
3413
3607
  path: slug,
3414
- limit: params?.limit,
3415
- offset: params?.offset,
3608
+ limit,
3609
+ offset: driverOffset,
3416
3610
  filter: params?.where,
3611
+ logical: params?.logical,
3417
3612
  orderBy: params?.orderBy?.[0],
3418
3613
  order: params?.orderBy?.[1],
3419
3614
  searchString: params?.searchString,
@@ -3421,7 +3616,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3421
3616
  onUpdate({
3422
3617
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
3423
3618
  meta: {
3424
- total: entities.length,
3619
+ total: offset + entities.length,
3425
3620
  limit,
3426
3621
  offset,
3427
3622
  hasMore: entities.length >= limit
@@ -3598,9 +3793,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
3598
3793
  async update(id, data) {
3599
3794
  return entityToRow(await snap.update(id, data));
3600
3795
  },
3796
+ async updateMany(updates) {
3797
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
3798
+ if (updates.length === 0) return [];
3799
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
3800
+ return (await snap.updateMany(updates.map((u) => ({
3801
+ id: u.id,
3802
+ data: u.data
3803
+ })))).map(entityToRow);
3804
+ },
3601
3805
  delete(id) {
3602
3806
  return snap.delete(id);
3603
3807
  },
3808
+ async deleteMany(ids) {
3809
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
3810
+ if (ids.length === 0) return;
3811
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
3812
+ await snap.deleteMany(ids);
3813
+ },
3604
3814
  count: snap.count ? (params) => snap.count(params) : void 0,
3605
3815
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
3606
3816
  data: res.data.map(entityToRow),
@@ -3623,7 +3833,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
3623
3833
  /**
3624
3834
  * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
3625
3835
  * {@link CollectionAccessor}. Every returned row is re-wrapped into the
3626
- * `{ id, path, values }` view-model the admin admin renders.
3836
+ * `{ id, path, values }` view-model the admin panel renders.
3627
3837
  */
3628
3838
  function toEntityAccessor(sdk, slug, getPks = () => []) {
3629
3839
  const accessor = {
@@ -3906,6 +4116,6 @@ async function detectJunctionTables(executeSql) {
3906
4116
  return junctionTables;
3907
4117
  }
3908
4118
  //#endregion
3909
- export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
4119
+ export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_FIND_ALL_MAX_ROWS, DEFAULT_MAX_PAGES, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, DEFAULT_PAGE_SIZE, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, collectAllPages, cond, createDataSourceRegistry, createPaginationHelpers, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationalCollection, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, relationalCollections, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
3910
4120
 
3911
4121
  //# sourceMappingURL=index.es.js.map