@rebasepro/common 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.
package/dist/index.es.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
2
- import { deepClone, generateForeignKeyName, getIn, getPolicyOperations, isDefaultFieldConfigId, mergeDeep, randomString, removeFunctions, toSnakeCase } from "@rebasepro/utils";
1
+ import { ANONYMOUS_USER_ID, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
2
+ import { deepClone, generateForeignKeyName, getIn, 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";
5
5
  //#region src/util/common.ts
@@ -142,7 +142,7 @@ function traverseValueProperty(inputValue, property, operation) {
142
142
  return value;
143
143
  }
144
144
  /**
145
- * Create a lightweight relation stub for CMS views.
145
+ * Create a lightweight relation stub for admin views.
146
146
  * Replaces inline `{ id, path, __type: "relation" }` object literals.
147
147
  */
148
148
  function createRelationRef(id, path) {
@@ -494,7 +494,7 @@ var _resolvedRelationsCache = /* @__PURE__ */ new WeakMap();
494
494
  function resolveCollectionRelations(collection) {
495
495
  const cached = _resolvedRelationsCache.get(collection);
496
496
  if (cached) return cached;
497
- if (!getDataSourceCapabilities(collection.engine).supportsRelations) return {};
497
+ if (!isRelationalCollectionConfig(collection)) return {};
498
498
  const relations = {};
499
499
  for (const relation of collection.relations ?? []) {
500
500
  const resolved = resolveRelation(relation, collection);
@@ -510,7 +510,7 @@ function resolveCollectionRelations(collection) {
510
510
  return relations;
511
511
  }
512
512
  function getTableName(collection) {
513
- if (getDataSourceCapabilities(collection.engine).supportsRelations) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
513
+ if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
514
514
  return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
515
515
  }
516
516
  function getTableVarName(tableName) {
@@ -1606,8 +1606,8 @@ function getIdPropertyName$1(collection) {
1606
1606
  * Collections that opt out via `disableDefaultPolicies` are returned unchanged.
1607
1607
  */
1608
1608
  function getEffectiveSecurityRules(collection) {
1609
- const explicit = [...(isPostgresCollectionConfig(collection) ? collection.securityRules : void 0) ?? []];
1610
- if (collection.disableDefaultPolicies) return explicit;
1609
+ const explicit = [...collection.securityRules ?? []];
1610
+ if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return explicit;
1611
1611
  const tableName = getTableName(collection);
1612
1612
  const injected = [];
1613
1613
  injected.push({
@@ -1649,8 +1649,8 @@ function getEffectiveSecurityRules(collection) {
1649
1649
  * DDL, which policies are injected and how to take them off.
1650
1650
  */
1651
1651
  function getInjectedSecurityRules(collection) {
1652
- if (collection.disableDefaultPolicies) return [];
1653
- const explicitCount = ((isPostgresCollectionConfig(collection) ? collection.securityRules : void 0) ?? []).length;
1652
+ if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) return [];
1653
+ const explicitCount = (collection.securityRules ?? []).length;
1654
1654
  return getEffectiveSecurityRules(collection).slice(explicitCount);
1655
1655
  }
1656
1656
  //#endregion
@@ -1794,7 +1794,7 @@ function coversUpdate(rule) {
1794
1794
  * stays locked (RLS is still enabled) until they write policies for it.
1795
1795
  */
1796
1796
  function getJunctionSecurityRules(spec) {
1797
- if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
1797
+ if (spec.declaringSides.every((side) => isPostgresCollectionConfig(side.collection) && side.collection.disableDefaultPolicies)) return [];
1798
1798
  const rules = [];
1799
1799
  rules.push({
1800
1800
  name: `${spec.table}_default_admin_read`,
@@ -1947,6 +1947,243 @@ function buildConditionContext(params) {
1947
1947
  * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
1948
1948
  */
1949
1949
  //#endregion
1950
+ //#region src/util/pg-column-to-property.ts
1951
+ /**
1952
+ * Maps a PostgreSQL column data type to a Rebase property type.
1953
+ */
1954
+ function pgTypeToRebaseProperty(column) {
1955
+ const { column_name, data_type, udt_name, is_nullable, column_default, enum_values } = column;
1956
+ const required = is_nullable === "NO";
1957
+ const prettifiedName = prettifyIdentifier(column_name);
1958
+ const isAutoId = column_default != null && (column_default.includes("nextval") || column_default.includes("gen_random_uuid") || column_default.includes("uuid_generate") || column_default.includes("identity"));
1959
+ if (data_type === "USER-DEFINED" && enum_values && enum_values.length > 0) return {
1960
+ type: "string",
1961
+ name: prettifiedName,
1962
+ enum: enum_values.map((v) => ({
1963
+ id: v,
1964
+ label: prettifyIdentifier(v)
1965
+ })),
1966
+ validation: required ? { required: true } : void 0
1967
+ };
1968
+ const dt = data_type.toLowerCase();
1969
+ switch (dt) {
1970
+ case "character varying":
1971
+ case "varchar":
1972
+ case "text":
1973
+ case "char":
1974
+ case "character":
1975
+ case "citext": {
1976
+ let colType = "varchar";
1977
+ if (dt === "text" || dt === "citext") colType = "text";
1978
+ if (dt === "char" || dt === "character") colType = "char";
1979
+ const prop = {
1980
+ type: "string",
1981
+ name: prettifiedName,
1982
+ columnType: colType,
1983
+ validation: required ? { required: true } : void 0
1984
+ };
1985
+ if (isAutoId) prop.isId = "manual";
1986
+ return prop;
1987
+ }
1988
+ case "uuid": {
1989
+ const prop = {
1990
+ type: "string",
1991
+ name: prettifiedName,
1992
+ validation: required ? { required: true } : void 0
1993
+ };
1994
+ if (isAutoId) prop.isId = "uuid";
1995
+ return prop;
1996
+ }
1997
+ case "integer":
1998
+ case "bigint":
1999
+ case "smallint": {
2000
+ const prop = {
2001
+ type: "number",
2002
+ name: prettifiedName,
2003
+ columnType: dt === "bigint" ? "bigint" : "integer",
2004
+ validation: {
2005
+ ...required ? { required: true } : {},
2006
+ integer: true
2007
+ }
2008
+ };
2009
+ if (isAutoId) prop.isId = "increment";
2010
+ return prop;
2011
+ }
2012
+ case "serial":
2013
+ case "bigserial":
2014
+ case "smallserial": return {
2015
+ type: "number",
2016
+ name: prettifiedName,
2017
+ columnType: dt === "bigserial" ? "bigserial" : "serial",
2018
+ isId: "increment",
2019
+ validation: {
2020
+ ...required ? { required: true } : {},
2021
+ integer: true
2022
+ }
2023
+ };
2024
+ case "numeric":
2025
+ case "decimal":
2026
+ case "real":
2027
+ case "double precision": {
2028
+ let colType = "numeric";
2029
+ if (dt === "real") colType = "real";
2030
+ if (dt === "double precision") colType = "double precision";
2031
+ return {
2032
+ type: "number",
2033
+ name: prettifiedName,
2034
+ columnType: colType,
2035
+ validation: required ? { required: true } : void 0
2036
+ };
2037
+ }
2038
+ case "boolean": return {
2039
+ type: "boolean",
2040
+ name: prettifiedName,
2041
+ validation: required ? { required: true } : void 0
2042
+ };
2043
+ case "timestamp with time zone":
2044
+ case "timestamp without time zone":
2045
+ case "timestamp":
2046
+ case "timestamptz":
2047
+ case "date":
2048
+ case "time with time zone":
2049
+ case "time without time zone":
2050
+ case "time": {
2051
+ let colType = "timestamp";
2052
+ if (dt.startsWith("date")) colType = "date";
2053
+ if (dt.startsWith("time ") || dt === "time") colType = "time";
2054
+ return {
2055
+ type: "date",
2056
+ name: prettifiedName,
2057
+ columnType: colType,
2058
+ validation: required ? { required: true } : void 0
2059
+ };
2060
+ }
2061
+ case "jsonb":
2062
+ case "json": return {
2063
+ type: "map",
2064
+ name: prettifiedName,
2065
+ columnType: dt === "jsonb" ? "jsonb" : "json",
2066
+ keyValue: true,
2067
+ properties: {}
2068
+ };
2069
+ case "array":
2070
+ case "ARRAY": {
2071
+ let innerType = "string";
2072
+ let colType = void 0;
2073
+ if (udt_name === "_text" || udt_name === "_varchar") {
2074
+ innerType = "string";
2075
+ colType = "text[]";
2076
+ } else if (udt_name === "_int4" || udt_name === "_int2" || udt_name === "_int8") {
2077
+ innerType = "number";
2078
+ colType = "integer[]";
2079
+ } else if (udt_name === "_bool") {
2080
+ innerType = "boolean";
2081
+ colType = "boolean[]";
2082
+ } else if (udt_name === "_numeric") {
2083
+ innerType = "number";
2084
+ colType = "numeric[]";
2085
+ }
2086
+ return {
2087
+ type: "array",
2088
+ name: prettifiedName,
2089
+ columnType: colType,
2090
+ of: { type: innerType }
2091
+ };
2092
+ }
2093
+ default: return {
2094
+ type: "string",
2095
+ name: prettifiedName,
2096
+ validation: required ? { required: true } : void 0
2097
+ };
2098
+ }
2099
+ }
2100
+ /**
2101
+ * Builds a collection description from PostgreSQL table metadata.
2102
+ * This is used when creating a new collection from an existing database table.
2103
+ */
2104
+ function buildCollectionFromTableMetadata(tableName, metadata) {
2105
+ const properties = {};
2106
+ const propertiesOrder = [];
2107
+ const relations = [];
2108
+ const securityRules = [];
2109
+ for (const column of metadata.columns) {
2110
+ const property = pgTypeToRebaseProperty(column);
2111
+ if (property) {
2112
+ const propRecord = property;
2113
+ Object.keys(propRecord).forEach((key) => propRecord[key] === void 0 && delete propRecord[key]);
2114
+ properties[column.column_name] = property;
2115
+ propertiesOrder.push(column.column_name);
2116
+ }
2117
+ }
2118
+ if (metadata.foreignKeys) for (const fk of metadata.foreignKeys) {
2119
+ const relName = fk.column_name.endsWith("_id") ? fk.column_name.substring(0, fk.column_name.length - 3) : fk.column_name;
2120
+ relations.push({
2121
+ id: fk.column_name,
2122
+ relationName: relName,
2123
+ target: fk.foreign_table_name,
2124
+ kind: "belongsTo",
2125
+ localKey: fk.column_name
2126
+ });
2127
+ }
2128
+ if (metadata.junctions) for (const junction of metadata.junctions) {
2129
+ const relName = junction.target_table_name;
2130
+ relations.push({
2131
+ id: junction.target_table_name + "_relation",
2132
+ relationName: relName,
2133
+ target: junction.target_table_name,
2134
+ kind: "manyToMany",
2135
+ through: {
2136
+ table: junction.junction_table_name,
2137
+ sourceColumn: junction.source_column_name,
2138
+ targetColumn: junction.target_column_name
2139
+ }
2140
+ });
2141
+ }
2142
+ if (metadata.policies) for (const policy of metadata.policies) {
2143
+ let operations = [];
2144
+ switch (policy.cmd) {
2145
+ case "ALL":
2146
+ operations = ["all"];
2147
+ break;
2148
+ case "SELECT":
2149
+ operations = ["select"];
2150
+ break;
2151
+ case "INSERT":
2152
+ operations = ["insert"];
2153
+ break;
2154
+ case "UPDATE":
2155
+ operations = ["update"];
2156
+ break;
2157
+ case "DELETE":
2158
+ operations = ["delete"];
2159
+ break;
2160
+ }
2161
+ const qual = policy.qual ?? void 0;
2162
+ const withCheck = policy.with_check ?? void 0;
2163
+ if (qual) securityRules.push({
2164
+ name: policy.policy_name,
2165
+ operations,
2166
+ roles: policy.roles ?? [],
2167
+ using: qual,
2168
+ ...withCheck ? { withCheck } : {}
2169
+ });
2170
+ else securityRules.push({
2171
+ name: policy.policy_name,
2172
+ operations,
2173
+ roles: policy.roles ?? []
2174
+ });
2175
+ }
2176
+ return {
2177
+ name: prettifyIdentifier(tableName),
2178
+ slug: tableName,
2179
+ table: tableName,
2180
+ properties,
2181
+ propertiesOrder,
2182
+ ...relations.length > 0 ? { relations } : {},
2183
+ ...securityRules.length > 0 ? { securityRules } : {}
2184
+ };
2185
+ }
2186
+ //#endregion
1950
2187
  //#region src/data/resolveDataSource.ts
1951
2188
  /**
1952
2189
  * Build a keyed registry from a list of {@link DataSourceDefinition}s.
@@ -2453,6 +2690,163 @@ var QueryBuilder = class {
2453
2690
  }
2454
2691
  };
2455
2692
  //#endregion
2693
+ //#region src/data/paginate.ts
2694
+ /**
2695
+ * The pagination engine behind `iterate()` / `findAll()`.
2696
+ *
2697
+ * It lives here, above both transports, on purpose: the HTTP client and the
2698
+ * in-process accessor implement the same `SDKCollectionClient` contract, and a
2699
+ * helper written twice is a helper that drifts. Both call into this file, so
2700
+ * "the SDK paginates like *this*" has exactly one definition.
2701
+ *
2702
+ * Everything below is expressed in terms of a single `find(params)` function,
2703
+ * which is all either transport has to supply.
2704
+ */
2705
+ /** Rows requested per page when the caller does not say. */
2706
+ var DEFAULT_PAGE_SIZE = 200;
2707
+ /** Rows `findAll()` will materialise before it refuses to continue. */
2708
+ var DEFAULT_FIND_ALL_MAX_ROWS = 1e4;
2709
+ /**
2710
+ * Requests one walk may make before it gives up on the server ever saying
2711
+ * `hasMore: false`. At the default page size that is two million rows — far
2712
+ * past any legitimate walk, and short of running forever.
2713
+ */
2714
+ var DEFAULT_MAX_PAGES = 1e4;
2715
+ /**
2716
+ * Thrown when a walk stops for a reason the caller needs to know about.
2717
+ *
2718
+ * Every one of these is a case where the alternative would be silent: a
2719
+ * truncated array that looks complete, or a loop that never returns. Check
2720
+ * {@link code} to tell them apart.
2721
+ */
2722
+ var RebasePaginationError = class RebasePaginationError extends Error {
2723
+ code;
2724
+ constructor(code, message) {
2725
+ super(message);
2726
+ this.name = "RebasePaginationError";
2727
+ this.code = code;
2728
+ Object.setPrototypeOf(this, RebasePaginationError.prototype);
2729
+ }
2730
+ };
2731
+ function normalizePageSize(raw) {
2732
+ if (raw === void 0 || !Number.isFinite(raw)) return 200;
2733
+ return Math.max(1, Math.floor(raw));
2734
+ }
2735
+ function normalizeMaxPages(raw) {
2736
+ if (raw === void 0) return DEFAULT_MAX_PAGES;
2737
+ if (raw === Number.POSITIVE_INFINITY) return raw;
2738
+ if (!Number.isFinite(raw)) return DEFAULT_MAX_PAGES;
2739
+ return Math.max(1, Math.floor(raw));
2740
+ }
2741
+ function normalizeMaxRows(raw) {
2742
+ if (raw === void 0) return DEFAULT_FIND_ALL_MAX_ROWS;
2743
+ if (raw === Number.POSITIVE_INFINITY) return raw;
2744
+ if (!Number.isFinite(raw)) return DEFAULT_FIND_ALL_MAX_ROWS;
2745
+ return Math.max(0, Math.floor(raw));
2746
+ }
2747
+ /**
2748
+ * Add one condition to a `where` map without disturbing what is already there.
2749
+ *
2750
+ * The caller's own filter on the cursor column has to survive — dropping it
2751
+ * would widen the query, which is the silent-filter-loss failure mode — so a
2752
+ * second condition on the same column becomes the array-of-tuples form that
2753
+ * `FindParams.where` already accepts, and both are AND-ed.
2754
+ */
2755
+ function appendCondition(where, column, condition) {
2756
+ const next = { ...where ?? {} };
2757
+ const existing = next[column];
2758
+ if (existing === void 0) next[column] = condition;
2759
+ else if (Array.isArray(existing) && existing.length > 0 && Array.isArray(existing[0])) next[column] = [...existing, condition];
2760
+ else next[column] = [existing, condition];
2761
+ return next;
2762
+ }
2763
+ function cursorEquals(a, b) {
2764
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
2765
+ return Object.is(a, b);
2766
+ }
2767
+ /**
2768
+ * Walk every row a query matches, yielding one row at a time and fetching the
2769
+ * next page only when the consumer asks for it.
2770
+ *
2771
+ * See {@link SDKCollectionClient.iterate} for the caller-facing contract,
2772
+ * including the offset-drift caveat and the `cursor` alternative.
2773
+ *
2774
+ * @param find the transport's single-page read
2775
+ * @param params `find()` parameters minus the window, plus the walk options
2776
+ * @param label the collection name, so an error says which walk failed
2777
+ */
2778
+ async function* paginateFind(find, params, label = "collection") {
2779
+ const { pageSize, cursor, maxPages, ...rest } = params ?? {};
2780
+ const findParams = { ...rest };
2781
+ const size = normalizePageSize(pageSize);
2782
+ const pageCap = normalizeMaxPages(maxPages);
2783
+ const cursorField = typeof cursor === "string" ? cursor : cursor?.field;
2784
+ const requestedDirection = typeof cursor === "object" && cursor !== null ? cursor.direction : void 0;
2785
+ let direction = "asc";
2786
+ if (cursorField) {
2787
+ const orderBy = findParams.orderBy;
2788
+ 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.`);
2789
+ direction = requestedDirection ?? orderBy?.[1] ?? "asc";
2790
+ findParams.orderBy = [cursorField, direction];
2791
+ }
2792
+ const seekOp = direction === "desc" ? "<" : ">";
2793
+ const baseWhere = findParams.where;
2794
+ let offset = 0;
2795
+ let pages = 0;
2796
+ let cursorValue;
2797
+ let seeking = false;
2798
+ for (;;) {
2799
+ 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\`.`);
2800
+ const pageParams = {
2801
+ ...findParams,
2802
+ limit: size
2803
+ };
2804
+ if (cursorField) {
2805
+ if (seeking) pageParams.where = appendCondition(baseWhere, cursorField, [seekOp, cursorValue]);
2806
+ } else pageParams.offset = offset;
2807
+ const page = await find(pageParams);
2808
+ pages += 1;
2809
+ const rows = page?.data ?? [];
2810
+ if (rows.length === 0) return;
2811
+ for (const row of rows) yield row;
2812
+ if (page?.meta?.hasMore !== true) return;
2813
+ if (cursorField) {
2814
+ const nextValue = rows[rows.length - 1]?.[cursorField];
2815
+ 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.`);
2816
+ 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.`);
2817
+ cursorValue = nextValue;
2818
+ seeking = true;
2819
+ } else offset += rows.length;
2820
+ }
2821
+ }
2822
+ /**
2823
+ * {@link paginateFind}, collected into an array under a ceiling.
2824
+ *
2825
+ * See {@link SDKCollectionClient.findAll}.
2826
+ */
2827
+ async function collectAllPages(find, params, label = "collection") {
2828
+ const { maxRows, ...rest } = params ?? {};
2829
+ const cap = normalizeMaxRows(maxRows);
2830
+ const out = [];
2831
+ for await (const row of paginateFind(find, rest, label)) {
2832
+ out.push(row);
2833
+ 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()\`.`);
2834
+ }
2835
+ return out;
2836
+ }
2837
+ /**
2838
+ * Build the `iterate` / `findAll` pair for one collection from its `find`.
2839
+ *
2840
+ * Both transports call this, which is what keeps the two implementations from
2841
+ * being two implementations.
2842
+ */
2843
+ function createPaginationHelpers(find, label) {
2844
+ return {
2845
+ iterate: (params) => paginateFind(find, params, label),
2846
+ findAll: (params) => collectAllPages(find, params, label)
2847
+ };
2848
+ }
2849
+ //#endregion
2456
2850
  //#region src/data/filter-dialect.ts
2457
2851
  /**
2458
2852
  * REST wire-format adapter for the unified filter system.
@@ -2755,6 +3149,42 @@ function rowToEntity(row, slug, primaryKeys = []) {
2755
3149
  values: row
2756
3150
  };
2757
3151
  }
3152
+ /**
3153
+ * The relation envelope `toCmsRow` writes where a relation was:
3154
+ * `{ id, path, __type: "relation", data: { id, path, values } }`. It is the
3155
+ * admin's view-model, and the only pipeline that produces one is postgres'.
3156
+ */
3157
+ function isRelationEnvelope(value) {
3158
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value.__type === "relation";
3159
+ }
3160
+ /** The target's own columns, as `toRestRow` would have inlined them. */
3161
+ function inlineEnvelope(envelope) {
3162
+ return envelope.data?.values ?? {};
3163
+ }
3164
+ /**
3165
+ * Replace every relation envelope on a row with the target's flat columns.
3166
+ *
3167
+ * The SDK serves one relation shape — the inlined one (see
3168
+ * {@link RestFetchService}) — and reads that come back through a *driver*
3169
+ * method rather than the REST pipeline still carry envelopes. Realtime is the
3170
+ * one such read left: there is no `listenForRest`, so the rows arrive shaped
3171
+ * for the admin and are flattened here instead.
3172
+ *
3173
+ * Only applied where the REST pipeline is the contract (see `find`); a driver
3174
+ * without a `restFetchService` keeps whatever it returns, so the admin's own
3175
+ * path through {@link buildRebaseData} is untouched.
3176
+ */
3177
+ function inlineRelationRefs(row) {
3178
+ let out;
3179
+ for (const [key, value] of Object.entries(row)) if (isRelationEnvelope(value)) {
3180
+ out = out ?? { ...row };
3181
+ out[key] = inlineEnvelope(value);
3182
+ } else if (Array.isArray(value) && value.some(isRelationEnvelope)) {
3183
+ out = out ?? { ...row };
3184
+ out[key] = value.map((item) => isRelationEnvelope(item) ? inlineEnvelope(item) : item);
3185
+ }
3186
+ return out ?? row;
3187
+ }
2758
3188
  function createDriverAccessor(driver, slug, getPks = () => []) {
2759
3189
  const accessor = {
2760
3190
  async find(params) {
@@ -2762,14 +3192,14 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2762
3192
  const limit = params?.limit ?? 20;
2763
3193
  const offset = params?.offset ?? 0;
2764
3194
  const fetchService = driver.restFetchService;
2765
- const rows = fetchService && params?.include && params.include.length > 0 ? await fetchService.fetchCollectionForRest(slug, {
3195
+ const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
2766
3196
  filter,
2767
3197
  limit: params?.limit,
2768
3198
  offset: params?.offset,
2769
3199
  orderBy: params?.orderBy?.[0],
2770
3200
  order: params?.orderBy?.[1],
2771
3201
  searchString: params?.searchString
2772
- }, params.include) : await driver.fetchCollection({
3202
+ }, params?.include) : await driver.fetchCollection({
2773
3203
  path: slug,
2774
3204
  limit: params?.limit,
2775
3205
  offset: params?.offset,
@@ -2798,7 +3228,8 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2798
3228
  };
2799
3229
  },
2800
3230
  async findById(id) {
2801
- const row = await driver.fetchOne({
3231
+ const fetchService = driver.restFetchService;
3232
+ const row = fetchService ? await fetchService.fetchOneForRest(slug, id) : await driver.fetchOne({
2802
3233
  path: slug,
2803
3234
  id
2804
3235
  });
@@ -2844,6 +3275,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2844
3275
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
2845
3276
  const limit = params?.limit ?? 20;
2846
3277
  const offset = params?.offset ?? 0;
3278
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
2847
3279
  return driver.listenCollection({
2848
3280
  path: slug,
2849
3281
  limit: params?.limit,
@@ -2854,7 +3286,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2854
3286
  searchString: params?.searchString,
2855
3287
  onUpdate: (entities) => {
2856
3288
  onUpdate({
2857
- data: entities.map((row) => rowToEntity(row, slug, getPks())),
3289
+ data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
2858
3290
  meta: {
2859
3291
  total: entities.length,
2860
3292
  limit,
@@ -2867,10 +3299,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
2867
3299
  });
2868
3300
  } : void 0,
2869
3301
  listenById: driver.listenOne ? (id, onUpdate, onError) => {
3302
+ const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
2870
3303
  return driver.listenOne({
2871
3304
  path: slug,
2872
3305
  id,
2873
- onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug, getPks()) : void 0),
3306
+ onUpdate: (entity) => onUpdate(entity ? rowToEntity(normalize(entity), slug, getPks()) : void 0),
2874
3307
  onError
2875
3308
  });
2876
3309
  } : void 0,
@@ -3001,7 +3434,7 @@ var SdkQueryBuilder = class {
3001
3434
  * {@link SDKCollectionClient}. Every returned record is unwrapped to a flat row
3002
3435
  * so the backend SDK is byte-for-byte the same shape as the frontend client.
3003
3436
  */
3004
- function toSdkCollectionClient(snap) {
3437
+ function toSdkCollectionClient(snap, slug = "collection") {
3005
3438
  const client = {
3006
3439
  async find(params) {
3007
3440
  const res = await snap.find(params);
@@ -3010,6 +3443,12 @@ function toSdkCollectionClient(snap) {
3010
3443
  meta: res.meta
3011
3444
  };
3012
3445
  },
3446
+ iterate(params) {
3447
+ return paginateFind((p) => client.find(p), params, slug);
3448
+ },
3449
+ findAll(params) {
3450
+ return collectAllPages((p) => client.find(p), params, slug);
3451
+ },
3013
3452
  async findById(id) {
3014
3453
  const s = await snap.findById(id);
3015
3454
  return s ? entityToRow(s) : void 0;
@@ -3051,7 +3490,7 @@ function toSdkCollectionClient(snap) {
3051
3490
  /**
3052
3491
  * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
3053
3492
  * {@link CollectionAccessor}. Every returned row is re-wrapped into the
3054
- * `{ id, path, values }` view-model the admin CMS renders.
3493
+ * `{ id, path, values }` view-model the admin admin renders.
3055
3494
  */
3056
3495
  function toEntityAccessor(sdk, slug, getPks = () => []) {
3057
3496
  const accessor = {
@@ -3099,10 +3538,10 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
3099
3538
  /**
3100
3539
  * Wrap a flat {@link RebaseSdkData} into a Entity-shaped {@link RebaseData}.
3101
3540
  *
3102
- * This is the **CMS boundary**: the SDK client (`client.data`) returns flat
3541
+ * This is the **admin boundary**: the SDK client (`client.data`) returns flat
3103
3542
  * rows, but the admin renders the `Entity` view-model (`entity.values.*`).
3104
3543
  * `core/Rebase.tsx` wraps `client.data` through this before handing it to the
3105
- * CMS `RebaseDataContext` — without it the admin renders rows with only their
3544
+ * admin `RebaseDataContext` — without it the admin renders rows with only their
3106
3545
  * `id`.
3107
3546
  */
3108
3547
  function wrapAsEntityData(sdkData, options) {
@@ -3136,7 +3575,7 @@ function wrapAsSdkData(entityData) {
3136
3575
  function getAccessor(slug) {
3137
3576
  let accessor = cache.get(slug);
3138
3577
  if (!accessor) {
3139
- accessor = toSdkCollectionClient(entityData.collection(slug));
3578
+ accessor = toSdkCollectionClient(entityData.collection(slug), slug);
3140
3579
  cache.set(slug, accessor);
3141
3580
  }
3142
3581
  return accessor;
@@ -3153,8 +3592,12 @@ function wrapAsSdkData(entityData) {
3153
3592
  *
3154
3593
  * This is the developer-facing SDK data layer used by backend framework
3155
3594
  * callbacks & scripts (`context.data` / `rebase.data`). It returns flat rows —
3156
- * identical in shape to the frontend SDK client so the API is symmetric
3157
- * across front and back. The admin CMS uses {@link buildRebaseData} (Entity).
3595
+ * identical in shape to the frontend SDK client, down to how a relation is
3596
+ * served: a foreign key stays a foreign key, and a relation named in `include`
3597
+ * arrives as the target's own columns. The `{ __type: "relation" }` envelope is
3598
+ * the admin's view-model and never reaches here.
3599
+ *
3600
+ * The admin uses {@link buildRebaseData} (Entity) over its own driver.
3158
3601
  */
3159
3602
  function buildSdkData(driver) {
3160
3603
  return wrapAsSdkData(buildRebaseData(driver));
@@ -3330,6 +3773,6 @@ async function detectJunctionTables(executeSql) {
3330
3773
  return junctionTables;
3331
3774
  }
3332
3775
  //#endregion
3333
- export { COLLECTION_PATH_SEPARATOR, COMPOSITE_ID_SEPARATOR, CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, and, buildCollection, buildCompositeId, buildConditionContext, buildProperty, buildPropertyCallbacks, buildRebaseData, buildRoutedRebaseData, buildSdkData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, checkOperation, classifyTable, cond, createDataSourceRegistry, createRelationRef, createRelationRefWithData, defaultUsersCollection, defineCollection, deserializeFilter, deserializeLogicalCondition, deserializeOrderBy, detectJunctionTables, embedParentExpression, enumToObjectEntries, evaluateCondition, evaluatePolicy, findAnonymousGrants, findRelation, fullPathToCollectionSegments, getArrayResolvedProperties, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeToEntityRelation, or, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
3776
+ 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, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollection, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, buildProperty, 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, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
3334
3777
 
3335
3778
  //# sourceMappingURL=index.es.js.map