@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.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 (30) hide show
  1. package/dist/PostgresBootstrapper.d.ts +8 -0
  2. package/dist/collections/buildRegistry.d.ts +1 -1
  3. package/dist/{ensure-collection-tables-DGMYK0fr.js → ensure-collection-tables-CNTcZGvn.js} +3 -3
  4. package/dist/{ensure-collection-tables-DGMYK0fr.js.map → ensure-collection-tables-CNTcZGvn.js.map} +1 -1
  5. package/dist/history/HistoryService.d.ts +9 -29
  6. package/dist/index.es.js +397 -53
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/schema/dynamic-tables.d.ts +1 -1
  9. package/dist/schema/introspect-runtime.d.ts +1 -1
  10. package/dist/services/FetchService.d.ts +36 -1
  11. package/dist/services/row-pipeline.d.ts +3 -1
  12. package/dist/{src-3VmUJ8Xn.js → src-BbFOPJ1S.js} +197 -18
  13. package/dist/src-BbFOPJ1S.js.map +1 -0
  14. package/dist/{src-D5xBTl32.js → src-Zqwaw3P5.js} +136 -90
  15. package/dist/src-Zqwaw3P5.js.map +1 -0
  16. package/dist/utils/drizzle-conditions.d.ts +157 -3
  17. package/dist/utils/pg-error-utils.d.ts +6 -3
  18. package/package.json +6 -6
  19. package/src/PostgresBootstrapper.ts +23 -6
  20. package/src/collections/buildRegistry.ts +1 -1
  21. package/src/history/HistoryService.ts +13 -31
  22. package/src/schema/dynamic-tables.ts +1 -1
  23. package/src/schema/generate-drizzle-schema-logic.ts +10 -2
  24. package/src/schema/introspect-runtime.ts +1 -1
  25. package/src/services/FetchService.ts +79 -11
  26. package/src/services/row-pipeline.ts +3 -1
  27. package/src/utils/drizzle-conditions.ts +509 -45
  28. package/src/utils/pg-error-utils.ts +52 -3
  29. package/dist/src-3VmUJ8Xn.js.map +0 -1
  30. package/dist/src-D5xBTl32.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Build drizzle tables at runtime from an introspected schema.
3
3
  *
4
- * CMS mode gets its drizzle tables from a generated `schema.generated.ts` that
4
+ * A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that
5
5
  * the developer commits. BaaS mode has no such file — it points at a database
6
6
  * and serves it — so the equivalent table objects are constructed here from
7
7
  * `information_schema` metadata.
@@ -7,7 +7,7 @@
7
7
  * single config file.
8
8
  *
9
9
  * Distinct from `introspect-db.ts`, which runs the same queries but emits
10
- * TypeScript *source* for a developer to edit and commit (CMS mode). The two
10
+ * TypeScript *source* for a developer to edit and commit (declared collections). The two
11
11
  * share the mapping helpers in `introspect-db-logic.ts` so a table is described
12
12
  * the same way whether it was generated or introspected.
13
13
  */
