@rebasepro/server-postgres 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.
Files changed (38) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/backup-service-CD8o_1Sl.js.map +1 -1
  3. package/dist/cli-helpers.d.ts +1 -1
  4. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-BTdgHBKV.js} +2 -2
  5. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-BTdgHBKV.js.map} +1 -1
  6. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-CdRsuy33.js} +33 -7
  7. package/dist/ensure-collection-tables-CdRsuy33.js.map +1 -0
  8. package/dist/index.es.js +231 -94
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/schema/ensure-collection-tables.d.ts +1 -1
  11. package/dist/schema/generate-drizzle-schema-logic.d.ts +1 -1
  12. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  13. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  14. package/dist/services/FetchService.d.ts +10 -7
  15. package/dist/services/dataService.d.ts +2 -0
  16. package/dist/services/realtimeService.d.ts +25 -21
  17. package/dist/{src-DlPBctw_.js → src-B0PGnKU3.js} +174 -16
  18. package/dist/src-B0PGnKU3.js.map +1 -0
  19. package/dist/src-DoU9yPqq.js.map +1 -1
  20. package/package.json +9 -8
  21. package/src/PostgresBackendDriver.ts +165 -3
  22. package/src/cli-helpers.ts +8 -2
  23. package/src/cli.ts +79 -0
  24. package/src/collections/validate-relations.ts +124 -17
  25. package/src/data-transformer.ts +13 -3
  26. package/src/schema/doctor.ts +7 -5
  27. package/src/schema/ensure-collection-tables.ts +8 -2
  28. package/src/schema/generate-drizzle-schema-logic.ts +8 -2
  29. package/src/schema/generate-postgres-ddl-logic.ts +47 -11
  30. package/src/schema/generated-schema-staleness.ts +169 -0
  31. package/src/schema/introspect-db-logic.ts +1 -1
  32. package/src/schema/non-sql-collections.test.ts +131 -0
  33. package/src/services/FetchService.ts +10 -93
  34. package/src/services/PersistService.ts +14 -1
  35. package/src/services/dataService.ts +2 -0
  36. package/src/services/realtimeService.ts +36 -33
  37. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  38. package/dist/src-DlPBctw_.js.map +0 -1
