@rebasepro/common 0.13.0 → 0.13.1-canary.g249daa1

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,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 = auth.uid())`
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 = auth.uid())`
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*auth\.roles\(\)\s*,\s*','\s*\)\s*&&\s*ARRAY\s*\[(.+)\]$/i);
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*auth\.roles\(\)\s*,\s*','\s*\)\s*@>\s*ARRAY\s*\[(.+)\]$/i);
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(sql);
1125
+ return policy.raw(trimmed);
1035
1126
  }
1036
1127
  /**
1037
- * Literals from other BaaS platforms that people compare `auth.uid()` against
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
- /** `auth.uid() IS NOT NULL` in raw SQL, the clause that is always true. */
1049
- var UID_NOT_NULL = /auth\.uid\(\)\s+IS\s+NOT\s+NULL/i;
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()` really
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
- * - `auth.uid() IS NOT NULL` is a tautology on the user path, and
1059
- * - `auth.uid() != 'anon'` excludes one spelling of anonymous and admits the
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: `\`auth.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".`
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) || /auth\.uid\(\)/i.test(str)) return policy.authUid();
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(auth.roles(), ',') && ${rolesArraySql(expr.roles)}`;
1196
- case "rolesContain": return `string_to_array(auth.roles(), ',') @> ${rolesArraySql(expr.roles)}`;
1197
- case "authenticated": return `auth.uid() IS NOT NULL AND auth.uid() NOT IN (${ANONYMOUS_USER_IDS.map(quoteLiteral).join(", ")})`;
1198
- case "serverContext": return "auth.uid() IS NULL";
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 "auth.uid()";
1238
- case "authRoles": return "string_to_array(auth.roles(), ',')";
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 JSON Logic rule against the given context.
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 {
@@ -2788,8 +3067,24 @@ var QueryBuilder = class {
2788
3067
  /**
2789
3068
  * Set a free-text search string if supported by the backend.
2790
3069
  */
2791
- search(searchString) {
3070
+ search(searchString, options) {
2792
3071
  this.params.searchString = searchString;
3072
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
3073
+ return this;
3074
+ }
3075
+ /**
3076
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
3077
+ *
3078
+ * Postgres only, over a property declared as `type: "vector"`. Rows come
3079
+ * back with a `_distance`; `where` filters before the ordering.
3080
+ */
3081
+ vectorSearch(property, vector, options) {
3082
+ this.params.vectorSearch = {
3083
+ property,
3084
+ vector,
3085
+ ...options?.distance !== void 0 && { distance: options.distance },
3086
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
3087
+ };
2793
3088
  return this;
2794
3089
  }
2795
3090
  /**
@@ -2861,6 +3156,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
2861
3156
  Object.setPrototypeOf(this, RebasePaginationError.prototype);
2862
3157
  }
2863
3158
  };
3159
+ /**
3160
+ * Resolve `limit`/`offset`/`page` into the window a read will actually use.
3161
+ *
3162
+ * Lives here, next to the walk, for the reason at the top of this file: every
3163
+ * transport has to mean the same thing by "page two". Four of them did not —
3164
+ * the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
3165
+ * evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
3166
+ * the published type documented a fourth number. Pages that overlap or skip
3167
+ * rows are the mildest of those outcomes.
3168
+ *
3169
+ * `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
3170
+ * is the value to hand a driver: it stays `undefined` when the caller named no
3171
+ * offset, because keyset pagination seeks with a `where` clause and must not
3172
+ * look like it is paging by offset.
3173
+ */
3174
+ function resolveFindWindow(params) {
3175
+ const limit = params?.limit ?? DEFAULT_LIST_LIMIT;
3176
+ const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
3177
+ return {
3178
+ limit,
3179
+ offset,
3180
+ driverOffset: params?.page != null ? offset : params?.offset
3181
+ };
3182
+ }
2864
3183
  function normalizePageSize(raw) {
2865
3184
  if (raw === void 0 || !Number.isFinite(raw)) return 200;
2866
3185
  return Math.max(1, Math.floor(raw));
@@ -3194,7 +3513,22 @@ function serializeLogicalCondition(cond) {
3194
3513
  * deserializeLogicalCondition("or(status.eq.active,age.gte.18)")
3195
3514
  * // → { type: "or", conditions: [...] }
3196
3515
  */
3197
- function deserializeLogicalCondition(str) {
3516
+ /**
3517
+ * How deeply `or(...)`/`and(...)` groups may nest.
3518
+ *
3519
+ * This parser recurses once per level, on a value that arrives in a query
3520
+ * string. Unbounded, twenty thousand levels reached `RangeError: Maximum call
3521
+ * stack size exceeded`, which a caller sees as a 500 about the call stack
3522
+ * rather than a 400 about their filter. Node's 16 KB header cap keeps a GET
3523
+ * below that in practice, but "the HTTP layer happens to stop it" is not a
3524
+ * bound this parser should rely on.
3525
+ *
3526
+ * Thirty-two is far past anything a real filter expresses; the deepest in this
3527
+ * repository's own tests is three.
3528
+ */
3529
+ var MAX_LOGICAL_NESTING_DEPTH = 32;
3530
+ function deserializeLogicalCondition(str, nesting = 0) {
3531
+ 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
3532
  const logicalMatch = str.match(/^(and|or)\((.+)\)$/);
3199
3533
  if (logicalMatch) {
3200
3534
  const type = logicalMatch[1];
@@ -3205,10 +3539,10 @@ function deserializeLogicalCondition(str) {
3205
3539
  for (let i = 0; i < innerStr.length; i++) if (innerStr[i] === "(") depth++;
3206
3540
  else if (innerStr[i] === ")") depth--;
3207
3541
  else if (innerStr[i] === "," && depth === 0) {
3208
- conditions.push(deserializeLogicalCondition(innerStr.slice(start, i)));
3542
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start, i), nesting + 1));
3209
3543
  start = i + 1;
3210
3544
  }
3211
- conditions.push(deserializeLogicalCondition(innerStr.slice(start)));
3545
+ conditions.push(deserializeLogicalCondition(innerStr.slice(start), nesting + 1));
3212
3546
  return {
3213
3547
  type,
3214
3548
  conditions
@@ -3276,10 +3610,12 @@ function createPrimaryKeyResolver(options) {
3276
3610
  * than postgres still serve rows with one, and this keeps them working.
3277
3611
  */
3278
3612
  function rowToEntity(row, slug, primaryKeys = []) {
3613
+ const { _matches, ...values } = row;
3279
3614
  return {
3280
3615
  id: primaryKeys.length > 0 ? buildCompositeId(row, primaryKeys) : row.id,
3281
3616
  path: slug,
3282
- values: row
3617
+ values,
3618
+ ..._matches ? { searchMatches: _matches } : {}
3283
3619
  };
3284
3620
  }
3285
3621
  /**
@@ -3322,21 +3658,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3322
3658
  const accessor = {
3323
3659
  async find(params) {
3324
3660
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3325
- const limit = params?.limit ?? 20;
3326
- const offset = params?.offset ?? 0;
3661
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3327
3662
  const fetchService = driver.restFetchService;
3328
3663
  const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
3329
3664
  filter,
3330
- limit: params?.limit,
3331
- offset: params?.offset,
3665
+ logical: params?.logical,
3666
+ limit,
3667
+ offset: driverOffset,
3332
3668
  orderBy: params?.orderBy?.[0],
3333
3669
  order: params?.orderBy?.[1],
3334
3670
  searchString: params?.searchString
3335
3671
  }, params?.include) : await driver.fetchCollection({
3336
3672
  path: slug,
3337
- limit: params?.limit,
3338
- offset: params?.offset,
3673
+ limit,
3674
+ offset: driverOffset,
3339
3675
  filter,
3676
+ logical: params?.logical,
3340
3677
  orderBy: params?.orderBy?.[0],
3341
3678
  order: params?.orderBy?.[1],
3342
3679
  searchString: params?.searchString
@@ -3346,7 +3683,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3346
3683
  if (driver.count) {
3347
3684
  total = await driver.count({
3348
3685
  path: slug,
3349
- filter
3686
+ filter,
3687
+ logical: params?.logical,
3688
+ searchString: params?.searchString
3350
3689
  });
3351
3690
  hasMore = offset + rows.length < total;
3352
3691
  }
@@ -3398,30 +3737,48 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3398
3737
  values: {}
3399
3738
  } });
3400
3739
  },
3740
+ updateMany: driver.updateMany ? async (updates) => {
3741
+ return (await driver.updateMany({
3742
+ path: slug,
3743
+ updates: updates.map((u) => ({
3744
+ id: u.id,
3745
+ values: u.data
3746
+ }))
3747
+ })).map((row) => rowToEntity(row, slug, getPks()));
3748
+ } : void 0,
3749
+ deleteMany: driver.deleteMany ? async (ids) => {
3750
+ await driver.deleteMany({
3751
+ path: slug,
3752
+ ids
3753
+ });
3754
+ } : void 0,
3401
3755
  count: driver.count ? async (params) => {
3402
3756
  const filter = params?.where ? deserializeFilter(params.where) : void 0;
3403
3757
  return driver.count({
3404
3758
  path: slug,
3405
- filter
3759
+ filter,
3760
+ logical: params?.logical,
3761
+ searchString: params?.searchString
3406
3762
  });
3407
3763
  } : void 0,
3408
3764
  listen: driver.listenCollection ? (params, onUpdate, onError) => {
3409
- const limit = params?.limit ?? 20;
3410
- const offset = params?.offset ?? 0;
3765
+ const { limit, offset, driverOffset } = resolveFindWindow(params);
3411
3766
  const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
3412
3767
  return driver.listenCollection({
3413
3768
  path: slug,
3414
- limit: params?.limit,
3415
- offset: params?.offset,
3769
+ limit,
3770
+ offset: driverOffset,
3416
3771
  filter: params?.where,
3772
+ logical: params?.logical,
3417
3773
  orderBy: params?.orderBy?.[0],
3418
3774
  order: params?.orderBy?.[1],
3419
3775
  searchString: params?.searchString,
3776
+ searchExplain: params?.searchExplain,
3420
3777
  onUpdate: (entities) => {
3421
3778
  onUpdate({
3422
3779
  data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
3423
3780
  meta: {
3424
- total: entities.length,
3781
+ total: offset + entities.length,
3425
3782
  limit,
3426
3783
  offset,
3427
3784
  hasMore: entities.length >= limit
@@ -3454,8 +3811,11 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
3454
3811
  offset(count) {
3455
3812
  return new QueryBuilder(accessor).offset(count);
3456
3813
  },
3457
- search(searchString) {
3458
- return new QueryBuilder(accessor).search(searchString);
3814
+ search(searchString, options) {
3815
+ return new QueryBuilder(accessor).search(searchString, options);
3816
+ },
3817
+ vectorSearch(property, vector, options) {
3818
+ return new QueryBuilder(accessor).vectorSearch(property, vector, options);
3459
3819
  },
3460
3820
  include(...relations) {
3461
3821
  return new QueryBuilder(accessor).include(...relations);
@@ -3543,8 +3903,18 @@ var SdkQueryBuilder = class {
3543
3903
  this.params.offset = count;
3544
3904
  return this;
3545
3905
  }
3546
- search(searchString) {
3906
+ search(searchString, options) {
3547
3907
  this.params.searchString = searchString;
3908
+ if (options?.explain !== void 0) this.params.searchExplain = options.explain;
3909
+ return this;
3910
+ }
3911
+ vectorSearch(property, vector, options) {
3912
+ this.params.vectorSearch = {
3913
+ property,
3914
+ vector,
3915
+ ...options?.distance !== void 0 && { distance: options.distance },
3916
+ ...options?.threshold !== void 0 && { threshold: options.threshold }
3917
+ };
3548
3918
  return this;
3549
3919
  }
3550
3920
  include(...relations) {
@@ -3598,9 +3968,24 @@ function toSdkCollectionClient(snap, slug = "collection") {
3598
3968
  async update(id, data) {
3599
3969
  return entityToRow(await snap.update(id, data));
3600
3970
  },
3971
+ async updateMany(updates) {
3972
+ if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
3973
+ if (updates.length === 0) return [];
3974
+ if (!snap.updateMany) throw new Error("Bulk updates are not supported by this collection's data source. Fall back to update() per record.");
3975
+ return (await snap.updateMany(updates.map((u) => ({
3976
+ id: u.id,
3977
+ data: u.data
3978
+ })))).map(entityToRow);
3979
+ },
3601
3980
  delete(id) {
3602
3981
  return snap.delete(id);
3603
3982
  },
3983
+ async deleteMany(ids) {
3984
+ if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
3985
+ if (ids.length === 0) return;
3986
+ if (!snap.deleteMany) throw new Error("Bulk deletes are not supported by this collection's data source. Fall back to delete() per record.");
3987
+ await snap.deleteMany(ids);
3988
+ },
3604
3989
  count: snap.count ? (params) => snap.count(params) : void 0,
3605
3990
  listen: snap.listen ? (params, onUpdate, onError) => snap.listen(params, (res) => onUpdate({
3606
3991
  data: res.data.map(entityToRow),
@@ -3616,6 +4001,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
3616
4001
  limit: (count) => new SdkQueryBuilder(client).limit(count),
3617
4002
  offset: (count) => new SdkQueryBuilder(client).offset(count),
3618
4003
  search: (searchString) => new SdkQueryBuilder(client).search(searchString),
4004
+ vectorSearch: (property, vector, options) => new SdkQueryBuilder(client).vectorSearch(property, vector, options),
3619
4005
  include: (...relations) => new SdkQueryBuilder(client).include(...relations)
3620
4006
  };
3621
4007
  return client;
@@ -3623,7 +4009,7 @@ function toSdkCollectionClient(snap, slug = "collection") {
3623
4009
  /**
3624
4010
  * Wrap a flat {@link SDKCollectionClient} into a Entity-shaped
3625
4011
  * {@link CollectionAccessor}. Every returned row is re-wrapped into the
3626
- * `{ id, path, values }` view-model the admin admin renders.
4012
+ * `{ id, path, values }` view-model the admin panel renders.
3627
4013
  */
3628
4014
  function toEntityAccessor(sdk, slug, getPks = () => []) {
3629
4015
  const accessor = {
@@ -3664,6 +4050,7 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
3664
4050
  limit: (count) => new QueryBuilder(accessor).limit(count),
3665
4051
  offset: (count) => new QueryBuilder(accessor).offset(count),
3666
4052
  search: (searchString) => new QueryBuilder(accessor).search(searchString),
4053
+ vectorSearch: (property, vector, options) => new QueryBuilder(accessor).vectorSearch(property, vector, options),
3667
4054
  include: (...relations) => new QueryBuilder(accessor).include(...relations)
3668
4055
  };
3669
4056
  return accessor;
@@ -3677,6 +4064,15 @@ function toEntityAccessor(sdk, slug, getPks = () => []) {
3677
4064
  * admin `RebaseDataContext` — without it the admin renders rows with only their
3678
4065
  * `id`.
3679
4066
  */
4067
+ /**
4068
+ * Only the by-slug accessor is asked for, so only that is required.
4069
+ *
4070
+ * Taking a whole `RebaseSdkData` meant taking `RebaseSdkData<unknown>`, whose
4071
+ * dynamic branch is an index signature — and no `RebaseSdkData<DB>` satisfies
4072
+ * it, because its own `collection` method is not a `SDKCollectionClient`. So a
4073
+ * caller holding a *typed* client could not pass it to a function that reads
4074
+ * one method off it, and that method is identical on every instantiation.
4075
+ */
3680
4076
  function wrapAsEntityData(sdkData, options) {
3681
4077
  const cache = /* @__PURE__ */ new Map();
3682
4078
  const primaryKeysFor = createPrimaryKeyResolver(options);
@@ -3906,6 +4302,6 @@ async function detectJunctionTables(executeSql) {
3906
4302
  return junctionTables;
3907
4303
  }
3908
4304
  //#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 };
4305
+ 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
4306
 
3911
4307
  //# sourceMappingURL=index.es.js.map