@rebasepro/common 0.13.0 → 0.13.1-canary.g18cfeb7
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 +10 -1
- package/dist/data/filter-dialect.d.ts +15 -1
- package/dist/data/paginate.d.ts +20 -0
- package/dist/data/resolveDataSource.d.ts +36 -0
- package/dist/index.es.js +405 -43
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +2 -2
- package/dist/util/conditions.d.ts +7 -3
- package/dist/util/entities.d.ts +8 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/internal-tables.d.ts +95 -0
- package/dist/util/policy/sqlToPolicy.d.ts +4 -4
- package/dist/util/relations.d.ts +18 -1
- package/dist/util/resolutions.d.ts +31 -0
- package/package.json +3 -3
- package/src/data/buildRebaseData.ts +100 -16
- package/src/data/filter-dialect.ts +28 -3
- package/src/data/paginate.ts +30 -0
- package/src/data/resolveDataSource.ts +56 -0
- package/src/util/builders.ts +3 -3
- package/src/util/conditions.ts +8 -3
- package/src/util/entities.ts +15 -1
- package/src/util/index.ts +1 -0
- package/src/util/internal-tables.ts +154 -0
- package/src/util/policy/policyToPostgres.ts +23 -10
- package/src/util/policy/sqlToPolicy.ts +40 -20
- package/src/util/relations.ts +31 -0
- package/src/util/resolutions.ts +77 -5
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, toCanonicalOp } from "@rebasepro/types";
|
|
1
|
+
import { ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, CANONICAL_TO_REST, DEFAULT_DATA_SOURCE_KEY, DEFAULT_LIST_LIMIT, EntityReference, EntityRelation, NULL_OPS, REST_TO_CANONICAL, RLS_ROLES_SQL, RLS_UID_SQL, getDataSourceCapabilities, getDeclaredSubcollections, isAnonymousUid, isManyToMany, isPostgresCollectionConfig, isRelationalCollectionConfig, policy, rewriteLegacyRlsFunctions, toCanonicalOp } from "@rebasepro/types";
|
|
2
2
|
import { deepClone, generateForeignKeyName, getIn, getPolicyNamesForRules, 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";
|
|
@@ -89,10 +89,21 @@ function getRelationFrom(entity) {
|
|
|
89
89
|
* have `id` and `path` fields — these are relation-shaped objects from
|
|
90
90
|
* edge cases in the data pipeline (REST fallback, stale cache, custom data source).
|
|
91
91
|
*
|
|
92
|
+
* When `targetPath` is given, also accepts a bare id. A relation column is a
|
|
93
|
+
* foreign key, and the REST layer returns it as the scalar it is; only some
|
|
94
|
+
* fetch paths hydrate it into an object. Which form a caller sees therefore
|
|
95
|
+
* depends on how the row was loaded, and a caller that only accepted objects
|
|
96
|
+
* reported half of its own data as a type error. The declared target is the
|
|
97
|
+
* missing half: with it, an id is a relation that has not been fetched yet.
|
|
98
|
+
*
|
|
92
99
|
* Returns null if the value cannot be coerced.
|
|
93
100
|
*/
|
|
94
|
-
function normalizeToEntityRelation(value, propertyType) {
|
|
101
|
+
function normalizeToEntityRelation(value, propertyType, targetPath) {
|
|
95
102
|
if (value instanceof EntityRelation) return value;
|
|
103
|
+
if (targetPath && (typeof value === "string" || typeof value === "number")) {
|
|
104
|
+
if (value === "") return null;
|
|
105
|
+
return new EntityRelation(value, targetPath);
|
|
106
|
+
}
|
|
96
107
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
97
108
|
const obj = value;
|
|
98
109
|
if (!(obj.__type === "relation" || obj.__type === "reference" || typeof obj.isEntityRelation === "function" && obj.isEntityRelation() || typeof obj.isEntityReference === "function" && obj.isEntityReference() || propertyType === "relation" && typeof obj.id !== "undefined" && typeof obj.path === "string")) return null;
|
|
@@ -608,6 +619,33 @@ function resolveCollectionRelations(collection) {
|
|
|
608
619
|
_resolvedRelationsCache.set(collection, relations);
|
|
609
620
|
return relations;
|
|
610
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* The path of the collection a relation property points at, derived from the
|
|
624
|
+
* property alone.
|
|
625
|
+
*
|
|
626
|
+
* A preview holds a property and a value and no collection, so it cannot call
|
|
627
|
+
* `resolveRelationProperty`. It does not need to: both forms that carry a
|
|
628
|
+
* target — the stamped `resolvedRelation` and the inline `relation` — name it
|
|
629
|
+
* directly. Only the third form, a relation declared by name in the
|
|
630
|
+
* collection's `relations` array, is out of reach, and that one has no target
|
|
631
|
+
* to read without the collection anyway.
|
|
632
|
+
*
|
|
633
|
+
* This is what lets a preview render a relation column that arrived as a bare
|
|
634
|
+
* foreign key: the id says *which* row, the declared target says *which
|
|
635
|
+
* collection*, and `RelationPreview` fetches the rest. Without it a scalar id
|
|
636
|
+
* is indistinguishable from a value of the wrong type.
|
|
637
|
+
*/
|
|
638
|
+
function getRelationTargetPath(property) {
|
|
639
|
+
const stamped = property.resolvedRelation?.targetSlug;
|
|
640
|
+
if (stamped) return stamped;
|
|
641
|
+
const target = property.relation?.target;
|
|
642
|
+
if (typeof target !== "function") return void 0;
|
|
643
|
+
try {
|
|
644
|
+
return target()?.slug;
|
|
645
|
+
} catch (_e) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
611
649
|
function getTableName(collection) {
|
|
612
650
|
if (isRelationalCollectionConfig(collection)) return collection.table ?? toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
613
651
|
return toSnakeCase(collection.slug) ?? toSnakeCase(collection.name);
|
|
@@ -882,6 +920,59 @@ function getEntityChildViews(collection) {
|
|
|
882
920
|
return views;
|
|
883
921
|
}
|
|
884
922
|
/**
|
|
923
|
+
* Each of `collection`'s tabs paired with the property that declared it, when a
|
|
924
|
+
* property declared it: child view key → property key.
|
|
925
|
+
*
|
|
926
|
+
* A many-relation can only be declared as a property — that is the documented
|
|
927
|
+
* and only mechanism — and {@link getEntityChildViews} promotes it to a tab. So
|
|
928
|
+
* one declaration reaches the panel twice, and neither surface knew about the
|
|
929
|
+
* other. The form rendered a relation picker beside the tab, and the collection
|
|
930
|
+
* table rendered *two* columns under one heading: the relation's own column,
|
|
931
|
+
* showing the child rows, and a jump-to-tab button carrying the same name.
|
|
932
|
+
*
|
|
933
|
+
* The pairing is what lets each surface decide which half is redundant, and it
|
|
934
|
+
* has to be a pairing rather than two sets because the two keys differ whenever
|
|
935
|
+
* a relation is named. The match is on the resolved `relationName` — the
|
|
936
|
+
* identity `getEntityChildViews` itself dedupes on — so a relation declared in
|
|
937
|
+
* `relations` and pointed at by a differently-named property is recognised too.
|
|
938
|
+
*
|
|
939
|
+
* A relation with no property of its own is absent here, which is the point: it
|
|
940
|
+
* has exactly one surface already, and nothing to weigh it against.
|
|
941
|
+
*
|
|
942
|
+
* Only top-level properties: a relation nested inside a `map` gets no tab.
|
|
943
|
+
*/
|
|
944
|
+
function getChildViewDeclaringProperties(collection) {
|
|
945
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
946
|
+
const relationProperties = Object.entries(collection.properties ?? {}).filter(([, property]) => property?.type === "relation");
|
|
947
|
+
if (relationProperties.length === 0) return pairs;
|
|
948
|
+
const relationViews = getEntityChildViews(collection).filter((view) => view.source.kind === "relation");
|
|
949
|
+
if (relationViews.length === 0) return pairs;
|
|
950
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
951
|
+
const identityOf = (relationKey) => resolvedRelations[relationKey]?.relationName ?? relationKey;
|
|
952
|
+
const declaringPropertyByIdentity = /* @__PURE__ */ new Map();
|
|
953
|
+
for (const [propertyKey, property] of relationProperties) {
|
|
954
|
+
const relation = property.resolvedRelation ?? resolvedRelations[propertyKey];
|
|
955
|
+
if (relation?.cardinality !== "many") continue;
|
|
956
|
+
const identity = relation.relationName ?? propertyKey;
|
|
957
|
+
if (!declaringPropertyByIdentity.has(identity)) declaringPropertyByIdentity.set(identity, propertyKey);
|
|
958
|
+
}
|
|
959
|
+
for (const view of relationViews) {
|
|
960
|
+
const propertyKey = declaringPropertyByIdentity.get(identityOf(view.source.relationKey));
|
|
961
|
+
if (propertyKey) pairs.set(view.key, propertyKey);
|
|
962
|
+
}
|
|
963
|
+
return pairs;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* The property keys of `collection` whose relation is already one of its tabs.
|
|
967
|
+
*
|
|
968
|
+
* What a form asks: the tab is the treatment for a list of child rows, so the
|
|
969
|
+
* picker beside it is the redundant half. See
|
|
970
|
+
* {@link getChildViewDeclaringProperties}.
|
|
971
|
+
*/
|
|
972
|
+
function getChildViewRelationPropertyKeys(collection) {
|
|
973
|
+
return new Set(getChildViewDeclaringProperties(collection).values());
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
885
976
|
* The child views of `collection` as bare collections.
|
|
886
977
|
*
|
|
887
978
|
* The flattened view of {@link getEntityChildViews}, for navigation code that
|
|
@@ -931,9 +1022,9 @@ function isKeywordAt(upper, i, keyword) {
|
|
|
931
1022
|
*
|
|
932
1023
|
* This used to be `sql.split(/ AND /i)`, which tore subqueries in half: the
|
|
933
1024
|
* `AND` inside
|
|
934
|
-
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id =
|
|
1025
|
+
* `EXISTS (SELECT 1 FROM organization_members m WHERE m.org = t.org AND m.user_id = rebase.uid())`
|
|
935
1026
|
* split the expression, and re-emitting the halves produced
|
|
936
|
-
* `(EXISTS (...) AND m.user_id =
|
|
1027
|
+
* `(EXISTS (...) AND m.user_id = rebase.uid())`
|
|
937
1028
|
* where `m` is no longer in scope — SQL that Postgres rejects outright with
|
|
938
1029
|
* "missing FROM-clause entry for table". Returning null instead keeps such a
|
|
939
1030
|
* clause as a `raw` expression, which round-trips verbatim.
|
|
@@ -1007,15 +1098,15 @@ function stripOuterParens(sql) {
|
|
|
1007
1098
|
}
|
|
1008
1099
|
}
|
|
1009
1100
|
function sqlToPolicy(sql) {
|
|
1010
|
-
const trimmed = stripOuterParens(sql.trim());
|
|
1101
|
+
const trimmed = stripOuterParens(rewriteLegacyRlsFunctions(sql).trim());
|
|
1011
1102
|
if (trimmed.toLowerCase() === "true") return policy.true();
|
|
1012
1103
|
if (trimmed.toLowerCase() === "false") return policy.false();
|
|
1013
|
-
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
1104
|
+
const overlapMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
|
|
1014
1105
|
if (overlapMatch) {
|
|
1015
1106
|
const roles = overlapMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
1016
1107
|
return policy.rolesOverlap(roles);
|
|
1017
1108
|
}
|
|
1018
|
-
const containMatch = trimmed.match(/^string_to_array\s*\(\s*
|
|
1109
|
+
const containMatch = trimmed.match(/^string_to_array\s*\(\s*rebase\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
|
|
1019
1110
|
if (containMatch) {
|
|
1020
1111
|
const roles = containMatch[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
|
|
1021
1112
|
return policy.rolesContain(roles);
|
|
@@ -1031,10 +1122,10 @@ function sqlToPolicy(sql) {
|
|
|
1031
1122
|
const right = parseOperand(rightStr.trim());
|
|
1032
1123
|
if (left && right) return policy.compare(left, op === "=" ? "eq" : "neq", right);
|
|
1033
1124
|
}
|
|
1034
|
-
return policy.raw(
|
|
1125
|
+
return policy.raw(trimmed);
|
|
1035
1126
|
}
|
|
1036
1127
|
/**
|
|
1037
|
-
* Literals from other BaaS platforms that people compare `
|
|
1128
|
+
* Literals from other BaaS platforms that people compare `rebase.uid()` against
|
|
1038
1129
|
* out of habit. Mirrors the driver's `FOREIGN_CONVENTION_ROLES` guard on
|
|
1039
1130
|
* `pgRoles`, one surface over: the same muscle memory inside a `using:` string
|
|
1040
1131
|
* is the more dangerous spelling, because it inverts a rule instead of
|
|
@@ -1045,18 +1136,25 @@ var FOREIGN_CONVENTION_UIDS = {
|
|
|
1045
1136
|
authenticated: "Supabase",
|
|
1046
1137
|
service_role: "Supabase"
|
|
1047
1138
|
};
|
|
1048
|
-
/**
|
|
1049
|
-
|
|
1139
|
+
/**
|
|
1140
|
+
* `rebase.uid() IS NOT NULL` in raw SQL, the clause that is always true.
|
|
1141
|
+
*
|
|
1142
|
+
* Both schema spellings, because this runs over policy bodies read back from a
|
|
1143
|
+
* database, and one migrated by a pre-1.0 release still holds `auth.uid()`.
|
|
1144
|
+
* A security check that stops recognising a dangerous clause because the
|
|
1145
|
+
* framework renamed a function is a check that silently turns off.
|
|
1146
|
+
*/
|
|
1147
|
+
var UID_NOT_NULL = /\b(?:rebase|auth)\.uid\(\)\s+IS\s+NOT\s+NULL/i;
|
|
1050
1148
|
/**
|
|
1051
1149
|
* Find clauses that read as "signed-in users only" but admit anonymous callers.
|
|
1052
1150
|
*
|
|
1053
|
-
* Both spellings come from the same place — Supabase, where `auth.uid()`
|
|
1054
|
-
* is NULL for an anonymous request. Rebase substitutes
|
|
1151
|
+
* Both spellings come from the same place — Supabase, where its own `auth.uid()`
|
|
1152
|
+
* really is NULL for an anonymous request. Rebase substitutes
|
|
1055
1153
|
* {@link ANONYMOUS_USER_ID} instead (a blank id would read back as NULL, which
|
|
1056
1154
|
* is how the trusted *server* context is recognised), so:
|
|
1057
1155
|
*
|
|
1058
|
-
* - `
|
|
1059
|
-
* - `
|
|
1156
|
+
* - `rebase.uid() IS NOT NULL` is a tautology on the user path, and
|
|
1157
|
+
* - `rebase.uid() != 'anon'` excludes one spelling of anonymous and admits the
|
|
1060
1158
|
* other. This one is not hypothetical and was not only a foreign habit:
|
|
1061
1159
|
* rebase's own request path reported `'anon'` while everything that compiled
|
|
1062
1160
|
* or checked a policy used `'anonymous'`, so whichever literal an author
|
|
@@ -1088,7 +1186,7 @@ function findAnonymousGrants(expr) {
|
|
|
1088
1186
|
if (UID_NOT_NULL.test(e.sql)) found.push({
|
|
1089
1187
|
pattern: "uid-not-null",
|
|
1090
1188
|
detail: e.sql,
|
|
1091
|
-
explanation: `\`
|
|
1189
|
+
explanation: `\`rebase.uid() IS NOT NULL\` is true for every request that came from a client, including anonymous ones — they carry '${ANONYMOUS_USER_ID}', not NULL. Use \`condition: policy.authenticated()\` to mean "signed in".`
|
|
1092
1190
|
});
|
|
1093
1191
|
return;
|
|
1094
1192
|
case "compare": {
|
|
@@ -1110,7 +1208,7 @@ function findAnonymousGrants(expr) {
|
|
|
1110
1208
|
return found;
|
|
1111
1209
|
}
|
|
1112
1210
|
function parseOperand(str) {
|
|
1113
|
-
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /
|
|
1211
|
+
if (/current_setting\s*\(\s*'app\.(uid|user_id)'\s*\)/i.test(str) || /rebase\.uid\(\)/i.test(str)) return policy.authUid();
|
|
1114
1212
|
const stringMatch = str.match(/^'(.+)'$/);
|
|
1115
1213
|
if (stringMatch) return policy.literal(stringMatch[1]);
|
|
1116
1214
|
if (/^\w+$/.test(str)) return policy.field(str);
|
|
@@ -1192,12 +1290,12 @@ function compile(expr, scope) {
|
|
|
1192
1290
|
const rightSql = castForAuthUid(expr.right, operandToSql(expr.right, scope), expr.left);
|
|
1193
1291
|
return `${leftSql} ${COMPARE_SQL[expr.op]} ${rightSql}`;
|
|
1194
1292
|
}
|
|
1195
|
-
case "rolesOverlap": return `string_to_array(
|
|
1196
|
-
case "rolesContain": return `string_to_array(
|
|
1197
|
-
case "authenticated": return
|
|
1198
|
-
case "serverContext": return
|
|
1293
|
+
case "rolesOverlap": return `string_to_array(${RLS_ROLES_SQL}, ',') && ${rolesArraySql(expr.roles)}`;
|
|
1294
|
+
case "rolesContain": return `string_to_array(${RLS_ROLES_SQL}, ',') @> ${rolesArraySql(expr.roles)}`;
|
|
1295
|
+
case "authenticated": return `${RLS_UID_SQL} IS NOT NULL AND ${RLS_UID_SQL} NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
|
|
1296
|
+
case "serverContext": return `${RLS_UID_SQL} IS NULL`;
|
|
1199
1297
|
case "existsIn": return compileExistsIn(expr, scope);
|
|
1200
|
-
case "raw": return expr.sql.replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
1298
|
+
case "raw": return rewriteLegacyRlsFunctions(expr.sql).replace(/\{(\w+)\}/g, (_, col) => `${outerQualifier(scope)}${resolveColumnName(col, scope.outerCollection)}`);
|
|
1201
1299
|
}
|
|
1202
1300
|
}
|
|
1203
1301
|
/**
|
|
@@ -1234,8 +1332,8 @@ function operandToSql(operand, scope) {
|
|
|
1234
1332
|
case "field": return `${scope.fieldPrefix}${resolveColumnName(operand.name, scope.fieldCollection)}`;
|
|
1235
1333
|
case "outerField": return `${scope.outerPrefix}${resolveColumnName(operand.name, scope.outerCollection)}`;
|
|
1236
1334
|
case "literal": return quoteLiteral(operand.value);
|
|
1237
|
-
case "authUid": return
|
|
1238
|
-
case "authRoles": return
|
|
1335
|
+
case "authUid": return RLS_UID_SQL;
|
|
1336
|
+
case "authRoles": return `string_to_array(${RLS_ROLES_SQL}, ',')`;
|
|
1239
1337
|
}
|
|
1240
1338
|
}
|
|
1241
1339
|
/**
|
|
@@ -1996,9 +2094,14 @@ function registerConditionOperations() {
|
|
|
1996
2094
|
operationsRegistered = true;
|
|
1997
2095
|
}
|
|
1998
2096
|
/**
|
|
1999
|
-
* Evaluate a
|
|
2097
|
+
* Evaluate a condition against the given context.
|
|
2098
|
+
*
|
|
2099
|
+
* A condition may be stated as a literal instead of a rule — `hidden: true`
|
|
2100
|
+
* rather than `hidden: { "==": [1, 1] }` — and a literal is already its own
|
|
2101
|
+
* answer, so it is returned rather than handed to the evaluator.
|
|
2000
2102
|
*/
|
|
2001
2103
|
function evaluateCondition(rule, context) {
|
|
2104
|
+
if (typeof rule === "boolean") return rule;
|
|
2002
2105
|
registerConditionOperations();
|
|
2003
2106
|
return jsonLogic.apply(rule, context);
|
|
2004
2107
|
}
|
|
@@ -2317,6 +2420,142 @@ function resolveStringColumnLength(prop) {
|
|
|
2317
2420
|
return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : 255;
|
|
2318
2421
|
}
|
|
2319
2422
|
//#endregion
|
|
2423
|
+
//#region src/util/internal-tables.ts
|
|
2424
|
+
/**
|
|
2425
|
+
* The tables Rebase creates for its own bookkeeping, and the SQL that keeps the
|
|
2426
|
+
* end-user role away from them.
|
|
2427
|
+
*
|
|
2428
|
+
* ## Why this exists
|
|
2429
|
+
*
|
|
2430
|
+
* Authenticated requests run as {@link REBASE_USER_ROLE}, and the boot-time role
|
|
2431
|
+
* provisioning grants that role `SELECT, INSERT, UPDATE, DELETE` on every table
|
|
2432
|
+
* in the schemas a project uses — including `rebase`, because a project's own
|
|
2433
|
+
* collections are allowed to live there (the scaffold puts `users` there). It
|
|
2434
|
+
* also sets `ALTER DEFAULT PRIVILEGES`, so a table created *later* by the
|
|
2435
|
+
* migrating role inherits the same grant.
|
|
2436
|
+
*
|
|
2437
|
+
* Every framework-internal table is created later: auth's tables come up during
|
|
2438
|
+
* `initializeAuth`, `api_keys` during route mounting, `cron_logs` when the first
|
|
2439
|
+
* job registers, `idempotency_keys` on the first request that carries a key. So
|
|
2440
|
+
* they all inherited full DML for the end-user role — and none of them enables
|
|
2441
|
+
* row-level security, because none of them is a collection with
|
|
2442
|
+
* `securityRules`. Measured on a freshly provisioned database, `SET ROLE
|
|
2443
|
+
* rebase_user` could read `rebase.refresh_tokens` (session token hashes),
|
|
2444
|
+
* `rebase.mfa_factors` (`secret_encrypted`), `rebase.recovery_codes`, and
|
|
2445
|
+
* `rebase.api_keys` (including its `admin` flag), and insert into
|
|
2446
|
+
* `rebase.app_config`.
|
|
2447
|
+
*
|
|
2448
|
+
* Nothing routes a user-context query at those tables today, so this was not
|
|
2449
|
+
* reachable over the API. That is the wrong thing to depend on: the documented
|
|
2450
|
+
* model is that RLS is the authorization boundary, and these tables sat outside
|
|
2451
|
+
* it. The boundary is now a privilege boundary instead — the role simply cannot
|
|
2452
|
+
* address them.
|
|
2453
|
+
*
|
|
2454
|
+
* ## Why REVOKE rather than ENABLE ROW LEVEL SECURITY
|
|
2455
|
+
*
|
|
2456
|
+
* RLS with no policy denies every row, which is the same outcome, but it is the
|
|
2457
|
+
* *weaker* statement: it leaves the grant in place, so a later policy — or a
|
|
2458
|
+
* `FORCE` flag cleared by some future migration — reopens the table. There is no
|
|
2459
|
+
* row of `refresh_tokens` any end user should ever reach, so the honest encoding
|
|
2460
|
+
* is "this role has no privilege here at all". It also keeps the owner
|
|
2461
|
+
* connection (which auth actually runs on) completely unaffected.
|
|
2462
|
+
*
|
|
2463
|
+
* ## Keeping it true
|
|
2464
|
+
*
|
|
2465
|
+
* `packages/rls-check` scans the `rebase` schema — it used to skip it as a
|
|
2466
|
+
* "platform" schema — and its `rls-disabled` check fires on exactly the
|
|
2467
|
+
* condition this module removes: RLS off *and* a DML grant to a reachable role.
|
|
2468
|
+
* So a table added here without a revoke is caught by `pnpm rls:check`, not by
|
|
2469
|
+
* someone re-reading this file.
|
|
2470
|
+
*/
|
|
2471
|
+
/**
|
|
2472
|
+
* The Postgres role authenticated requests run as.
|
|
2473
|
+
*
|
|
2474
|
+
* Defined here rather than in the Postgres driver because both the driver (which
|
|
2475
|
+
* provisions the role) and this module (which revokes on its behalf) need it,
|
|
2476
|
+
* and a second spelling of a role name is a silent no-op waiting to happen.
|
|
2477
|
+
*/
|
|
2478
|
+
var REBASE_USER_ROLE = "rebase_user";
|
|
2479
|
+
/**
|
|
2480
|
+
* Framework-internal table names, unqualified.
|
|
2481
|
+
*
|
|
2482
|
+
* Deliberately NOT including `users`: the auth user table is also a collection,
|
|
2483
|
+
* with `securityRules`, RLS enabled and policies applied. Users read their own
|
|
2484
|
+
* row through it — revoking there would break sign-in.
|
|
2485
|
+
*
|
|
2486
|
+
* `atlas_schema_revisions` is Atlas's migration ledger, which lands in `rebase`
|
|
2487
|
+
* because `db migrate apply` passes `--revisions-schema rebase`.
|
|
2488
|
+
*/
|
|
2489
|
+
var REBASE_INTERNAL_TABLES = [
|
|
2490
|
+
"user_identities",
|
|
2491
|
+
"refresh_tokens",
|
|
2492
|
+
"password_reset_tokens",
|
|
2493
|
+
"magic_link_tokens",
|
|
2494
|
+
"mfa_factors",
|
|
2495
|
+
"mfa_challenges",
|
|
2496
|
+
"recovery_codes",
|
|
2497
|
+
"app_config",
|
|
2498
|
+
"schema_meta",
|
|
2499
|
+
"api_keys",
|
|
2500
|
+
"cron_logs",
|
|
2501
|
+
"cron_claims",
|
|
2502
|
+
"idempotency_keys",
|
|
2503
|
+
"entity_history",
|
|
2504
|
+
"branches",
|
|
2505
|
+
"channel_messages",
|
|
2506
|
+
"channel_cursors",
|
|
2507
|
+
"channel_presence",
|
|
2508
|
+
"atlas_schema_revisions"
|
|
2509
|
+
];
|
|
2510
|
+
/** Postgres identifiers this module is willing to interpolate. */
|
|
2511
|
+
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
2512
|
+
/**
|
|
2513
|
+
* A single statement that takes every privilege on `schema.table` away from the
|
|
2514
|
+
* end-user role.
|
|
2515
|
+
*
|
|
2516
|
+
* Wrapped in a `DO` block guarded on `pg_roles` for two reasons, both of which
|
|
2517
|
+
* happen in practice:
|
|
2518
|
+
*
|
|
2519
|
+
* - the role does not exist when the connection is unprivileged (Rebase then
|
|
2520
|
+
* relies on native RLS rather than a role switch), and a bare `REVOKE` on a
|
|
2521
|
+
* missing role is an error, not a no-op;
|
|
2522
|
+
* - the table may not exist yet — `cron_logs` never appears in a project with
|
|
2523
|
+
* no cron jobs — and `to_regclass` returning NULL has to be tolerated too.
|
|
2524
|
+
*
|
|
2525
|
+
* One command, so it is safe on handles that speak the extended query protocol
|
|
2526
|
+
* and reject multi-statement strings.
|
|
2527
|
+
*/
|
|
2528
|
+
function revokeInternalTableSql(schema, table) {
|
|
2529
|
+
if (!SAFE_IDENTIFIER.test(schema)) throw new Error(`Refusing to build SQL with an unsafe schema name: ${JSON.stringify(schema)}`);
|
|
2530
|
+
if (!SAFE_IDENTIFIER.test(table)) throw new Error(`Refusing to build SQL with an unsafe table name: ${JSON.stringify(table)}`);
|
|
2531
|
+
const qualified = `"${schema}"."${table}"`;
|
|
2532
|
+
return `
|
|
2533
|
+
DO $rebase_revoke$
|
|
2534
|
+
BEGIN
|
|
2535
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}')
|
|
2536
|
+
AND to_regclass('${qualified}') IS NOT NULL THEN
|
|
2537
|
+
EXECUTE 'REVOKE ALL ON ${qualified} FROM ${REBASE_USER_ROLE}';
|
|
2538
|
+
END IF;
|
|
2539
|
+
END
|
|
2540
|
+
$rebase_revoke$;
|
|
2541
|
+
`.trim();
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Revoke on every internal table in `schema`, one statement at a time.
|
|
2545
|
+
*
|
|
2546
|
+
* Best-effort per table: a connection that does not own one of them (a
|
|
2547
|
+
* pre-provisioned database, a platform-managed ledger) cannot revoke on it, and
|
|
2548
|
+
* that must not take down a boot. The caller decides how loud to be — `onError`
|
|
2549
|
+
* exists so the driver can warn without this module importing a logger.
|
|
2550
|
+
*/
|
|
2551
|
+
async function revokeInternalTableAccess(execute, schema, options) {
|
|
2552
|
+
for (const table of options?.tables ?? REBASE_INTERNAL_TABLES) try {
|
|
2553
|
+
await execute(revokeInternalTableSql(schema, table));
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
options?.onError?.(table, error);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
//#endregion
|
|
2320
2559
|
//#region src/data/resolveDataSource.ts
|
|
2321
2560
|
/**
|
|
2322
2561
|
* Build a keyed registry from a list of {@link DataSourceDefinition}s.
|
|
@@ -2359,6 +2598,46 @@ function resolveDataSource(collection, registry) {
|
|
|
2359
2598
|
capabilities: getDataSourceCapabilities(engine)
|
|
2360
2599
|
};
|
|
2361
2600
|
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Does a SQL toolchain own this collection's storage?
|
|
2603
|
+
*
|
|
2604
|
+
* "Owns the storage" means: something generates a table for it, pushes that
|
|
2605
|
+
* table to a database, plans its RLS policies, and reports it as drifted when
|
|
2606
|
+
* the two disagree. That is true of a Postgres collection and false of a
|
|
2607
|
+
* Firestore or MongoDB one, whose documents live in a store Rebase never
|
|
2608
|
+
* migrates — and the two were never told apart. Every stage of the SQL
|
|
2609
|
+
* toolchain took "the collections" to mean *all* of them, so a Firestore
|
|
2610
|
+
* collection declared next to the Postgres ones got a `pgTable` in the
|
|
2611
|
+
* generated schema, a `CREATE TABLE` at boot, RLS policies, and a place in the
|
|
2612
|
+
* `db push` include list — where its name shielding a same-named real table
|
|
2613
|
+
* from Atlas's exclude list is the one that can lose data.
|
|
2614
|
+
*
|
|
2615
|
+
* The answer is the resolved engine's {@link DataSourceCapabilities}, not a
|
|
2616
|
+
* name check: an engine registered through `registerDataSourceCapabilities`
|
|
2617
|
+
* gets the same treatment as the built-in ones.
|
|
2618
|
+
*
|
|
2619
|
+
* Deliberately answers **true** for an engine nobody has heard of. Build-time
|
|
2620
|
+
* tooling (the CLI, the schema generator) has no data-source registry to
|
|
2621
|
+
* resolve a `dataSource` key against, so an unknown key resolves to an unknown
|
|
2622
|
+
* engine — and the cost of the two mistakes is not symmetric. Wrongly
|
|
2623
|
+
* including a collection generates a table nothing writes to; wrongly excluding
|
|
2624
|
+
* one silently stops generating a table the app is serving from. Declare
|
|
2625
|
+
* `engine` on a collection that is not SQL-backed and this is exact.
|
|
2626
|
+
*/
|
|
2627
|
+
function isRelationalCollection(collection, registry) {
|
|
2628
|
+
return getDataSourceCapabilities(collection?.engine ?? (collection?.dataSource ? resolveDataSource(collection, registry).engine : void 0)).supportsRelations;
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* The subset of `collections` a SQL toolchain owns — see
|
|
2632
|
+
* {@link isRelationalCollection}.
|
|
2633
|
+
*
|
|
2634
|
+
* Every stage that generates SQL from collections starts by calling this, so
|
|
2635
|
+
* the rule lives in one place rather than being re-decided per generator. It
|
|
2636
|
+
* keeps the input order.
|
|
2637
|
+
*/
|
|
2638
|
+
function relationalCollections(collections, registry) {
|
|
2639
|
+
return collections.filter((collection) => isRelationalCollection(collection, registry));
|
|
2640
|
+
}
|
|
2362
2641
|
//#endregion
|
|
2363
2642
|
//#region src/collections/CollectionRegistry.ts
|
|
2364
2643
|
var CollectionRegistry = class {
|
|
@@ -2861,6 +3140,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
2861
3140
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
2862
3141
|
}
|
|
2863
3142
|
};
|
|
3143
|
+
/**
|
|
3144
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3145
|
+
*
|
|
3146
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3147
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3148
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3149
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3150
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3151
|
+
* rows are the mildest of those outcomes.
|
|
3152
|
+
*
|
|
3153
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3154
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3155
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3156
|
+
* look like it is paging by offset.
|
|
3157
|
+
*/
|
|
3158
|
+
function resolveFindWindow(params) {
|
|
3159
|
+
const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
|
|
3160
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3161
|
+
return {
|
|
3162
|
+
limit,
|
|
3163
|
+
offset,
|
|
3164
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
2864
3167
|
function normalizePageSize(raw) {
|
|
2865
3168
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
2866
3169
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3194,7 +3497,22 @@ function serializeLogicalCondition(cond) {
|
|
|
3194
3497
|
* deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
|
|
3195
3498
|
* // → { type: "or", conditions: [...] }
|
|
3196
3499
|
*/
|
|
3197
|
-
|
|
3500
|
+
/**
|
|
3501
|
+
* How deeply `or(...)`/`and(...)` groups may nest.
|
|
3502
|
+
*
|
|
3503
|
+
* This parser recurses once per level, on a value that arrives in a query
|
|
3504
|
+
* string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
|
|
3505
|
+
* stack size exceeded`, which a caller sees as a 500 about the call stack
|
|
3506
|
+
* rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
|
|
3507
|
+
* below that in practice, but "the HTTP layer happens to stop it" is not a
|
|
3508
|
+
* bound this parser should rely on.
|
|
3509
|
+
*
|
|
3510
|
+
* Thirty-two is far past anything a real filter expresses; the deepest in this
|
|
3511
|
+
* repository's own tests is three.
|
|
3512
|
+
*/
|
|
3513
|
+
var MAX_LOGICAL_NESTING_DEPTH = 32;
|
|
3514
|
+
function deserializeLogicalCondition(str, nesting = 0) {
|
|
3515
|
+
if (nesting > 32) throw new Error(`Filter groups nest more than 32 levels deep. Flatten the condition — \`or(a,or(b,c))\` is \`or(a,b,c)\`.`);
|
|
3198
3516
|
const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
|
|
3199
3517
|
if (logicalMatch) {
|
|
3200
3518
|
const type = logicalMatch[1];
|
|
@@ -3205,10 +3523,10 @@ function deserializeLogicalCondition(str) {
|
|
|
3205
3523
|
for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
|
|
3206
3524
|
else if (innerStr[i] === ")") depth--;
|
|
3207
3525
|
else if (innerStr[i] === "," && depth === 0) {
|
|
3208
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
|
|
3526
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start, i), nesting + 1));
|
|
3209
3527
|
start = i + 1;
|
|
3210
3528
|
}
|
|
3211
|
-
conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
|
|
3529
|
+
conditions.push(deserializeLogicalCondition(innerStr.slice(start), nesting + 1));
|
|
3212
3530
|
return {
|
|
3213
3531
|
type,
|
|
3214
3532
|
conditions
|
|
@@ -3322,21 +3640,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3322
3640
|
const accessor = {
|
|
3323
3641
|
async find(params) {
|
|
3324
3642
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3325
|
-
const limit = params
|
|
3326
|
-
const offset = params?.offset ?? 0;
|
|
3643
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3327
3644
|
const fetchService = driver.restFetchService;
|
|
3328
3645
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3329
3646
|
filter,
|
|
3330
|
-
|
|
3331
|
-
|
|
3647
|
+
logical: params?.logical,
|
|
3648
|
+
limit,
|
|
3649
|
+
offset: driverOffset,
|
|
3332
3650
|
orderBy: params?.orderBy?.[0],
|
|
3333
3651
|
order: params?.orderBy?.[1],
|
|
3334
3652
|
searchString: params?.searchString
|
|
3335
3653
|
}, params?.include) : await driver.fetchCollection({
|
|
3336
3654
|
path: slug,
|
|
3337
|
-
limit
|
|
3338
|
-
offset:
|
|
3655
|
+
limit,
|
|
3656
|
+
offset: driverOffset,
|
|
3339
3657
|
filter,
|
|
3658
|
+
logical: params?.logical,
|
|
3340
3659
|
orderBy: params?.orderBy?.[0],
|
|
3341
3660
|
order: params?.orderBy?.[1],
|
|
3342
3661
|
searchString: params?.searchString
|
|
@@ -3346,7 +3665,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3346
3665
|
if (driver.count) {
|
|
3347
3666
|
total = await driver.count({
|
|
3348
3667
|
path: slug,
|
|
3349
|
-
filter
|
|
3668
|
+
filter,
|
|
3669
|
+
logical: params?.logical,
|
|
3670
|
+
searchString: params?.searchString
|
|
3350
3671
|
});
|
|
3351
3672
|
hasMore = offset + rows.length < total;
|
|
3352
3673
|
}
|
|
@@ -3398,22 +3719,39 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3398
3719
|
values: {}
|
|
3399
3720
|
} });
|
|
3400
3721
|
},
|
|
3722
|
+
updateMany: driver.updateMany ? async (updates) => {
|
|
3723
|
+
return (await driver.updateMany({
|
|
3724
|
+
path: slug,
|
|
3725
|
+
updates: updates.map((u) => ({
|
|
3726
|
+
id: u.id,
|
|
3727
|
+
values: u.data
|
|
3728
|
+
}))
|
|
3729
|
+
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
3730
|
+
} : void 0,
|
|
3731
|
+
deleteMany: driver.deleteMany ? async (ids) => {
|
|
3732
|
+
await driver.deleteMany({
|
|
3733
|
+
path: slug,
|
|
3734
|
+
ids
|
|
3735
|
+
});
|
|
3736
|
+
} : void 0,
|
|
3401
3737
|
count: driver.count ? async (params) => {
|
|
3402
3738
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3403
3739
|
return driver.count({
|
|
3404
3740
|
path: slug,
|
|
3405
|
-
filter
|
|
3741
|
+
filter,
|
|
3742
|
+
logical: params?.logical,
|
|
3743
|
+
searchString: params?.searchString
|
|
3406
3744
|
});
|
|
3407
3745
|
} : void 0,
|
|
3408
3746
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
3409
|
-
const limit = params
|
|
3410
|
-
const offset = params?.offset ?? 0;
|
|
3747
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3411
3748
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
3412
3749
|
return driver.listenCollection({
|
|
3413
3750
|
path: slug,
|
|
3414
|
-
limit
|
|
3415
|
-
offset:
|
|
3751
|
+
limit,
|
|
3752
|
+
offset: driverOffset,
|
|
3416
3753
|
filter: params?.where,
|
|
3754
|
+
logical: params?.logical,
|
|
3417
3755
|
orderBy: params?.orderBy?.[0],
|
|
3418
3756
|
order: params?.orderBy?.[1],
|
|
3419
3757
|
searchString: params?.searchString,
|
|
@@ -3421,7 +3759,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3421
3759
|
onUpdate({
|
|
3422
3760
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
3423
3761
|
meta: {
|
|
3424
|
-
total: entities.length,
|
|
3762
|
+
total: offset + entities.length,
|
|
3425
3763
|
limit,
|
|
3426
3764
|
offset,
|
|
3427
3765
|
hasMore: entities.length >= limit
|
|
@@ -3598,9 +3936,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3598
3936
|
async update(id, data) {
|
|
3599
3937
|
return entityToRow(await snap.update(id, data));
|
|
3600
3938
|
},
|
|
3939
|
+
async updateMany(updates) {
|
|
3940
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
3941
|
+
if (updates.length === 0) return [];
|
|
3942
|
+
if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
|
|
3943
|
+
return (await snap.updateMany(updates.map((u) => ({
|
|
3944
|
+
id: u.id,
|
|
3945
|
+
data: u.data
|
|
3946
|
+
})))).map(entityToRow);
|
|
3947
|
+
},
|
|
3601
3948
|
delete(id) {
|
|
3602
3949
|
return snap.delete(id);
|
|
3603
3950
|
},
|
|
3951
|
+
async deleteMany(ids) {
|
|
3952
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
3953
|
+
if (ids.length === 0) return;
|
|
3954
|
+
if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
|
|
3955
|
+
await snap.deleteMany(ids);
|
|
3956
|
+
},
|
|
3604
3957
|
count: snap.count ? (params) => snap.count(params) : void 0,
|
|
3605
3958
|
listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
|
|
3606
3959
|
data: res.data.map(entityToRow),
|
|
@@ -3623,7 +3976,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
|
|
|
3623
3976
|
/**
|
|
3624
3977
|
* Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
|
|
3625
3978
|
* {@link CollectionAccessor}. Every returned row is re-wrapped into the
|
|
3626
|
-
* `{ id, path, values }` view-model the admin
|
|
3979
|
+
* `{ id, path, values }` view-model the admin panel renders.
|
|
3627
3980
|
*/
|
|
3628
3981
|
function toEntityAccessor(sdk, slug, getPks = () => []) {
|
|
3629
3982
|
const accessor = {
|
|
@@ -3677,6 +4030,15 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
|
|
|
3677
4030
|
* admin `RebaseDataContext` — without it the admin renders rows with only their
|
|
3678
4031
|
* `id`.
|
|
3679
4032
|
*/
|
|
4033
|
+
/**
|
|
4034
|
+
* Only the by-slug accessor is asked for, so only that is required.
|
|
4035
|
+
*
|
|
4036
|
+
* Taking a whole `RebaseSdkData` meant taking `RebaseSdkData<unknown>`, whose
|
|
4037
|
+
* dynamic branch is an index signature — and no `RebaseSdkData<DB>` satisfies
|
|
4038
|
+
* it, because its own `collection` method is not a `SDKCollectionClient`. So a
|
|
4039
|
+
* caller holding a *typed* client could not pass it to a function that reads
|
|
4040
|
+
* one method off it, and that method is identical on every instantiation.
|
|
4041
|
+
*/
|
|
3680
4042
|
function wrapAsEntityData(sdkData, options) {
|
|
3681
4043
|
const cache = /* @__PURE__ */ new Map();
|
|
3682
4044
|
const primaryKeysFor = createPrimaryKeyResolver(options);
|
|
@@ -3906,6 +4268,6 @@ async function detectJunctionTables(executeSql) {
|
|
|
3906
4268
|
return junctionTables;
|
|
3907
4269
|
}
|
|
3908
4270
|
//#endregion
|
|
3909
|
-
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, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, 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, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
4271
|
+
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, DEFAULT_STRING_COLUMN_LENGTH, JUNCTION_TABLES_SQL, MAX_LOGICAL_NESTING_DEPTH, QueryBuilder, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_TABLES, REBASE_USER_ROLE, RebasePaginationError, and, buildCollectionFromTableMetadata, buildCompositeId, buildConditionContext, 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, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getColumnName, getDeclaredPrimaryKeys, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEffectiveSecurityRules, getEntityChildViews, getEnumVarName, getGeneratedPolicyNames, getInjectedSecurityRules, getJunctionCollectionConfig, getJunctionSecurityRules, getLabelOrConfigFrom, getPrimaryKeys, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, getTableVarName, isAddressableId, isJunctionBackedRelation, isPropertyBuilder, isRebaseInternalTable, isRelationalCollection, normalizeEmail, normalizeToEntityRelation, or, paginateFind, parseIdValues, policyToPostgres, registerConditionOperations, relationalCollections, resolveArrayProperties, resolveCollectionRelations, resolveDataSource, resolveEnumValues, resolveFindWindow, resolveJunctionSpecs, resolvePrimaryKeys, resolveProperties, resolveProperty, resolvePropertyEnum, resolveRelation, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, resolveStringColumnLength, revokeInternalTableAccess, revokeInternalTableSql, sanitizeData, securityRuleToConditions, segmentsToStrippedPath, serializeFilter, serializeLogicalCondition, serializeOrderBy, sortProperties, sqlToPolicy, stripCollectionPath, traverseValueProperty, traverseValuesProperties, updateDateAutoValues, wrapAsEntityData, wrapAsSdkData };
|
|
3910
4272
|
|
|
3911
4273
|
//# sourceMappingURL=index.es.js.map
|