@@ -107,7 +107,7 @@ export interface EnsureOutcome extends EnsurePlan {
107
107
  * that reference them, tables before the columns added to other tables (a new
108
108
  * table may be the target of a relation), and nothing is emitted twice.
109
109
  */
110
- export declare function planCollectionSchemaEnsure(collections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
110
+ export declare function planCollectionSchemaEnsure(allCollections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
111
111
  /** Read what the database has, for the schemas the collections live in. */
112
112
  export declare function readExistingSchema(client: Queryable, schemas: string[]): Promise<ExistingSchema>;
113
113
  /**
@@ -1,2 +1,2 @@
1
1
  import { CollectionConfig } from "@rebasepro/types";
2
- export declare const generateSchema: (collections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
2
+ export declare const generateSchema: (allCollections: CollectionConfig[], stripPolicies?: boolean) => Promise<string>;
@@ -29,7 +29,7 @@ export declare const generatePolicyStatements: (collection: CollectionConfig, ru
29
29
  */
30
30
  export declare const quoteSqlLiteral: (value: string) => string;
31
31
  export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
32
- export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
32
+ export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
33
33
  includePolicies?: boolean;
34
34
  }) => Promise<string>;
35
35
  /** The RLS statements one declared collection's table needs, ready to run. */
@@ -100,7 +100,7 @@ export interface JunctionTablePlan {
100
100
  * is unknown yields the column without a constraint. Both mirror the generator
101
101
  * exactly — a divergence here is a schema fork between boot and `db push`.
102
102
  */
103
- export declare const planRelationalColumns: (collections: CollectionConfig[]) => RelationalColumnPlan[];
103
+ export declare const planRelationalColumns: (allCollections: CollectionConfig[]) => RelationalColumnPlan[];
104
104
  /**
105
105
  * The junction tables a bundle's many-to-many relations imply.
106
106
  *
@@ -109,7 +109,7 @@ export declare const planRelationalColumns: (collections: CollectionConfig[]) =>
109
109
  * junction with row-level security left off is readable and writable by every
110
110
  * signed-in user, which is why the two must ship together.
111
111
  */
112
- export declare const planJunctionTables: (collections: CollectionConfig[]) => JunctionTablePlan[];
112
+ export declare const planJunctionTables: (allCollections: CollectionConfig[]) => JunctionTablePlan[];
113
113
  export interface CollectionPolicyPlan {
114
114
  /** The table's schema (e.g. `public`, `rebase`). */
115
115
  schema: string;
@@ -137,6 +137,6 @@ export interface CollectionPolicyPlan {
137
137
  * writable by every signed-in user. A junction whose table is still absent is
138
138
  * skipped by the applier, not planned away here.
139
139
  */
140
- export declare const planCollectionPolicies: (collections: CollectionConfig[]) => CollectionPolicyPlan[];
141
- export declare const generatePostgresPoliciesDdl: (collections: CollectionConfig[]) => string;
140
+ export declare const planCollectionPolicies: (allCollections: CollectionConfig[]) => CollectionPolicyPlan[];
141
+ export declare const generatePostgresPoliciesDdl: (allCollections: CollectionConfig[]) => string;
142
142
  export {};
@@ -0,0 +1,39 @@
1
+ import { CollectionConfig } from "@rebasepro/types";
2
+ /**
3
+ * Notice a generated Drizzle schema that a *library upgrade* invalidated.
4
+ *
5
+ * `rebase dev` already watches `config/collections` and warns when a collection
6
+ * file changes. That covers drift the developer caused. It cannot cover this one,
7
+ * because nothing the developer owns changed: 0.13 derives `category_id` where
8
+ * 0.12 derived `categorie_id`, from the same unedited collection. The watcher
9
+ * never fires, and `backend/src/schema.generated.ts` quietly stops describing the
10
+ * schema the runtime expects.
11
+ *
12
+ * The consequence is not cosmetic. Boot-ensure renames the column in the
13
+ * database, then relation validation reads the stale module and refuses to
14
+ * start — on that boot and every boot after it, because the rename is already
15
+ * applied and will not be attempted again.
16
+ *
17
+ * Deliberately narrow: this answers "does the generated schema name a foreign key
18
+ * the way the previous rule did", not "is this file what we would generate now".
19
+ * The wide question would report every whitespace change in the generator as a
20
+ * fatal staleness, and a check that cries wolf gets switched off.
21
+ */
22
+ /** One column the generated schema names under the pre-0.13 rule. */
23
+ export interface LegacyForeignKeyName {
24
+ /** Table whose column declaration is stale. */
25
+ table: string;
26
+ /** The name the generated schema declares. */
27
+ legacy: string;
28
+ /** The name this release derives, and which the database now carries. */
29
+ current: string;
30
+ /** `<collection>.<relation>` that derives it, for the message. */
31
+ relation: string;
32
+ }
33
+ /**
34
+ * @param generatedSource contents of `backend/src/schema.generated.ts`
35
+ * @param collections the project's collections, as this release reads them
36
+ */
37
+ export declare function findLegacyForeignKeyNames(generatedSource: string, collections: CollectionConfig[]): LegacyForeignKeyName[];
38
+ /** One-line summary for a log or a CLI notice. */
39
+ export declare function describeLegacyForeignKeyNames(found: LegacyForeignKeyName[]): string;
@@ -153,6 +153,16 @@ export declare class FetchService {
153
153
  */
154
154
  fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
155
155
  filter?: FilterValues<Extract<keyof M, string>>;
156
+ /**
157
+ * An `or(...)`/`and(...)` group, applied alongside `filter`.
158
+ *
159
+ * `fetchRowsWithConditions` below has always applied this; it was
160
+ * simply absent from this signature, so the only callers that could
161
+ * pass one were the ones that went around this method. Realtime
162
+ * came through here, which is why a subscription filtered by a
163
+ * logical group was pushed every row in the table.
164
+ */
165
+ logical?: LogicalCondition;
156
166
  orderBy?: string;
157
167
  order?: "desc" | "asc";
158
168
  limit?: number;
@@ -226,13 +236,6 @@ export declare class FetchService {
226
236
  * Note: Primary path now uses inline `getQueryBuilder()` checks.
227
237
  */
228
238
  private hasDrizzleQueryAPI;
229
- /**
230
- * Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
231
- * for efficient JOIN-based relation loading.
232
- * Returns null if the API is not available or the query fails.
233
- * Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
234
- */
235
- private fetchWithDrizzleQuery;
236
239
  /**
237
240
  * Fallback path used when db.query is unavailable.
238
241
  * The primary path uses db.query.findMany with `with` config, which
@@ -36,6 +36,8 @@ export declare class DataService implements DataRepository {
36
36
  */
37
37
  fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
38
38
  filter?: FilterValues<Extract<keyof M, string>>;
39
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
40
+ logical?: LogicalCondition;
39
41
  orderBy?: string;
40
42
  order?: "desc" | "asc";
41
43
  limit?: number;
@@ -1,6 +1,6 @@
1
1
  import { WebSocket } from "ws";
2
2
  import { EventEmitter } from "events";
3
- import { DataDriver, WebSocketMessage } from "@rebasepro/types";
3
+ import { DataDriver, WebSocketMessage, LogicalCondition } from "@rebasepro/types";
4
4
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
5
5
  import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces";
6
6
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
@@ -14,6 +14,27 @@ export interface SubscriptionAuthContext {
14
14
  uid: string;
15
15
  roles: string[];
16
16
  }
17
+ /**
18
+ * The narrowing a collection subscription was created with, kept so that every
19
+ * refetch answers the same query the initial fetch did.
20
+ *
21
+ * Named once because it used to be written out inline in five places, and a
22
+ * field missing from one of them is accepted over the wire and then silently
23
+ * ignored: `offset` was declared on the incoming props and never stored, so a
24
+ * live list on page three served page one, and `logical` was never stored
25
+ * either, so an `or(...)` subscription was pushed every row in the table.
26
+ */
27
+ type StoredCollectionRequest = {
28
+ filter?: Record<string, unknown>;
29
+ logical?: LogicalCondition;
30
+ orderBy?: string;
31
+ order?: "desc" | "asc";
32
+ limit?: number;
33
+ offset?: number;
34
+ startAfter?: Record<string, unknown>;
35
+ databaseId?: string;
36
+ searchString?: string;
37
+ };
17
38
  /**
18
39
  * PostgreSQL-specific realtime service.
19
40
  * Handles WebSocket connections and subscriptions for real-time row updates.
@@ -128,16 +149,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
128
149
  type: "collection" | "single";
129
150
  path: string;
130
151
  id?: string | number;
131
- collectionRequest?: {
132
- filter?: Record<string, unknown>;
133
- orderBy?: string;
134
- order?: "desc" | "asc";
135
- limit?: number;
136
- offset?: number;
137
- startAfter?: Record<string, unknown>;
138
- databaseId?: string;
139
- searchString?: string;
140
- };
152
+ collectionRequest?: StoredCollectionRequest;
141
153
  authContext?: SubscriptionAuthContext;
142
154
  }>;
143
155
  registerDataDriverSubscription(subscriptionId: string, subscription: {
@@ -145,16 +157,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
145
157
  type: "collection" | "single";
146
158
  path: string;
147
159
  id?: string | number;
148
- collectionRequest?: {
149
- filter?: Record<string, unknown>;
150
- orderBy?: string;
151
- order?: "desc" | "asc";
152
- limit?: number;
153
- offset?: number;
154
- startAfter?: Record<string, unknown>;
155
- databaseId?: string;
156
- searchString?: string;
157
- };
160
+ collectionRequest?: StoredCollectionRequest;
158
161
  authContext?: SubscriptionAuthContext;
159
162
  }): void;
160
163
  addSubscriptionCallback(subscriptionId: string, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void): void;
@@ -497,3 +500,4 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
497
500
  * This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.
498
501
  */
499
502
  export declare const PostgresRealtimeProvider: typeof RealtimeService;
503
+ export {};
@@ -55,6 +55,25 @@ function hasForeignKeyOnTarget(relation) {
55
55
  function isManyToMany(relation) {
56
56
  return relation.kind === "manyToMany";
57
57
  }
58
+ /**
59
+ * Resolve a client-supplied list `limit` into a safe, always-defined value.
60
+ *
61
+ * - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
62
+ * so `0`, negatives, and absurd values can never bypass the cap.
63
+ * - An absent / blank / non-numeric limit falls back to the mode default:
64
+ * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
65
+ *
66
+ * The return is never `undefined` — no ingress that routes its client limit
67
+ * through this can produce an unbounded read.
68
+ */
69
+ function resolveClientListLimit(rawLimit, opts = {}) {
70
+ const maxLimit = opts.maxLimit ?? 1e3;
71
+ if (rawLimit != null && String(rawLimit).trim() !== "") {
72
+ const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
73
+ if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
74
+ }
75
+ return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
76
+ }
58
77
  //#endregion
59
78
  //#region ../common/src/util/common.ts
60
79
  var DEFAULT_ONE_OF_TYPE = "type";
@@ -1007,6 +1026,35 @@ function camelCase(str) {
1007
1026
  });
1008
1027
  })))();
1009
1028
  /**
1029
+ * Segments that reach the prototype chain rather than a property of the object.
1030
+ *
1031
+ * The twin of this function in `@rebasepro/forms` could be made to write onto
1032
+ * `Object.prototype` through a path of `__proto__.x`. This copy survives the
1033
+ * write by accident — its `clone` always spreads into a fresh object, while the
1034
+ * form engine's has a "preserve class instances" branch that hands back
1035
+ * `Object.prototype` itself — but `getIn` still *reads* through the chain, and
1036
+ * handing back `Object.prototype` is how a polluted value is read out again.
1037
+ *
1038
+ * Closed on both sides here, so the two implementations agree.
1039
+ */
1040
+ var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
1041
+ "__proto__",
1042
+ "constructor",
1043
+ "prototype"
1044
+ ]);
1045
+ /**
1046
+ * Whether writing this single key with `obj[key] = …` would reach the prototype
1047
+ * chain instead of creating a property.
1048
+ *
1049
+ * The single-key counterpart of {@link pathTraversesPrototype}, for the many
1050
+ * places that copy an object one key at a time. `JSON.parse` creates
1051
+ * `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
1052
+ * `target[key] = value` invokes the setter and replaces the target's prototype.
1053
+ */
1054
+ function isPrototypePollutingKey(key) {
1055
+ return UNSAFE_PATH_SEGMENTS.has(key);
1056
+ }
1057
+ /**
1010
1058
  * Deep clone a value, preserving function references and class instances.
1011
1059
  * Unlike structuredClone, this handles objects that contain functions
1012
1060
  * (e.g. CollectionConfig with target(), childCollections(), callbacks).
@@ -1383,10 +1431,21 @@ function updateDateAutoValues({ inputValues, properties, status, timestampNowVal
1383
1431
  * have `id` and `path` fields — these are relation-shaped objects from
1384
1432
  * edge cases in the data pipeline (REST fallback, stale cache, custom data source).
1385
1433
  *
1434
+ * When `targetPath` is given, also accepts a bare id. A relation column is a
1435
+ * foreign key, and the REST layer returns it as the scalar it is; only some
1436
+ * fetch paths hydrate it into an object. Which form a caller sees therefore
1437
+ * depends on how the row was loaded, and a caller that only accepted objects
1438
+ * reported half of its own data as a type error. The declared target is the
1439
+ * missing half: with it, an id is a relation that has not been fetched yet.
1440
+ *
1386
1441
  * Returns null if the value cannot be coerced.
1387
1442
  */
