@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/data/buildRebaseData.d.ts +8 -4
- package/dist/data/paginate.d.ts +75 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +466 -23
- package/dist/index.es.js.map +1 -1
- package/dist/util/entities.d.ts +1 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/pg-column-to-property.d.ts +25 -0
- package/package.json +3 -3
- package/src/data/buildRebaseData.ts +101 -13
- package/src/data/paginate.ts +290 -0
- package/src/index.ts +1 -0
- package/src/util/auth-default-policies.ts +4 -4
- package/src/util/entities.ts +1 -1
- package/src/util/index.ts +1 -0
- package/src/util/junction-policies.ts +1 -1
- package/src/util/pg-column-to-property.ts +335 -0
- package/src/util/relations.ts +3 -3
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
|
|
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 (!
|
|
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 (
|
|
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 = [...
|
|
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 = (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 **
|
|
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
|
-
*
|
|
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
|
|
3157
|
-
*
|
|
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
|