@rebasepro/server 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.
@@ -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 { _ as toCanonicalOp, c as policy, d as getDeclaredSubcollections, g as REST_TO_CANONICAL, h as NULL_OPS, m as CANONICAL_TO_REST, o as getDataSourceCapabilities, y as EntityRelation } from "./src-BYbxB4PR.js";
5
+ import { S as toCanonicalOp, b as NULL_OPS, f as getDeclaredSubcollections, g as getDataSourceCapabilities, l as policy, m as isRelationalCollectionConfig, w as EntityRelation, x as REST_TO_CANONICAL, y as CANONICAL_TO_REST } from "./src-Ivjud8jD.js";
6
6
  (/* @__PURE__ */ __commonJSMin(((exports, module) => {
7
7
  (function(e) {
8
8
  var t;
@@ -1270,7 +1270,7 @@ var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
1270
1270
  function resolveCollectionRelations(collection) {
1271
1271
  const cached = _resolvedRelationsCache.get(collection);
1272
1272
  if (cached) return cached;
1273
- if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
1273
+ if (!isRelationalCollectionConfig(collection)) return {};
1274
1274
  const relations = {};
1275
1275
  for (const relation of collection.relations ?? []) {
1276
1276
  const resolved = resolveRelation(relation, collection);
@@ -1286,7 +1286,7 @@ function resolveCollectionRelations(collection) {
1286
1286
  return relations;
1287
1287
  }
1288
1288
  function getTableName(collection) {
1289
- if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1289
+ if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1290
1290
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
1291
1291
  }
1292
1292
  /**
@@ -2451,6 +2451,136 @@ var QueryBuilder = class {
2451
2451
  return this.collection.listen(this.params, onUpdate, onError);
2452
2452
  }
2453
2453
  };
2454
+ /** Rows `findAll()` will materialise before it refuses to continue. */
2455
+ var DEFAULT_FIND_ALL_MAX_ROWS = 1e4;
2456
+ /**
2457
+ * Requests one walk may make before it gives up on the server ever saying
2458
+ * `hasMore: false`. At the default page size that is two million rows — far
2459
+ * past any legitimate walk, and short of running forever.
2460
+ */
2461
+ var DEFAULT_MAX_PAGES = 1e4;
2462
+ /**
2463
+ * Thrown when a walk stops for a reason the caller needs to know about.
2464
+ *
2465
+ * Every one of these is a case where the alternative would be silent: a
2466
+ * truncated array that looks complete, or a loop that never returns. Check
2467
+ * {@link code} to tell them apart.
2468
+ */
2469
+ var RebasePaginationError = class RebasePaginationError extends Error {
2470
+ code;
2471
+ constructor(code, message) {
2472
+ super(message);
2473
+ this.name = "RebasePaginationError";
2474
+ this.code = code;
2475
+ Object.setPrototypeOf(this, RebasePaginationError.prototype);
2476
+ }
2477
+ };
2478
+ function normalizePageSize(raw) {
2479
+ if (raw === void 0 || !Number.isFinite(raw)) return 200;
2480
+ return Math.max(1, Math.floor(raw));
2481
+ }
2482
+ function normalizeMaxPages(raw) {
2483
+ if (raw === void 0) return DEFAULT_MAX_PAGES;
2484
+ if (raw === Number.POSITIVE_INFINITY) return raw;
2485
+ if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;
2486
+ return Math.max(1, Math.floor(raw));
2487
+ }
2488
+ function normalizeMaxRows(raw) {
2489
+ if (raw === void 0) return DEFAULT_FIND_ALL_MAX_ROWS;
2490
+ if (raw === Number.POSITIVE_INFINITY) return raw;
2491
+ if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;
2492
+ return Math.max(0, Math.floor(raw));
2493
+ }
2494
+ /**
2495
+ * Add one condition to a `where` map without disturbing what is already there.
2496
+ *
2497
+ * The caller's own filter on the cursor column has to survive — dropping it
2498
+ * would widen the query, which is the silent-filter-loss failure mode — so a
2499
+ * second condition on the same column becomes the array-of-tuples form that
2500
+ * `FindParams.where` already accepts, and both are AND-ed.
2501
+ */
2502
+ function appendCondition(where, column, condition) {
2503
+ const next = { ...where ?? {} };
2504
+ const existing = next[column];
2505
+ if (existing === void 0) next[column] = condition;
2506
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) next[column] = [...existing, condition];
2507
+ else next[column] = [existing, condition];
2508
+ return next;
2509
+ }
2510
+ function cursorEquals(a, b) {
2511
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
2512
+ return Object.is(a, b);
2513
+ }
2514
+ /**
2515
+ * Walk every row a query matches, yielding one row at a time and fetching the
2516
+ * next page only when the consumer asks for it.
2517
+ *
2518
+ * See {@link SDKCollectionClient.iterate} for the caller-facing contract,
2519
+ * including the offset-drift caveat and the `cursor` alternative.
2520
+ *
2521
+ * @param find the transport's single-page read
2522
+ * @param params `find()` parameters minus the window, plus the walk options
2523
+ * @param label the collection name, so an error says which walk failed
2524
+ */
2525
+ async function* paginateFind(find, params, label = "collection") {
2526
+ const { pageSize, cursor, maxPages, ...rest } = params ?? {};
2527
+ const findParams = { ...rest };
2528
+ const size = normalizePageSize(pageSize);
2529
+ const pageCap = normalizeMaxPages(maxPages);
2530
+ const cursorField = typeof cursor === "string" ? cursor : cursor?.field;
2531
+ const requestedDirection = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
2532
+ let direction = "asc";
2533
+ if (cursorField) {
2534
+ const orderBy = findParams.orderBy;
2535
+ 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.`);
2536
+ direction = requestedDirection ?? orderBy?.[1] ?? "asc";
2537
+ findParams.orderBy = [cursorField, direction];
2538
+ }
2539
+ const seekOp = direction === "desc" ? "<" : ">";
2540
+ const baseWhere = findParams.where;
2541
+ let offset = 0;
2542
+ let pages = 0;
2543
+ let cursorValue;
2544
+ let seeking = false;
2545
+ for (;;) {
2546
+ 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\`.`);
2547
+ const pageParams = {
2548
+ ...findParams,
2549
+ limit: size
2550
+ };
2551
+ if (cursorField) {
2552
+ if (seeking) pageParams.where = appendCondition(baseWhere, cursorField, [seekOp, cursorValue]);
2553
+ } else pageParams.offset = offset;
2554
+ const page = await find(pageParams);
2555
+ pages += 1;
2556
+ const rows = page?.data ?? [];
2557
+ if (rows.length === 0) return;
2558
+ for (const row of rows) yield row;
2559
+ if (page?.meta?.hasMore !== true) return;
2560
+ if (cursorField) {
2561
+ const nextValue = rows[rows.length - 1]?.[cursorField];
2562
+ 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.`);
2563
+ 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.`);
2564
+ cursorValue = nextValue;
2565
+ seeking = true;
2566
+ } else offset += rows.length;
2567
+ }
2568
+ }
2569
+ /**
2570
+ * {@link paginateFind}, collected into an array under a ceiling.
2571
+ *
2572
+ * See {@link SDKCollectionClient.findAll}.
2573
+ */
2574
+ async function collectAllPages(find, params, label = "collection") {
2575
+ const { maxRows, ...rest } = params ?? {};
2576
+ const cap = normalizeMaxRows(maxRows);
2577
+ const out = [];
2578
+ for await (const row of paginateFind(find, rest, label)) {
2579
+ out.push(row);
2580
+ 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()\`.`);
2581
+ }
2582
+ return out;
2583
+ }
2454
2584
  //#endregion
2455
2585
  //#region ../common/src/data/filter-dialect.ts
2456
2586
  /**
@@ -2754,6 +2884,42 @@ function rowToEntity(row, slug, primaryKeys = []) {
2754
2884
  values: row
2755
2885
  };
2756
2886
  }
2887
+ /**
2888
+ * The relation envelope `toCmsRow` writes where a relation was:
2889
+ * `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
2890
+ * admin's view-model, and the only pipeline that produces one is postgres'.
2891
+ */
2892
+ function isRelationEnvelope(value) {
2893
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.__type === "relation";
2894
+ }
2895
+ /** The target's own columns, as `toRestRow` would have inlined them. */
2896
+ function inlineEnvelope(envelope) {
2897
+ return envelope.data?.values ?? {};
2898
+ }
2899
+ /**
2900
+ * Replace every relation envelope on a row with the target's flat columns.
2901
+ *
2902
+ * The SDK serves one relation shape — the inlined one (see
2903
+ * {@link RestFetchService}) — and reads that come back through a *driver*
2904
+ * method rather than the REST pipeline still carry envelopes. Realtime is the
2905
+ * one such read left: there is no `listenForRest`, so the rows arrive shaped
2906
+ * for the admin and are flattened here instead.
2907
+ *
2908
+ * Only applied where the REST pipeline is the contract (see `find`); a driver
2909
+ * without a `restFetchService` keeps whatever it returns, so the admin's own
2910
+ * path through {@link buildRebaseData} is untouched.
2911
+ */
2912
+ function inlineRelationRefs(row) {
2913
+ let out;
2914
+ for (const [key, value] of Object.entries(row)) if (isRelationEnvelope(value)) {
2915
+ out = out ?? { ...row };
2916
+ out[key] = inlineEnvelope(value);
2917
+ } else if (Array.isArray(value) && value.some(isRelationEnvelope)) {
2918
+ out = out ?? { ...row };
2919
+ out[key] = value.map((item) => isRelationEnvelope(item) ? inlineEnvelope(item) : item);
2920
+ }
2921
+ return out ?? row;
2922
+ }
2757
2923
  function createDriverAccessor(driver, slug, getPks = () => []) {
2758
2924
  const accessor = {
2759
2925
  async find(params) {
@@ -2761,14 +2927,14 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2761
2927
  const limit = params?.limit ?? 20;
2762
2928
  const offset = params?.offset ?? 0;
2763
2929
  const fetchService = driver.restFetchService;
2764
- const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
2930
+ const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
2765
2931
  filter,
2766
2932
  limit: params?.limit,
2767
2933
  offset: params?.offset,
2768
2934
  orderBy: params?.orderBy?.[0],
2769
2935
  order: params?.orderBy?.[1],
2770
2936
  searchString: params?.searchString
2771
- }, params.include) : await driver.fetchCollection({
2937
+ }, params?.include) : await driver.fetchCollection({
2772
2938
  path: slug,
2773
2939
  limit: params?.limit,
2774
2940
  offset: params?.offset,
@@ -2797,7 +2963,8 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2797
2963
  };
2798
2964
  },
2799
2965
  async findById(id) {
2800
- const row = await driver.fetchOne({
2966
+ const fetchService = driver.restFetchService;
2967
+ const row = fetchService ? await fetchService.fetchOneForRest(slug, id) : await driver.fetchOne({
2801
2968
  path: slug,
2802
2969
  id
2803
2970
  });
@@ -2843,6 +3010,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2843
3010
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
2844
3011
  const limit = params?.limit ?? 20;
2845
3012
  const offset = params?.offset ?? 0;
3013
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
2846
3014
  return driver.listenCollection({
2847
3015
  path: slug,
2848
3016
  limit: params?.limit,
@@ -2853,7 +3021,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2853
3021
  searchString: params?.searchString,
2854
3022
  onUpdate: (entities) => {
2855
3023
  onUpdate({
2856
- data: entities.map((row) => rowToEntity(row, slug, getPks())),
3024
+ data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
2857
3025
  meta: {
2858
3026
  total: entities.length,
2859
3027
  limit,
@@ -2866,10 +3034,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2866
3034
  });
2867
3035
  } : void 0,
2868
3036
  listenById: driver.listenOne ? (id, onUpdate, onError) => {
3037
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
2869
3038
  return driver.listenOne({
2870
3039
  path: slug,
2871
3040
  id,
2872
- onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug, getPks()) : void 0),
3041
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks()) : void 0),
2873
3042
  onError
2874
3043
  });
2875
3044
  } : void 0,
@@ -3000,7 +3169,7 @@ var SdkQueryBuilder = class {
3000
3169
  * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
3001
3170
  * so the backend SDK is byte-for-byte the same shape as the frontend client.
3002
3171
  */
3003
- function toSdkCollectionClient(snap) {
3172
+ function toSdkCollectionClient(snap, slug = "collection") {
3004
3173
  const client = {
3005
3174
  async find(params) {
3006
3175
  const res = await snap.find(params);
@@ -3009,6 +3178,12 @@ function toSdkCollectionClient(snap) {
3009
3178
  meta: res.meta
3010
3179
  };
3011
3180
  },
3181
+ iterate(params) {
3182
+ return paginateFind((p) => client.find(p), params, slug);
3183
+ },
3184
+ findAll(params) {
3185
+ return collectAllPages((p) => client.find(p), params, slug);
3186
+ },
3012
3187
  async findById(id) {
3013
3188
  const s = await snap.findById(id);
3014
3189
  return s ? entityToRow(s) : void 0;
@@ -3060,7 +3235,7 @@ function wrapAsSdkData(entityData) {
3060
3235
  function getAccessor(slug) {
3061
3236
  let accessor = cache.get(slug);
3062
3237
  if (!accessor) {
3063
- accessor = toSdkCollectionClient(entityData.collection(slug));
3238
+ accessor = toSdkCollectionClient(entityData.collection(slug), slug);
3064
3239
  cache.set(slug, accessor);
3065
3240
  }
3066
3241
  return accessor;
@@ -3077,8 +3252,12 @@ function wrapAsSdkData(entityData) {
3077
3252
  *
3078
3253
  * This is the developer-facing SDK data layer used by backend framework
3079
3254
  * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
3080
- * identical in shape to the frontend SDK client so the API is symmetric
3081
- * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
3255
+ * identical in shape to the frontend SDK client, down to how a relation is
3256
+ * served: a foreign key stays a foreign key, and a relation named in `include`
3257
+ * arrives as the target's own columns. The `{ __type: "relation" }` envelope is
3258
+ * the admin's view-model and never reaches here.
3259
+ *
3260
+ * The admin uses {@link buildRebaseData} (Entity) over its own driver.
3082
3261
  */
3083
3262
  function buildSdkData(driver) {
3084
3263
  return wrapAsSdkData(buildRebaseData(driver));
@@ -3178,6 +3357,6 @@ function deserializeOrderBy(raw) {
3178
3357
  return [raw.slice(0, idx), raw.slice(idx + 1) === "desc" ? "desc" : "asc"];
3179
3358
  }
3180
3359
  //#endregion
3181
- export { deserializeFilter as a, serializeLogicalCondition as c, resolveDataSource as d, findRelation as f, toSnakeCase as h, buildSdkData as i, CollectionRegistry as l, buildCompositeId as m, serializeOrderBy as n, deserializeLogicalCondition as o, resolveCollectionRelations as p, buildRoutedRebaseData as r, serializeFilter as s, deserializeOrderBy as t, createDataSourceRegistry as u };
3360
+ export { toSnakeCase as _, deserializeFilter as a, serializeLogicalCondition as c, CollectionRegistry as d, createDataSourceRegistry as f, buildCompositeId as g, resolveCollectionRelations as h, buildSdkData as i, collectAllPages as l, findRelation as m, serializeOrderBy as n, deserializeLogicalCondition as o, resolveDataSource as p, buildRoutedRebaseData as r, serializeFilter as s, deserializeOrderBy as t, paginateFind as u };
3182
3361
 
3183
- //# sourceMappingURL=src-q6_elgGZ.js.map
3362
+ //# sourceMappingURL=src-CoOAMnBh.js.map