1388
- function normalizeToEntityRelation(value, propertyType) {
1443
+ function normalizeToEntityRelation(value, propertyType, targetPath) {
1389
1444
  if (value instanceof EntityRelation) return value;
1445
+ if (targetPath && (typeof value === "string" || typeof value === "number")) {
1446
+ if (value === "") return null;
1447
+ return new EntityRelation(value, targetPath);
1448
+ }
1390
1449
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1391
1450
  const obj = value;
1392
1451
  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;
@@ -3308,6 +3367,46 @@ function resolveDataSource(collection, registry) {
3308
3367
  capabilities: getDataSourceCapabilities(engine)
3309
3368
  };
3310
3369
  }
3370
+ /**
3371
+ * Does a SQL toolchain own this collection's storage?
3372
+ *
3373
+ * "Owns the storage" means: something generates a table for it, pushes that
3374
+ * table to a database, plans its RLS policies, and reports it as drifted when
3375
+ * the two disagree. That is true of a Postgres collection and false of a
3376
+ * Firestore or MongoDB one, whose documents live in a store Rebase never
3377
+ * migrates — and the two were never told apart. Every stage of the SQL
3378
+ * toolchain took "the collections" to mean *all* of them, so a Firestore
3379
+ * collection declared next to the Postgres ones got a `pgTable` in the
3380
+ * generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
3381
+ * `db push` include list — where its name shielding a same-named real table
3382
+ * from Atlas's exclude list is the one that can lose data.
3383
+ *
3384
+ * The answer is the resolved engine's {@link DataSourceCapabilities}, not a
3385
+ * name check: an engine registered through `registerDataSourceCapabilities`
3386
+ * gets the same treatment as the built-in ones.
3387
+ *
3388
+ * Deliberately answers **true** for an engine nobody has heard of. Build-time
3389
+ * tooling (the CLI, the schema generator) has no data-source registry to
3390
+ * resolve a `dataSource` key against, so an unknown key resolves to an unknown
3391
+ * engine — and the cost of the two mistakes is not symmetric. Wrongly
3392
+ * including a collection generates a table nothing writes to; wrongly excluding
3393
+ * one silently stops generating a table the app is serving from. Declare
3394
+ * `engine` on a collection that is not SQL-backed and this is exact.
3395
+ */
3396
+ function isRelationalCollection(collection, registry) {
3397
+ return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
3398
+ }
3399
+ /**
3400
+ * The subset of `collections` a SQL toolchain owns — see
3401
+ * {@link isRelationalCollection}.
3402
+ *
3403
+ * Every stage that generates SQL from collections starts by calling this, so
3404
+ * the rule lives in one place rather than being re-decided per generator. It
3405
+ * keeps the input order.
3406
+ */
3407
+ function relationalCollections(collections, registry) {
3408
+ return collections.filter((collection) => isRelationalCollection(collection, registry));
3409
+ }
3311
3410
  //#endregion
3312
3411
  //#region ../common/src/collections/CollectionRegistry.ts
3313
3412
  var CollectionRegistry = class {
@@ -3660,6 +3759,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
3660
3759
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
3661
3760
  }
3662
3761
  };