@@ -20,6 +20,32 @@ export declare class FetchService {
20
20
  * Safely narrows the DrizzleClient union type to access db.query[tableName].
21
21
  */
22
22
  private getQueryBuilder;
23
+ /**
24
+ * The context the condition builder needs to compile a filter key that is
25
+ * not a column name outright.
26
+ *
27
+ * Two such keys. An owning relation's key resolves through the collection's
28
+ * relations to its foreign-key column; a relation whose link lives on the
29
+ * target table or in a junction resolves to a correlated `EXISTS`, which
30
+ * needs the registry to reach that other table and this table's key column
31
+ * to correlate back.
32
+ *
33
+ * Looked up rather than passed: every read path already has the path, only
34
+ * some have the collection, and a path that names no registered collection
35
+ * (a nested/derived one) is not an error here — the builder simply falls
36
+ * back to guessing the default key shapes, and a relation filter it cannot
37
+ * compile stays unresolvable and so fails closed.
38
+ */
39
+ private filterContext;
40
+ /**
41
+ * The table column this collection's rows are keyed by, or `undefined`.
42
+ *
43
+ * `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
44
+ * resolvable key is not an error on the filter path — it only means the
45
+ * relation filters that would correlate on it cannot be compiled, which
46
+ * the builder already handles by failing that field closed.
47
+ */
48
+ private resolveIdColumn;
23
49
  /**
24
50
  * Build filter conditions from FilterValues
25
51
  * Delegates to DrizzleConditionBuilder.buildFilterConditions
@@ -28,6 +54,15 @@ export declare class FetchService {
28
54
  /**
29
55
  * Resolves the correct Drizzle column for sorting.
30
56
  * Automatically maps owning relation property keys to their underlying foreign key column.
57
+ *
58
+ * The relation's own `localKey` is the authority for that foreign key, not
59
+ * `<field>_id`. The default local key comes from `generateForeignKeyName`,
60
+ * which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
61
+ * `users` → `user_id` — and an author can override it outright. A wrong
62
+ * guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
63
+ * come back in whatever order Postgres pleases: paging over that repeats
64
+ * and skips rows rather than erroring. The guesses stay, last, for a
65
+ * caller that hands over no collection to resolve against.
31
66
  */
32
67
  private resolveOrderByField;
33
68
  /**
@@ -35,7 +70,7 @@ export declare class FetchService {
35
70
  * Converts collection relations to a Drizzle-compatible `with` object.
36
71
  *
37
72
  * When `include` is provided, only those relations are loaded.
38
- * When `include` is absent, ALL relations are loaded (CMS path).
73
+ * When `include` is absent, ALL relations are loaded (the admin path).
39
74
  *
40
75
  * Automatically detects many-to-many junction tables and nests
41
76
  * the target relation so actual row data is returned.
@@ -10,7 +10,9 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
10
10
  *
11
11
  * - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
12
12
  * target's values. This is what the admin renders.
13
- * - `"inline"` — the target's own columns, flat. This is what REST serves.
13
+ * - `"inline"` — the target's own columns, flat. This is what REST serves, and
14
+ * — since the in-process SDK reads through the same pipeline — what
15
+ * `rebase.data` / `context.data` serve too. A developer never sees a ref.
14
16
  *
15
17
  * They used to be two functions that happened to agree, and the agreement was
16
18
  * not enforced by anything: the row-identity bug had to be fixed five times
@@ -2,7 +2,7 @@ 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 "./chunk-DSJWtz9O.js";
5
- import { d as isPostgresCollectionConfig, f as NULL_OPS, h as EntityRelation, l as isManyToMany, m as toCanonicalOp, n as getDataSourceCapabilities, o as ANONYMOUS_USER_ID, p as REST_TO_CANONICAL, s as policy, u as getDeclaredSubcollections } from "./src-D5xBTl32.js";
5
+ import { a as policy, c as getDeclaredSubcollections, f as getDataSourceCapabilities, g as EntityRelation, h as toCanonicalOp, i as ANONYMOUS_USER_ID, l as isPostgresCollectionConfig, m as REST_TO_CANONICAL, p as NULL_OPS, s as isManyToMany, u as isRelationalCollectionConfig } from "./src-Zqwaw3P5.js";
6
6
  //#region ../common/src/util/common.ts
7
7
  var DEFAULT_ONE_OF_TYPE = "type";
8
8
  var DEFAULT_ONE_OF_VALUE = "value";
@@ -1264,7 +1264,7 @@ function traverseValueProperty(inputValue, property, operation) {
1264
1264
  return value;
1265
1265
  }
1266
1266
  /**
1267
- * Create a lightweight relation stub for CMS views.
1267
+ * Create a lightweight relation stub for admin views.
1268
1268
  * Replaces inline `{ id, path, __type: "relation" }` object literals.
1269
1269
  */
1270
1270
  function createRelationRef(id, path) {
@@ -1527,7 +1527,7 @@ var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
1527
1527
  function resolveCollectionRelations(collection) {
1528
1528
  const cached = _resolvedRelationsCache.get(collection);
1529
1529
  if (cached) return cached;
1530
- if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
1530
+ if (!isRelationalCollectionConfig(collection)) return {};
1531
1531
  const relations = {};
1532
1532
  for (const relation of collection.relations ?? []) {
1533
1533
  const resolved = resolveRelation(relation, collection);
@@ -1543,7 +1543,7 @@ function resolveCollectionRelations(collection) {
1543
1543
  return relations;
1544
1544
  }
1545
1545
  function getTableName(collection) {
1546
- if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1546
+ if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1547
1547
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1548
1548
  }
1549
1549
  function getTableVarName(tableName) {
@@ -2152,8 +2152,8 @@ function getIdPropertyName$1(collection) {
2152
2152
  * Collections that opt out via `disableDefaultPolicies` are returned unchanged.
2153
2153
  */
2154
2154
  function getEffectiveSecurityRules(collection) {
2155
- const explicit = [...(isPostgresCollectionConfig(collection) ? collection.securityRules : void 0) ?? []];
2156
- if (collection.disableDefaultPolicies) return explicit;
2155
+ const explicit = [...collection.securityRules ?? []];
2156
+ if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return explicit;
2157
2157
  const tableName = getTableName(collection);
2158
2158
  const injected = [];
2159
2159
  injected.push({
@@ -2324,7 +2324,7 @@ function coversUpdate(rule) {
2324
2324
  * stays locked (RLS is still enabled) until they write policies for it.
2325
2325
  */
2326
2326
  function getJunctionSecurityRules(spec) {
2327
- if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
2327
+ if (spec.declaringSides.every((side) => isPostgresCollectionConfig(side.collection) && side.collection.disableDefaultPolicies)) return [];
2328
2328
  const rules = [];
2329
2329
  rules.push({
2330
2330
  name: `${spec.table}_default_admin_read`,
@@ -3444,6 +3444,136 @@ var QueryBuilder = class {
3444
3444
  return this.collection.listen(this.params, onUpdate, onError);
3445
3445
  }
3446
3446
  };
3447
+ /** Rows `findAll()` will materialise before it refuses to continue. */
3448
+ var DEFAULT_FIND_ALL_MAX_ROWS = 1e4;
3449
+ /**
3450
+ * Requests one walk may make before it gives up on the server ever saying
3451
+ * `hasMore: false`. At the default page size that is two million rows — far
3452
+ * past any legitimate walk, and short of running forever.
3453
+ */
3454
+ var DEFAULT_MAX_PAGES = 1e4;
3455
+ /**
3456
+ * Thrown when a walk stops for a reason the caller needs to know about.
3457
+ *
3458
+ * Every one of these is a case where the alternative would be silent: a
3459
+ * truncated array that looks complete, or a loop that never returns. Check
3460
+ * {@link code} to tell them apart.
3461
+ */
3462
+ var RebasePaginationError = class RebasePaginationError extends Error {
3463
+ code;
3464
+ constructor(code, message) {
3465
+ super(message);
3466
+ this.name = "RebasePaginationError";
3467
+ this.code = code;
3468
+ Object.setPrototypeOf(this, RebasePaginationError.prototype);
3469
+ }
3470
+ };
3471
+ function normalizePageSize(raw) {
3472
+ if (raw === void 0 || !Number.isFinite(raw)) return 200;
3473
+ return Math.max(1, Math.floor(raw));
3474
+ }
3475
+ function normalizeMaxPages(raw) {
3476
+ if (raw === void 0) return DEFAULT_MAX_PAGES;
3477
+ if (raw === Number.POSITIVE_INFINITY) return raw;
3478
+ if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;
3479
+ return Math.max(1, Math.floor(raw));
3480
+ }
3481
+ function normalizeMaxRows(raw) {
3482
+ if (raw === void 0) return DEFAULT_FIND_ALL_MAX_ROWS;
3483
+ if (raw === Number.POSITIVE_INFINITY) return raw;
3484
+ if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;
3485
+ return Math.max(0, Math.floor(raw));
3486
+ }
3487
+ /**
3488
+ * Add one condition to a `where` map without disturbing what is already there.
3489
+ *
3490
+ * The caller's own filter on the cursor column has to survive — dropping it
3491
+ * would widen the query, which is the silent-filter-loss failure mode — so a
3492
+ * second condition on the same column becomes the array-of-tuples form that
3493
+ * `FindParams.where` already accepts, and both are AND-ed.
3494
+ */
3495
+ function appendCondition(where, column, condition) {
3496
+ const next = { ...where ?? {} };
3497
+ const existing = next[column];
3498
+ if (existing === void 0) next[column] = condition;
3499
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) next[column] = [...existing, condition];
3500
+ else next[column] = [existing, condition];
3501
+ return next;
3502
+ }
3503
+ function cursorEquals(a, b) {
3504
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
3505
+ return Object.is(a, b);
3506
+ }
3507
+ /**
3508
+ * Walk every row a query matches, yielding one row at a time and fetching the
3509
+ * next page only when the consumer asks for it.
3510
+ *
3511
+ * See {@link SDKCollectionClient.iterate} for the caller-facing contract,
3512
+ * including the offset-drift caveat and the `cursor` alternative.
3513
+ *
3514
+ * @param find the transport's single-page read
3515
+ * @param params `find()` parameters minus the window, plus the walk options
3516
+ * @param label the collection name, so an error says which walk failed
3517
+ */
3518
+ async function* paginateFind(find, params, label = "collection") {
3519
+ const { pageSize, cursor, maxPages, ...rest } = params ?? {};
3520
+ const findParams = { ...rest };
3521
+ const size = normalizePageSize(pageSize);
3522
+ const pageCap = normalizeMaxPages(maxPages);
3523
+ const cursorField = typeof cursor === "string" ? cursor : cursor?.field;
3524
+ const requestedDirection = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
3525
+ let direction = "asc";
3526
+ if (cursorField) {
3527
+ const orderBy = findParams.orderBy;
3528
+ if (orderBy && orderBy[0] !== cursorField) throw new RebasePaginationError("cursor-order-mismatch", `Cannot seek on "${cursorField}" while ordering "${label}" by "${orderBy[0]}": keyset pagination only advances along the column the query is sorted by. Order by "${cursorField}", or drop the cursor and page by offset.`);
3529
+ direction = requestedDirection ?? orderBy?.[1] ?? "asc";
3530
+ findParams.orderBy = [cursorField, direction];
3531
+ }
3532
+ const seekOp = direction === "desc" ? "<" : ">";
3533
+ const baseWhere = findParams.where;
3534
+ let offset = 0;
3535
+ let pages = 0;
3536
+ let cursorValue;
3537
+ let seeking = false;
3538
+ for (;;) {
3539
+ if (pages >= pageCap) throw new RebasePaginationError("max-pages", `Iterating "${label}" made ${pages} requests without the server reporting the end of the collection. Stopping rather than looping forever — raise \`maxPages\` if the walk is genuinely this long, or check that the backend sets \`meta.hasMore\`.`);
3540
+ const pageParams = {
3541
+ ...findParams,
3542
+ limit: size
3543
+ };
3544
+ if (cursorField) {
3545
+ if (seeking) pageParams.where = appendCondition(baseWhere, cursorField, [seekOp, cursorValue]);
3546
+ } else pageParams.offset = offset;
3547
+ const page = await find(pageParams);
3548
+ pages += 1;
3549
+ const rows = page?.data ?? [];
3550
+ if (rows.length === 0) return;
3551
+ for (const row of rows) yield row;
3552
+ if (page?.meta?.hasMore !== true) return;
3553
+ if (cursorField) {
3554
+ const nextValue = rows[rows.length - 1]?.[cursorField];
3555
+ if (nextValue === void 0 || nextValue === null) throw new RebasePaginationError("cursor-missing", `Cannot seek past the last row of "${label}": it has no value for the cursor column "${cursorField}". Pick a column that is present and non-null on every row.`);
3556
+ if (seeking && cursorEquals(nextValue, cursorValue)) throw new RebasePaginationError("cursor-stalled", `Iterating "${label}" is stuck: two pages in a row ended at ${cursorField}=${String(nextValue)}. The cursor column has to be unique — a repeated value cannot be seeked past, and continuing would either loop forever or skip the duplicates. Use the primary key, or page by offset.`);
3557
+ cursorValue = nextValue;
3558
+ seeking = true;
3559
+ } else offset += rows.length;
3560
+ }
3561
+ }
3562
+ /**
3563
+ * {@link paginateFind}, collected into an array under a ceiling.
3564
+ *
3565
+ * See {@link SDKCollectionClient.findAll}.
3566
+ */
3567
+ async function collectAllPages(find, params, label = "collection") {
3568
+ const { maxRows, ...rest } = params ?? {};
3569
+ const cap = normalizeMaxRows(maxRows);
3570
+ const out = [];
3571
+ for await (const row of paginateFind(find, rest, label)) {
3572
+ out.push(row);
3573
+ if (out.length > cap) throw new RebasePaginationError("max-rows", `findAll("${label}") matched more than ${cap} rows. Returning the first ${cap} would look like the whole answer and quietly not be one, so this throws instead. Raise \`maxRows\` if you meant to load them all, or stream with \`iterate()\`.`);
3574
+ }
3575
+ return out;
3576
+ }
3447
3577
  //#endregion
3448
3578
  //#region ../common/src/data/filter-dialect.ts
3449
3579
  /**
@@ -3593,6 +3723,42 @@ function rowToEntity(row, slug, primaryKeys = []) {
3593
3723
  values: row
3594
3724
  };
3595
3725
  }
3726
+ /**
3727
+ * The relation envelope `toCmsRow` writes where a relation was:
3728
+ * `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
3729
+ * admin's view-model, and the only pipeline that produces one is postgres'.
3730
+ */
3731
+ function isRelationEnvelope(value) {
3732
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.__type === "relation";
3733
+ }
3734
+ /** The target's own columns, as `toRestRow` would have inlined them. */
3735
+ function inlineEnvelope(envelope) {
3736
+ return envelope.data?.values ?? {};
3737
+ }
3738
+ /**
3739
+ * Replace every relation envelope on a row with the target's flat columns.
3740
+ *
3741
+ * The SDK serves one relation shape — the inlined one (see
3742
+ * {@link RestFetchService}) — and reads that come back through a *driver*
3743
+ * method rather than the REST pipeline still carry envelopes. Realtime is the
3744
+ * one such read left: there is no `listenForRest`, so the rows arrive shaped
3745
+ * for the admin and are flattened here instead.
3746
+ *
3747
+ * Only applied where the REST pipeline is the contract (see `find`); a driver
3748
+ * without a `restFetchService` keeps whatever it returns, so the admin's own
3749
+ * path through {@link buildRebaseData} is untouched.
3750
+ */
3751
+ function inlineRelationRefs(row) {
3752
+ let out;
3753
+ for (const [key, value] of Object.entries(row)) if (isRelationEnvelope(value)) {
3754
+ out = out ?? { ...row };
3755
+ out[key] = inlineEnvelope(value);
3756
+ } else if (Array.isArray(value) && value.some(isRelationEnvelope)) {
3757
+ out = out ?? { ...row };
3758
+ out[key] = value.map((item) => isRelationEnvelope(item) ? inlineEnvelope(item) : item);
3759
+ }
3760
+ return out ?? row;
3761
+ }
3596
3762
  function createDriverAccessor(driver, slug, getPks = () => []) {
3597
3763
  const accessor = {
3598
3764
  async find(params) {
@@ -3600,14 +3766,14 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3600
3766
  const limit = params?.limit ?? 20;
3601
3767
  const offset = params?.offset ?? 0;
3602
3768
  const fetchService = driver.restFetchService;
3603
- const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
3769
+ const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3604
3770
  filter,
3605
3771
  limit: params?.limit,
3606
3772
  offset: params?.offset,
3607
3773
  orderBy: params?.orderBy?.[0],
3608
3774
  order: params?.orderBy?.[1],
3609
3775
  searchString: params?.searchString
3610
- }, params.include) : await driver.fetchCollection({
3776
+ }, params?.include) : await driver.fetchCollection({
3611
3777
  path: slug,
3612
3778
  limit: params?.limit,
3613
3779
  offset: params?.offset,
@@ -3636,7 +3802,8 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3636
3802
  };
3637
3803
  },
3638
3804
  async findById(id) {
3639
- const row = await driver.fetchOne({
3805
+ const fetchService = driver.restFetchService;
3806
+ const row = fetchService ? await fetchService.fetchOneForRest(slug, id) : await driver.fetchOne({
3640
3807
  path: slug,
3641
3808
  id
3642
3809
  });
@@ -3682,6 +3849,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3682
3849
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
3683
3850
  const limit = params?.limit ?? 20;
3684
3851
  const offset = params?.offset ?? 0;
3852
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
3685
3853
  return driver.listenCollection({
3686
3854
  path: slug,
3687
3855
  limit: params?.limit,
@@ -3692,7 +3860,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3692
3860
  searchString: params?.searchString,
3693
3861
  onUpdate: (entities) => {
3694
3862
  onUpdate({
3695
- data: entities.map((row) => rowToEntity(row, slug, getPks())),
3863
+ data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
3696
3864
  meta: {
3697
3865
  total: entities.length,
3698
3866
  limit,
@@ -3705,10 +3873,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3705
3873
  });
3706
3874
  } : void 0,
3707
3875
  listenById: driver.listenOne ? (id, onUpdate, onError) => {
3876
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
3708
3877
  return driver.listenOne({
3709
3878
  path: slug,
3710
3879
  id,
3711
- onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug, getPks()) : void 0),
3880
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks()) : void 0),
3712
3881
  onError
3713
3882
  });
3714
3883
  } : void 0,
@@ -3839,7 +4008,7 @@ var SdkQueryBuilder = class {
3839
4008
  * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
3840
4009
  * so the backend SDK is byte-for-byte the same shape as the frontend client.
3841
4010
  */
3842
- function toSdkCollectionClient(snap) {
4011
+ function toSdkCollectionClient(snap, slug = "collection") {
3843
4012
  const client = {
3844
4013
  async find(params) {
3845
4014
  const res = await snap.find(params);
@@ -3848,6 +4017,12 @@ function toSdkCollectionClient(snap) {
3848
4017
  meta: res.meta
3849
4018
  };
3850
4019
  },
4020
+ iterate(params) {
4021
+ return paginateFind((p) => client.find(p), params, slug);
4022
+ },
4023
+ findAll(params) {
4024
+ return collectAllPages((p) => client.find(p), params, slug);
4025
+ },
3851
4026
  async findById(id) {
3852
4027
  const s = await snap.findById(id);
3853
4028
  return s ? entityToRow(s) : void 0;
@@ -3899,7 +4074,7 @@ function wrapAsSdkData(entityData) {
3899
4074
  function getAccessor(slug) {
3900
4075
  let accessor = cache.get(slug);
3901
4076
  if (!accessor) {
3902
- accessor = toSdkCollectionClient(entityData.collection(slug));
4077
+ accessor = toSdkCollectionClient(entityData.collection(slug), slug);
3903
4078
  cache.set(slug, accessor);
3904
4079
  }
3905
4080
  return accessor;
@@ -3916,8 +4091,12 @@ function wrapAsSdkData(entityData) {
3916
4091
  *
3917
4092
  * This is the developer-facing SDK data layer used by backend framework
3918
4093
  * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
3919
- * identical in shape to the frontend SDK client so the API is symmetric
3920
- * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
4094
+ * identical in shape to the frontend SDK client, down to how a relation is
4095
+ * served: a foreign key stays a foreign key, and a relation named in `include`
4096
+ * arrives as the target's own columns. The `{ __type: "relation" }` envelope is
4097
+ * the admin's view-model and never reaches here.
4098
+ *
4099
+ * The admin uses {@link buildRebaseData} (Entity) over its own driver.
3921
4100
  */
3922
4101
  function buildSdkData(driver) {
3923
4102
  return wrapAsSdkData(buildRebaseData(driver));
@@ -3989,6 +4168,6 @@ async function detectJunctionTables(executeSql) {
3989
4168
  return junctionTables;
3990
4169
  }
3991
4170
  //#endregion
3992
- export { DEFAULT_ONE_OF_TYPE as A, createRelationRefWithData as C, mergeDeep as D, getPolicyNamesForRule as E, camelCase as O, createRelationRef as S, updateDateAutoValues as T, getTableVarName as _, getJunctionCollectionConfig as a, getDeclaredPrimaryKeys as b, getEffectiveSecurityRules as c, securityRuleToConditions as d, findAnonymousGrants as f, getTableName as g, getEnumVarName as h, CollectionRegistry as i, DEFAULT_ONE_OF_VALUE as j, toSnakeCase as k, buildPropertyCallbacks as l, getColumnName as m, detectJunctionTables as n, getJunctionSecurityRules as o, findRelation as p, buildSdkData as r, resolveJunctionSpecs as s, classifyTable as t, policyToPostgres as u, resolveCollectionRelations as v, normalizeToEntityRelation as w, parseIdValues as x, buildCompositeId as y };
4171
+ export { toSnakeCase as A, createRelationRefWithData as C, getPolicyNamesForRule as D, generateForeignKeyName as E, DEFAULT_ONE_OF_VALUE as M, mergeDeep as O, createRelationRef as S, updateDateAutoValues as T, getTableVarName as _, getJunctionCollectionConfig as a, getDeclaredPrimaryKeys as b, getEffectiveSecurityRules as c, securityRuleToConditions as d, findAnonymousGrants as f, getTableName as g, getEnumVarName as h, CollectionRegistry as i, DEFAULT_ONE_OF_TYPE as j, camelCase as k, buildPropertyCallbacks as l, getColumnName as m, detectJunctionTables as n, getJunctionSecurityRules as o, findRelation as p, buildSdkData as r, resolveJunctionSpecs as s, classifyTable as t, policyToPostgres as u, resolveCollectionRelations as v, normalizeToEntityRelation as w, parseIdValues as x, buildCompositeId as y };
3993
4172
 
3994
- //# sourceMappingURL=src-3VmUJ8Xn.js.map
4173
+ //# sourceMappingURL=src-BbFOPJ1S.js.map