3762
+ /**
3763
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
3764
+ *
3765
+ * Lives here, next to the walk, for the reason at the top of this file: every
3766
+ * transport has to mean the same thing by "page two". Four of them did not —
3767
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
3768
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
3769
+ * the published type documented a fourth number. Pages that overlap or skip
3770
+ * rows are the mildest of those outcomes.
3771
+ *
3772
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
3773
+ * is the value to hand a driver: it stays `undefined` when the caller named no
3774
+ * offset, because keyset pagination seeks with a `where` clause and must not
3775
+ * look like it is paging by offset.
3776
+ */
3777
+ function resolveFindWindow(params) {
3778
+ const limit = params?.limit ?? 50;
3779
+ const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
3780
+ return {
3781
+ limit,
3782
+ offset,
3783
+ driverOffset: params?.page != null ? offset : params?.offset
3784
+ };
3785
+ }
3663
3786
  function normalizePageSize(raw) {
3664
3787
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
3665
3788
  return Math.max(1, Math.floor(raw));
@@ -3955,21 +4078,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3955
4078
  const accessor = {
3956
4079
  async find(params) {
3957
4080
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3958
- const limit = params?.limit ?? 20;
3959
- const offset = params?.offset ?? 0;
4081
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3960
4082
  const fetchService = driver.restFetchService;
3961
4083
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3962
4084
  filter,
3963
- limit: params?.limit,
3964
- offset: params?.offset,
4085
+ logical: params?.logical,
4086
+ limit,
4087
+ offset: driverOffset,
3965
4088
  orderBy: params?.orderBy?.[0],
3966
4089
  order: params?.orderBy?.[1],
3967
4090
  searchString: params?.searchString
3968
4091
  }, params?.include) : await driver.fetchCollection({
3969
4092
  path: slug,
3970
- limit: params?.limit,
3971
- offset: params?.offset,
4093
+ limit,
4094
+ offset: driverOffset,
3972
4095
  filter,
4096
+ logical: params?.logical,
3973
4097
  orderBy: params?.orderBy?.[0],
3974
4098
  order: params?.orderBy?.[1],
3975
4099
  searchString: params?.searchString
@@ -3979,7 +4103,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3979
4103
  if (driver.count) {
3980
4104
  total = await driver.count({
3981
4105
  path: slug,
3982
- filter
4106
+ filter,
4107
+ logical: params?.logical,
4108
+ searchString: params?.searchString
3983
4109
  });
3984
4110
  hasMore = offset + rows.length < total;
3985
4111
  }
@@ -4031,22 +4157,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4031
4157
  values: {}
4032
4158
  } });
4033
4159
  },
4160
+ updateMany: driver.updateMany ? async (updates) => {
4161
+ return (await driver.updateMany({
4162
+ path: slug,
4163
+ updates: updates.map((u) => ({
4164
+ id: u.id,
4165
+ values: u.data
4166
+ }))
4167
+ })).map((row) => rowToEntity(row, slug, getPks()));
4168
+ } : void 0,
4169
+ deleteMany: driver.deleteMany ? async (ids) => {
4170
+ await driver.deleteMany({
4171
+ path: slug,
4172
+ ids
4173
+ });
4174
+ } : void 0,
4034
4175
  count: driver.count ? async (params) => {
4035
4176
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
4036
4177
  return driver.count({
4037
4178
  path: slug,
4038
- filter
4179
+ filter,
4180
+ logical: params?.logical,
4181
+ searchString: params?.searchString
4039
4182
  });
4040
4183
  } : void 0,
4041
4184
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
4042
- const limit = params?.limit ?? 20;
4043
- const offset = params?.offset ?? 0;
4185
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
4044
4186
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
4045
4187
  return driver.listenCollection({
4046
4188
  path: slug,
4047
- limit: params?.limit,
4048
- offset: params?.offset,
4189
+ limit,
4190
+ offset: driverOffset,
4049
4191
  filter: params?.where,
4192
+ logical: params?.logical,
4050
4193
  orderBy: params?.orderBy?.[0],
4051
4194
  order: params?.orderBy?.[1],
4052
4195
  searchString: params?.searchString,
@@ -4054,7 +4197,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
4054
4197
  onUpdate({
4055
4198
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
4056
4199
  meta: {
4057
- total: entities.length,
4200
+ total: offset + entities.length,
4058
4201
  limit,
4059
4202
  offset,
4060
4203
  hasMore: entities.length >= limit
@@ -4231,9 +4374,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
4231
4374
  async update(id, data) {
4232
4375
  return entityToRow(await snap.update(id, data));
4233
4376
  },
4377
+ async updateMany(updates) {
4378
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
4379
+ if (updates.length === 0) return [];
4380
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
4381
+ return (await snap.updateMany(updates.map((u) => ({
4382
+ id: u.id,
4383
+ data: u.data
4384
+ })))).map(entityToRow);
4385
+ },
4234
4386
  delete(id) {
4235
4387
  return snap.delete(id);
4236
4388
  },
4389
+ async deleteMany(ids) {
4390
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
4391
+ if (ids.length === 0) return;
4392
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
4393
+ await snap.deleteMany(ids);
4394
+ },
4237
4395
  count: snap.count ? (params) => snap.count(params) : void 0,
4238
4396
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
4239
4397
  data: res.data.map(entityToRow),
@@ -4294,6 +4452,6 @@ function buildSdkData(driver) {
4294
4452
  return wrapAsSdkData(buildRebaseData(driver));
4295
4453
  }
4296
4454
  //#endregion
4297
- export { toSnakeCase as A, normalizeToEntityRelation as C, getPolicyNamesForRule as D, legacyForeignKeyName as E, Vector as F, DEFAULT_ONE_OF_VALUE as M, hasForeignKeyOnTarget as N, mergeDeep as O, isManyToMany as P, createRelationRefWithData as S, generateForeignKeyName as T, buildCompositeId as _, getJunctionSecurityRules as a, parseIdValues as b, policyToPostgres as c, findRelation as d, getColumnName as f, resolveCollectionRelations as g, getTableVarName as h, getJunctionCollectionConfig as i, DEFAULT_ONE_OF_TYPE as j, camelCase as k, securityRuleToConditions as l, getTableName as m, CollectionRegistry as n, resolveJunctionSpecs as o, getEnumVarName as p, resolveStringColumnLength as r, getEffectiveSecurityRules as s, buildSdkData as t, findAnonymousGrants as u, getDeclaredPrimaryKeys as v, updateDateAutoValues as w, createRelationRef as x, isAddressableId as y };
4455
+ export { mergeDeep as A, createRelationRefWithData as C, legacyForeignKeyName as D, generateForeignKeyName as E, resolveClientListLimit as F, hasForeignKeyOnTarget as I, isManyToMany as L, toSnakeCase as M, DEFAULT_ONE_OF_TYPE as N, getPolicyNamesForRule as O, DEFAULT_ONE_OF_VALUE as P, Vector as R, createRelationRef as S, updateDateAutoValues as T, resolveCollectionRelations as _, getJunctionCollectionConfig as a, isAddressableId as b, getEffectiveSecurityRules as c, findAnonymousGrants as d, findRelation as f, getTableVarName as g, getTableName as h, resolveStringColumnLength as i, camelCase as j, isPrototypePollutingKey as k, policyToPostgres as l, getEnumVarName as m, CollectionRegistry as n, getJunctionSecurityRules as o, getColumnName as p, relationalCollections as r, resolveJunctionSpecs as s, buildSdkData as t, securityRuleToConditions as u, buildCompositeId as v, normalizeToEntityRelation as w, parseIdValues as x, getDeclaredPrimaryKeys as y };
4298
4456
 
4299
- //# sourceMappingURL=src-DlPBctw_.js.map
4457
+ //# sourceMappingURL=src-B0PGnKU3.js.map