@rebasepro/server-postgres 0.14.1 → 0.14.2-canary.g27a129e

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.
Files changed (29) hide show
  1. package/dist/{auth-users-columns-C-FDnL_e.js → auth-users-columns-JJ8ngvy5.js} +59 -17
  2. package/dist/auth-users-columns-JJ8ngvy5.js.map +1 -0
  3. package/dist/{backup-service-BH0Dzo_h.js → backup-service-czK-OAuG.js} +2 -2
  4. package/dist/{backup-service-BH0Dzo_h.js.map → backup-service-czK-OAuG.js.map} +1 -1
  5. package/dist/{ensure-collection-policies-DoHwhVf8.js → ensure-collection-policies-D5PtQLyR.js} +3 -3
  6. package/dist/{ensure-collection-policies-DoHwhVf8.js.map → ensure-collection-policies-D5PtQLyR.js.map} +1 -1
  7. package/dist/{ensure-collection-tables-DT2eq859.js → ensure-collection-tables-BHUjQ-z4.js} +3 -3
  8. package/dist/{ensure-collection-tables-DT2eq859.js.map → ensure-collection-tables-BHUjQ-z4.js.map} +1 -1
  9. package/dist/index.es.js +485 -19
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/{rls-bootstrap-sql-69hYT8nr.js → rls-bootstrap-sql-B5Sajku6.js} +2 -2
  12. package/dist/{rls-bootstrap-sql-69hYT8nr.js.map → rls-bootstrap-sql-B5Sajku6.js.map} +1 -1
  13. package/dist/{rls-enforcement-gUNDfm7l.js → rls-enforcement-BDBfuTD4.js} +4 -3
  14. package/dist/rls-enforcement-BDBfuTD4.js.map +1 -0
  15. package/dist/services/FetchService.d.ts +22 -0
  16. package/dist/{src-DCdn3Val.js → src-BBFsDaeA.js} +60 -2
  17. package/dist/src-BBFsDaeA.js.map +1 -0
  18. package/dist/utils/drizzle-conditions.d.ts +168 -1
  19. package/dist/utils/pg-error-utils.d.ts +27 -0
  20. package/dist/{websocket-D2jXv0Ds.js → websocket-BVgDVO-V.js} +2 -2
  21. package/dist/{websocket-D2jXv0Ds.js.map → websocket-BVgDVO-V.js.map} +1 -1
  22. package/package.json +6 -6
  23. package/src/services/FetchService.ts +155 -15
  24. package/src/services/PersistService.ts +23 -2
  25. package/src/utils/drizzle-conditions.ts +594 -1
  26. package/src/utils/pg-error-utils.ts +49 -4
  27. package/dist/auth-users-columns-C-FDnL_e.js.map +0 -1
  28. package/dist/rls-enforcement-gUNDfm7l.js.map +0 -1
  29. package/dist/src-DCdn3Val.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -3,12 +3,12 @@ import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
5
5
  import { n as resolveClientListLimit, t as ListLimitError } from "./data_driver-ULAyJEi9.js";
6
- import { $ as mergeDeep, A as fieldKeyForColumn, B as parseIdValues, C as getJunctionCollectionConfig, D as policyToPostgres, E as getEffectiveSecurityRules, F as getTableVarName, G as updateDateAutoValues, H as createRelationRef, I as resolveCollectionRelations, J as legacyForeignKeyName, K as firstFreeKey, L as buildCompositeId, M as getColumnName, N as getEnumVarName, O as securityRuleToConditions, P as getTableName$1, Q as isPrototypePollutingKey, R as getDeclaredPrimaryKeys, S as resolveStringColumnLength, T as resolveJunctionSpecs, U as createRelationRefWithData, V as sortCollectionsBySlug, W as normalizeToEntityRelation, X as toWireKey, Z as getPolicyNamesForRule, _ as OrderBySpecError, at as isManyToMany, b as CollectionRegistry, et as camelCase, g as buildSdkData, h as visibleColumnProjection, it as hasForeignKeyOnTarget, j as findRelation, l as buildSearchColumnSpec, ot as Vector, q as generateForeignKeyName, r as authUsersColumnSql, s as SEARCH_UNACCENT_FN, t as AUTH_USERS_COLUMNS, tt as toSnakeCase, u as hiddenColumnsOption, v as normalizeDriverOrderBy, w as getJunctionSecurityRules, x as relationalCollections, y as parseOrderBySpecStrict, z as isAddressableId } from "./auth-users-columns-C-FDnL_e.js";
7
- import { c as isPostgresCollectionConfig, f as ALL_WHERE_FILTER_OPS, l as isRelationalCollectionConfig } from "./src-DCdn3Val.js";
8
- import { t as createPostgresWebSocket } from "./websocket-D2jXv0Ds.js";
9
- import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-gUNDfm7l.js";
10
- import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-BH0Dzo_h.js";
11
- import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-69hYT8nr.js";
6
+ import { $ as mergeDeep, A as fieldKeyForColumn, B as parseIdValues, C as getJunctionCollectionConfig, D as policyToPostgres, E as getEffectiveSecurityRules, F as getTableVarName, G as updateDateAutoValues, H as createRelationRef, I as resolveCollectionRelations, J as legacyForeignKeyName, K as firstFreeKey, L as buildCompositeId, M as getColumnName, N as getEnumVarName, O as securityRuleToConditions, P as getTableName$1, Q as isPrototypePollutingKey, R as getDeclaredPrimaryKeys, S as resolveStringColumnLength, T as resolveJunctionSpecs, U as createRelationRefWithData, V as sortCollectionsBySlug, W as normalizeToEntityRelation, X as toWireKey, Z as getPolicyNamesForRule, _ as OrderBySpecError, at as isManyToMany, b as CollectionRegistry, et as camelCase, g as buildSdkData, h as visibleColumnProjection, it as hasForeignKeyOnTarget, j as findRelation, l as buildSearchColumnSpec, ot as Vector, q as generateForeignKeyName, r as authUsersColumnSql, s as SEARCH_UNACCENT_FN, t as AUTH_USERS_COLUMNS, tt as toSnakeCase, u as hiddenColumnsOption, v as normalizeDriverOrderBy, w as getJunctionSecurityRules, x as relationalCollections, y as parseOrderBySpecStrict, z as isAddressableId } from "./auth-users-columns-JJ8ngvy5.js";
7
+ import { c as isPostgresCollectionConfig, f as ALL_WHERE_FILTER_OPS, g as encodeRelationAggregateSort, l as isRelationalCollectionConfig, v as parseRelationAggregateSort } from "./src-BBFsDaeA.js";
8
+ import { t as createPostgresWebSocket } from "./websocket-BVgDVO-V.js";
9
+ import { a as warnOnAnonymousGrants, c as REBASE_USER_ROLE, i as validatePolicyPgRoles, l as revokeInternalTableAccess, n as detectConnectionPosture, o as warnOnLegacyRlsFunctions, r as ensureAppRole, s as warnOnRoleSchemaCollision, t as applyAuthContext, u as revokeInternalTableSql } from "./rls-enforcement-BDBfuTD4.js";
10
+ import { A as parsePgToolMajor, C as checkToolServerCompatibility, D as parseBackupDestination, E as joinStorageKey, M as serverVersionNumToMajor, N as splitGlobalsStatements, O as parseBackupTimestamp, P as withDatabaseName, S as buildRowSecurityPgOptions, T as globalsFileForDump, _ as buildBackupFilename, a as detectToolMajor, b as buildPgRestoreArgs, c as listBackups, d as resolvePgBinary, f as restoreDump, g as selectBackupsToPrune, h as require_source, i as createDump, j as resolveConnectionString, k as parseDbNameFromUrl, l as preflight, m as validateDump, n as applyGlobals, o as ensureDatabaseExists, p as uploadBackup, s as getServerVersionMajor, t as BackupToolError, u as pruneBackups, v as buildPgDumpArgs, w as diagnoseRowSecurityDumpFailure, x as buildPgRestoreListArgs, y as buildPgDumpallGlobalsArgs } from "./backup-service-czK-OAuG.js";
11
+ import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-B5Sajku6.js";
12
12
  import { Client, Pool } from "pg";
13
13
  import { drizzle } from "drizzle-orm/node-postgres";
14
14
  import { ApiError, createDdlBootstrapper, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
@@ -552,6 +552,22 @@ var targetOf = (relation) => {
552
552
  return;
553
553
  }
554
554
  };
555
+ /**
556
+ * The SQL each aggregate sort function compiles to.
557
+ *
558
+ * A lookup rather than interpolation, and `sql.raw` applied to five constants
559
+ * written out here rather than to anything derived from a request. The key is
560
+ * already validated by `parseRelationAggregateSort` — this is the second lock
561
+ * on the same door, so that a future caller reaching the builder directly
562
+ * cannot put a string from the wire where a function name goes.
563
+ */
564
+ var AGGREGATE_SQL = {
565
+ min: sql.raw("min"),
566
+ max: sql.raw("max"),
567
+ count: sql.raw("count"),
568
+ sum: sql.raw("sum"),
569
+ avg: sql.raw("avg")
570
+ };
555
571
  /** Column types `ILIKE '%…%'` is defined on. */
556
572
  var ILIKE_SQL_TYPES = /^(text|varchar|character varying|char|character|bpchar|citext)\b/;
557
573
  /**
@@ -616,6 +632,27 @@ function getUnknownFilterFieldsMode() {
616
632
  * of habit, and refusing it would be pedantry about a distinction this layer
617
633
  * erases anyway.
618
634
  */
635
+ /**
636
+ * Split `applications.status` into the relation and the column it addresses.
637
+ *
638
+ * One dot, and only the first one: a relation name cannot contain a dot, and
639
+ * everything after it is handed to the target as a single column key. A second
640
+ * dot would be a second hop — `talents.applications.job.title` — which is a
641
+ * different feature (it needs a chain of subqueries, and a chain has to decide
642
+ * what "some" means at every level), so it is refused rather than silently read
643
+ * as a column named `job.title` that no table has.
644
+ *
645
+ * Returns `undefined` for a field with no dot, which leaves every existing
646
+ * filter on exactly the path it took before.
647
+ */
648
+ function parseRelationFieldPath(field) {
649
+ const dot = field.indexOf(".");
650
+ if (dot <= 0 || dot === field.length - 1) return void 0;
651
+ return {
652
+ relationKey: field.slice(0, dot),
653
+ fieldKey: field.slice(dot + 1)
654
+ };
655
+ }
619
656
  function parseJsonFieldPath(field) {
620
657
  if (!field.includes("->")) return void 0;
621
658
  const segments = field.split(/->>?/).map((s) => s.trim()).filter(Boolean);
@@ -819,6 +856,11 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
819
856
  sourceIdColumn: correlationColumn
820
857
  };
821
858
  }
859
+ const relationField = parseRelationFieldPath(field);
860
+ if (relationField && registry && sourceIdColumn) {
861
+ const target = this.resolveRelationFieldTarget(table, relationField, collection, registry, sourceIdColumn, field, collectionPath);
862
+ if (target) return target;
863
+ }
822
864
  }
823
865
  for (const guess of [
824
866
  `${field}Id`,
@@ -847,6 +889,59 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
847
889
  });
848
890
  }
849
891
  /**
892
+ * `applications.status` — the relation, and the column of the target it
893
+ * addresses.
894
+ *
895
+ * `undefined` when the first segment names no relation: the field simply is
896
+ * not a relation path, and resolution carries on to the guesses and then to
897
+ * the unknown-field answer, which is where a typo belongs. A segment that
898
+ * *does* name a relation is a different matter — the author plainly meant
899
+ * this shape — so everything after that point throws rather than returning,
900
+ * naming what went wrong. Falling through would report "unknown filter
901
+ * field 'applications.status'" and list the columns of the wrong table.
902
+ *
903
+ * `via` is refused for the reason it is absent from
904
+ * `filterableRelationKinds`: its join path is authored source → target with
905
+ * no stated inverse, so there is nothing to correlate a subquery back to.
906
+ */
907
+ static resolveRelationFieldTarget(table, path, collection, registry, sourceIdColumn, field, collectionPath) {
908
+ const relation = resolveCollectionRelations(collection)[path.relationKey];
909
+ if (!relation) return void 0;
910
+ if (relation.kind === "via") throw ApiError.badRequest(`Cannot filter by '${field}' on collection '${collectionPath}': '${path.relationKey}' is a \`via\` relation, whose join path is authored one way only, so there is nothing to correlate a subquery back to.`, "UNSUPPORTED_RELATION_FILTER", {
911
+ field,
912
+ collection: collectionPath,
913
+ relation: path.relationKey,
914
+ kind: relation.kind
915
+ });
916
+ const targetCollection = targetOf(relation);
917
+ const targetTable = targetCollection && registry.getTable(getTableName$1(targetCollection));
918
+ if (!targetCollection || !targetTable) throw new Error(`Table not found for the target of relation '${relation.relationName}' on '${collectionPath}', so '${field}' has nothing to filter against.`);
919
+ const targetColumn = relationColumn(targetTable, targetCollection, path.fieldKey) ?? (path.fieldKey in targetTable ? targetTable[path.fieldKey] : void 0);
920
+ if (!targetColumn) {
921
+ let validFields = [];
922
+ try {
923
+ validFields = Object.keys(getTableColumns(targetTable)).sort();
924
+ } catch {}
925
+ throw ApiError.badRequest(`Unknown field '${path.fieldKey}' on '${targetCollection.slug}', the target of relation '${path.relationKey}' on collection '${collectionPath}'` + (validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""), "UNKNOWN_FILTER_FIELD", {
926
+ field,
927
+ collection: collectionPath,
928
+ relation: path.relationKey,
929
+ targetCollection: targetCollection.slug,
930
+ ...validFields.length > 0 && { validFields }
931
+ });
932
+ }
933
+ const correlationColumn = relation.kind === "belongsTo" ? relationColumn(table, collection, relation.localKey) : hasForeignKeyOnTarget(relation) && relation.sourceKey ? relationColumn(table, collection, relation.sourceKey) : sourceIdColumn;
934
+ if (!correlationColumn) throw new Error(`Relation '${relation.relationName}' on '${collectionPath}' names a key that is not a column there, so '${field}' has nothing to correlate against.`);
935
+ return {
936
+ kind: "relation-field",
937
+ relation,
938
+ registry,
939
+ sourceIdColumn: correlationColumn,
940
+ targetTable,
941
+ targetColumn
942
+ };
943
+ }
944
+ /**
850
945
  * Build filter conditions from FilterValues
851
946
  */
852
947
  static buildFilterConditions(filter, table, collectionPath, options = {}) {
@@ -881,6 +976,7 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
881
976
  static compileFilterTarget(target, op, value, field, collectionPath) {
882
977
  if (target.kind === "column") return this.buildSingleFilterCondition(target.column, op, value);
883
978
  if (target.kind === "json") return this.buildJsonPathCondition(target.column, target.path, op, value);
979
+ if (target.kind === "relation-field") return this.buildRelationFieldCondition(target, op, value, field, collectionPath);
884
980
  return this.buildRelationFilterCondition(target.relation, op, value, target.sourceIdColumn, target.registry, field, collectionPath);
885
981
  }
886
982
  /**
@@ -1095,6 +1191,297 @@ var DrizzleConditionBuilder = class DrizzleConditionBuilder {
1095
1191
  });
1096
1192
  }
1097
1193
  }
1194
+ /**
1195
+ * A filter on a *column of the related row* — `applications.status`.
1196
+ *
1197
+ * The same `EXISTS` {@link buildRelationFilterCondition} builds, with the
1198
+ * predicate moved off the target's id and onto one of its columns:
1199
+ *
1200
+ * EXISTS (SELECT 1 FROM talent_applications AS t
1201
+ * WHERE t.talent_id = talents.id
1202
+ * AND t.status IN ('applied', 'reviewing', 'interview'))
1203
+ *
1204
+ * which is the shape every "who is waiting" queue is written in. Without
1205
+ * it the only way to ask is to fetch every row and filter in the browser,
1206
+ * and a filter the client applies after paging is not a filter — the page
1207
+ * was already chosen without it.
1208
+ *
1209
+ * A many-to-many needs one more table than the id filter does. That one
1210
+ * stops at the junction, because the junction already holds the value it
1211
+ * compares; a column of the target is a table further out, so the subquery
1212
+ * joins the target to the junction and correlates from the junction. The
1213
+ * join is inside `EXISTS`, so it cannot multiply the outer rows the way a
1214
+ * top-level join through a junction would.
1215
+ *
1216
+ * `belongsTo` is included even though its foreign key is a column here:
1217
+ * `author.name` is a column of another table either way, and refusing the
1218
+ * one relation kind that reads most naturally would be a rule about
1219
+ * implementation rather than about meaning.
1220
+ *
1221
+ * Under RLS the subquery runs as the reader, so it sees the target rows
1222
+ * that reader's policies allow and no others. On the positive direction
1223
+ * that is exactly right. On the negative — `!=`, `not-in`, and any
1224
+ * `NOT EXISTS` — "no related row satisfies this" and "no related row this
1225
+ * reader can see satisfies this" are the same sentence, so a target table
1226
+ * with row-level security and no `SELECT` policy for `rebase_user` makes
1227
+ * every row look unmatched and the negative filter over-reports. Nothing is
1228
+ * leaked: the outer table's own policies still decide which rows exist. The
1229
+ * cause is a missing policy on the target rather than anything here, and it
1230
+ * is the same caveat the id-filter path carries.
1231
+ */
1232
+ static buildRelationFieldCondition(target, op, value, field, collectionPath) {
1233
+ const alias = "__rel_field";
1234
+ const { relation, targetTable, targetColumn } = target;
1235
+ const ref = sql`${sql.identifier(alias)}.${sql.identifier(targetColumn.name)}`;
1236
+ const { predicate, negate } = this.buildRelationColumnPredicate(ref, targetColumn, op, value, field, collectionPath);
1237
+ let from;
1238
+ let correlation;
1239
+ if (relation.kind === "manyToMany") {
1240
+ const { table: junctionName, sourceColumn, targetColumn: junctionTargetColumn } = relation.through;
1241
+ const junctionTable = target.registry.getTable(junctionName);
1242
+ if (!junctionTable) throw new Error(`Junction table not found: ${junctionName}`);
1243
+ const sourceCol = junctionTable[sourceColumn];
1244
+ const targetCol = junctionTable[junctionTargetColumn];
1245
+ if (!sourceCol || !targetCol) throw new Error(`Junction columns '${sourceColumn}'/'${junctionTargetColumn}' not found in '${junctionName}'`);
1246
+ const targetKey = this.primaryKeyColumn(targetTable);
1247
+ if (!targetKey) throw new Error(`No primary key or "id" column in the target table of relation '${relation.relationName}', so '${field}' has nothing to join the junction against.`);
1248
+ const junctionAlias = "__rel_field_junction";
1249
+ from = sql`${targetTable} AS ${sql.identifier(alias)} INNER JOIN ${junctionTable} AS ${sql.identifier(junctionAlias)} ON ${sql.identifier(junctionAlias)}.${sql.identifier(targetCol.name)} = ${sql.identifier(alias)}.${sql.identifier(targetKey.name)}`;
1250
+ correlation = sql`${sql.identifier(junctionAlias)}.${sql.identifier(sourceCol.name)} = ${target.sourceIdColumn}`;
1251
+ } else if (relation.kind === "belongsTo") {
1252
+ const targetKey = this.primaryKeyColumn(targetTable);
1253
+ if (!targetKey) throw new Error(`No primary key or "id" column in the target table of relation '${relation.relationName}', so '${field}' has nothing to correlate against.`);
1254
+ from = sql`${targetTable} AS ${sql.identifier(alias)}`;
1255
+ correlation = sql`${sql.identifier(alias)}.${sql.identifier(targetKey.name)} = ${target.sourceIdColumn}`;
1256
+ } else {
1257
+ const foreignKey = relationColumn(targetTable, targetOf(relation), relation.foreignKeyOnTarget);
1258
+ if (!foreignKey) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'.`);
1259
+ from = sql`${targetTable} AS ${sql.identifier(alias)}`;
1260
+ correlation = sql`${sql.identifier(alias)}.${sql.identifier(foreignKey.name)} = ${target.sourceIdColumn}`;
1261
+ }
1262
+ const where = predicate ? sql`${correlation} AND ${predicate}` : correlation;
1263
+ const exists = sql`EXISTS (SELECT 1 FROM ${from} WHERE ${where})`;
1264
+ return negate ? sql`NOT ${exists}` : exists;
1265
+ }
1266
+ /**
1267
+ * The inner predicate of a relation *column* filter, and whether the
1268
+ * `EXISTS` wrapping it is negated.
1269
+ *
1270
+ * The negation rule is the one {@link buildRelationFilterPredicate} states
1271
+ * and holds for exactly the same reason, one column over. A negative
1272
+ * operator is `NOT EXISTS` of the **positive** predicate, never `EXISTS` of
1273
+ * a negated one: `EXISTS (… AND status != 'hired')` asks "does some
1274
+ * application differ from hired", which is true of nearly every candidate
1275
+ * with more than one application and answers nothing anybody asked.
1276
+ * `NOT EXISTS (… AND status = 'hired')` asks "is there no hired
1277
+ * application", which is what unticking a value means — and it makes `==`
1278
+ * and `!=` partition the rows, the way a filter implies they do.
1279
+ *
1280
+ * `is-null` and `is-not-null` are the exception, and deliberately not a
1281
+ * complementary pair here. On a column they compile to `EXISTS (… AND col
1282
+ * IS NULL)` and `EXISTS (… AND col IS NOT NULL)` — "has a related row whose
1283
+ * column is unset" and "has one where it is set" — which is the plain
1284
+ * reading of `applications.status is-not-null` and the useful one. They are
1285
+ * both true of a candidate with two applications, one of each. Making
1286
+ * `is-not-null` the negation instead would make it "no application has an
1287
+ * unset status", which is true of a candidate with no applications at all
1288
+ * and so answers a queue with the very rows the queue exists to exclude.
1289
+ *
1290
+ * Unlike the id path, every operator is available: the compared value is an
1291
+ * ordinary column, so `>=` on a date and `ilike` on a name mean here what
1292
+ * they mean anywhere else. Only an operator that does not exist is refused,
1293
+ * and it throws rather than returning `null` — a dropped condition widens
1294
+ * the read, which is the whole reason this file fails closed.
1295
+ */
1296
+ static buildRelationColumnPredicate(ref, column, op, value, field, collectionPath) {
1297
+ value = unwrapRelationFilterValue(value);
1298
+ const isNullish = value === null || value === void 0;
1299
+ const equals = () => sql`${ref} = ${value}`;
1300
+ const inList = () => {
1301
+ const values = toMembershipList(value);
1302
+ return values.length === 0 ? sql`FALSE` : sql`${ref} IN (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`;
1303
+ };
1304
+ const isNull = () => sql`${ref} IS NULL`;
1305
+ const contains = () => {
1306
+ const meta = getColumnMeta(column);
1307
+ const isNativeArray = meta.dataType === "array" || meta.columnType === "PgArray";
1308
+ if (op === "array-contains-any") {
1309
+ if (Array.isArray(value) && value.length === 0) return sql`FALSE`;
1310
+ if (Array.isArray(value) && value.length > 0) return isNativeArray ? sql`${ref} && ARRAY[${sql.join(value.map((v) => sql`${v}`), sql`, `)}]` : sql`${ref} ?| array[${sql.join(value.map((v) => sql`${String(v)}`), sql`, `)}]`;
1311
+ }
1312
+ return isNativeArray ? sql`${ref} @> ARRAY[${value}]` : sql`${ref} @> ${JSON.stringify([value])}`;
1313
+ };
1314
+ switch (op) {
1315
+ case "==": return {
1316
+ predicate: isNullish ? isNull() : equals(),
1317
+ negate: false
1318
+ };
1319
+ case "!=": return {
1320
+ predicate: isNullish ? isNull() : equals(),
1321
+ negate: true
1322
+ };
1323
+ case ">": return {
1324
+ predicate: sql`${ref} > ${value}`,
1325
+ negate: false
1326
+ };
1327
+ case ">=": return {
1328
+ predicate: sql`${ref} >= ${value}`,
1329
+ negate: false
1330
+ };
1331
+ case "<": return {
1332
+ predicate: sql`${ref} < ${value}`,
1333
+ negate: false
1334
+ };
1335
+ case "<=": return {
1336
+ predicate: sql`${ref} <= ${value}`,
1337
+ negate: false
1338
+ };
1339
+ case "in": return {
1340
+ predicate: isNullish ? isNull() : inList(),
1341
+ negate: false
1342
+ };
1343
+ case "not-in": return {
1344
+ predicate: isNullish ? isNull() : inList(),
1345
+ negate: true
1346
+ };
1347
+ case "like": return {
1348
+ predicate: sql`${ref} LIKE ${String(value)}`,
1349
+ negate: false
1350
+ };
1351
+ case "not-like": return {
1352
+ predicate: sql`${ref} LIKE ${String(value)}`,
1353
+ negate: true
1354
+ };
1355
+ case "ilike":
1356
+ case "not-ilike":
1357
+ if (!supportsILike(column)) throw ApiError.badRequest(`Operator '${op}' cannot be applied to '${field}' on collection '${collectionPath}': '${column.name}' is ${column.getSQLType?.() ?? "not a text column"}, and case-insensitive matching is only defined on text.`, "UNSUPPORTED_RELATION_FILTER_OPERATOR", {
1358
+ field,
1359
+ collection: collectionPath,
1360
+ operator: op
1361
+ });
1362
+ return {
1363
+ predicate: sql`${ref} ILIKE ${String(value)}`,
1364
+ negate: op === "not-ilike"
1365
+ };
1366
+ case "is-null": return {
1367
+ predicate: isNull(),
1368
+ negate: false
1369
+ };
1370
+ case "is-not-null": return {
1371
+ predicate: sql`${ref} IS NOT NULL`,
1372
+ negate: false
1373
+ };
1374
+ case "array-contains":
1375
+ case "array-contains-any": return {
1376
+ predicate: contains(),
1377
+ negate: false
1378
+ };
1379
+ default: throw ApiError.badRequest(`Unknown filter operator '${op}'. Valid operators: ${ALL_WHERE_FILTER_OPS.join(", ")}.`, "UNKNOWN_FILTER_OPERATOR", {
1380
+ operator: op,
1381
+ validOperators: ALL_WHERE_FILTER_OPS
1382
+ });
1383
+ }
1384
+ }
1385
+ /**
1386
+ * An aggregate over the rows a relation reaches, as a scalar expression —
1387
+ * what `orderBy: [{ relation: "applications", field: "created_at", agg:
1388
+ * "min" }, "asc"]` compiles to.
1389
+ *
1390
+ * (SELECT min(t.created_at) FROM talent_applications AS t
1391
+ * WHERE t.talent_id = talents.id)
1392
+ *
1393
+ * A correlated scalar subquery rather than a `LEFT JOIN LATERAL`: the join
1394
+ * would have to be threaded into a query the relational query builder
1395
+ * assembles, while a scalar expression drops straight into `ORDER BY` and
1396
+ * into the keyset comparison behind cursor paging — which has to be the
1397
+ * *same* expression, or paging and ordering disagree and rows are skipped.
1398
+ *
1399
+ * `correlateTo` is what the subquery is pinned against. Left out, it is the
1400
+ * outer row's key column and the expression is correlated in the ordinary
1401
+ * way. Given a literal — the cursor row's id — the subquery stops being
1402
+ * correlated at all, so Postgres evaluates it once for the whole statement
1403
+ * rather than per row. That is how a cursor pages over an aggregate it has
1404
+ * no stored value for: the value is recomputed from the id it does have.
1405
+ *
1406
+ * Over zero related rows `count` is 0 and every other function is NULL,
1407
+ * which is what puts "nobody waiting" at a defined end of the order rather
1408
+ * than wherever a missing value would land. See `buildOrderExpressions` for
1409
+ * where that end is pinned.
1410
+ *
1411
+ * Under RLS the subquery runs as the reader, so a related row the reader
1412
+ * cannot see does not contribute — an aggregate is over the rows that
1413
+ * reader can see, which is the only total it could honestly report.
1414
+ */
1415
+ static buildRelationAggregateExpression(spec, table, collection, registry, sourceIdColumn, collectionPath, correlateTo) {
1416
+ const relation = resolveCollectionRelations(collection)[spec.relation];
1417
+ if (!relation) throw ApiError.badRequest(`Cannot sort by '${encodeRelationAggregateSort(spec)}' on collection '${collectionPath}': '${spec.relation}' is not a relation there.`, "UNKNOWN_ORDER_BY_FIELD", {
1418
+ field: encodeRelationAggregateSort(spec),
1419
+ collection: collectionPath,
1420
+ relation: spec.relation
1421
+ });
1422
+ if (relation.kind === "via") throw ApiError.badRequest(`Cannot sort by '${encodeRelationAggregateSort(spec)}' on collection '${collectionPath}': \`via\` relations are authored one way only, so there is nothing to correlate a subquery back to.`, "ORDER_BY_FIELD_NOT_SORTABLE", {
1423
+ field: encodeRelationAggregateSort(spec),
1424
+ collection: collectionPath,
1425
+ kind: relation.kind
1426
+ });
1427
+ const targetCollection = targetOf(relation);
1428
+ const targetTable = targetCollection && registry.getTable(getTableName$1(targetCollection));
1429
+ if (!targetCollection || !targetTable) throw new Error(`Table not found for the target of relation '${relation.relationName}' on '${collectionPath}', so there is nothing to aggregate.`);
1430
+ const alias = "__rel_agg";
1431
+ let aggregand = sql`*`;
1432
+ if (spec.field) {
1433
+ const column = relationColumn(targetTable, targetCollection, spec.field) ?? (spec.field in targetTable ? targetTable[spec.field] : void 0);
1434
+ if (!column) {
1435
+ let validFields = [];
1436
+ try {
1437
+ validFields = Object.keys(getTableColumns(targetTable)).sort();
1438
+ } catch {}
1439
+ throw ApiError.badRequest(`Unknown field '${spec.field}' on '${targetCollection.slug}', the target of relation '${spec.relation}' on collection '${collectionPath}'` + (validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""), "UNKNOWN_ORDER_BY_FIELD", {
1440
+ field: encodeRelationAggregateSort(spec),
1441
+ collection: collectionPath,
1442
+ relation: spec.relation,
1443
+ targetCollection: targetCollection.slug,
1444
+ ...validFields.length > 0 && { validFields }
1445
+ });
1446
+ }
1447
+ aggregand = sql`${sql.identifier(alias)}.${sql.identifier(column.name)}`;
1448
+ } else if (spec.agg !== "count") throw ApiError.badRequest(`'${spec.agg}' needs a field to aggregate — only 'count' means something on its own.`, "ORDER_BY_FIELD_NOT_SORTABLE", {
1449
+ field: encodeRelationAggregateSort(spec),
1450
+ collection: collectionPath
1451
+ });
1452
+ const source = correlateTo === void 0 ? sql`${sourceIdColumn}` : sql`${correlateTo}`;
1453
+ let from;
1454
+ let correlation;
1455
+ if (relation.kind === "manyToMany") {
1456
+ const { table: junctionName, sourceColumn, targetColumn } = relation.through;
1457
+ const junctionTable = registry.getTable(junctionName);
1458
+ if (!junctionTable) throw new Error(`Junction table not found: ${junctionName}`);
1459
+ const sourceCol = junctionTable[sourceColumn];
1460
+ const targetCol = junctionTable[targetColumn];
1461
+ if (!sourceCol || !targetCol) throw new Error(`Junction columns '${sourceColumn}'/'${targetColumn}' not found in '${junctionName}'`);
1462
+ const targetKey = this.primaryKeyColumn(targetTable);
1463
+ if (!targetKey) throw new Error(`No primary key or "id" column in the target table of relation '${relation.relationName}'.`);
1464
+ const junctionAlias = "__rel_agg_junction";
1465
+ from = sql`${targetTable} AS ${sql.identifier(alias)} INNER JOIN ${junctionTable} AS ${sql.identifier(junctionAlias)} ON ${sql.identifier(junctionAlias)}.${sql.identifier(targetCol.name)} = ${sql.identifier(alias)}.${sql.identifier(targetKey.name)}`;
1466
+ correlation = sql`${sql.identifier(junctionAlias)}.${sql.identifier(sourceCol.name)} = ${source}`;
1467
+ } else if (relation.kind === "belongsTo") {
1468
+ const targetKey = this.primaryKeyColumn(targetTable);
1469
+ const localKey = relationColumn(table, collection, relation.localKey);
1470
+ if (!targetKey || !localKey) throw new Error(`Relation '${relation.relationName}' on '${collectionPath}' names a key that is not a column, so there is nothing to aggregate against.`);
1471
+ from = sql`${targetTable} AS ${sql.identifier(alias)}`;
1472
+ const owner = correlateTo === void 0 ? sql`${localKey}` : sql`(SELECT ${sql.identifier(localKey.name)} FROM ${table} WHERE ${sourceIdColumn} = ${correlateTo})`;
1473
+ correlation = sql`${sql.identifier(alias)}.${sql.identifier(targetKey.name)} = ${owner}`;
1474
+ } else {
1475
+ const foreignKey = relationColumn(targetTable, targetCollection, relation.foreignKeyOnTarget);
1476
+ if (!foreignKey) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'.`);
1477
+ const sourceKeyColumn = relation.sourceKey ? relationColumn(table, collection, relation.sourceKey) : sourceIdColumn;
1478
+ if (!sourceKeyColumn) throw new Error(`\`sourceKey: "${relation.sourceKey}"\` on relation '${relation.relationName}' is not a column on '${collectionPath}'.`);
1479
+ const pinned = correlateTo === void 0 ? sql`${sourceKeyColumn}` : relation.sourceKey ? sql`(SELECT ${sql.identifier(sourceKeyColumn.name)} FROM ${table} WHERE ${sourceIdColumn} = ${correlateTo})` : sql`${correlateTo}`;
1480
+ from = sql`${targetTable} AS ${sql.identifier(alias)}`;
1481
+ correlation = sql`${sql.identifier(alias)}.${sql.identifier(foreignKey.name)} = ${pinned}`;
1482
+ }
1483
+ return sql`(SELECT ${AGGREGATE_SQL[spec.agg]}(${aggregand}) FROM ${from} WHERE ${correlation})`;
1484
+ }
1098
1485
  /** The column a table's rows are keyed by: its primary key, else `id`. */
1099
1486
  static primaryKeyColumn(table) {
1100
1487
  return Object.values(table).find((col) => col.primary) ?? Object.values(table).find((col) => col.name === "id");
@@ -3155,6 +3542,37 @@ function isRoleSwitchingPermissionError(error) {
3155
3542
  return msg.includes("set role") || msg.includes("member of role");
3156
3543
  }
3157
3544
  /**
3545
+ * Was this `42501` the *caller* being refused by a policy, rather than the
3546
+ * server lacking a privilege?
3547
+ *
3548
+ * Both arrive as `insufficient_privilege`, and they are opposite kinds of
3549
+ * problem. A row-level-security refusal is a working access-control system
3550
+ * doing its job: the caller asked for something their policies do not permit,
3551
+ * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being
3552
+ * wrong — the connection role cannot touch the table at all, no policy is
3553
+ * involved, and nothing the caller changes about the request will help.
3554
+ *
3555
+ * Postgres distinguishes them in the message, so this does too:
3556
+ *
3557
+ * new row violates row-level security policy for table "notes" → the caller
3558
+ * permission denied for table notes → the server
3559
+ *
3560
+ * Only writes reach this. A read that RLS excludes is not an error — the rows
3561
+ * are filtered and the caller gets an empty page — so the erroring case is
3562
+ * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.
3563
+ *
3564
+ * Matched on the message because that is the only thing carrying the
3565
+ * distinction; the SQLSTATE is identical either way. Narrow by design: anything
3566
+ * not naming row-level security stays the server's problem, since reporting a
3567
+ * genuine privilege misconfiguration as "forbidden" would send an operator
3568
+ * hunting for a policy bug that does not exist.
3569
+ */
3570
+ function isRowLevelSecurityDenial(error) {
3571
+ const pgError = extractPgError(error);
3572
+ if (!pgError || pgError.code !== "42501") return false;
3573
+ return pgError.message.toLowerCase().includes("row-level security policy");
3574
+ }
3575
+ /**
3158
3576
  * Translate a raw PostgreSQL error into a user-friendly message.
3159
3577
  *
3160
3578
  * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
@@ -3209,8 +3627,11 @@ function pgErrorToFriendlyMessage(pgError, context) {
3209
3627
  message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
3210
3628
  code
3211
3629
  };
3212
- case "42501": return {
3213
- message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
3630
+ case "42501": return pgMessage.toLowerCase().includes("row-level security policy") ? {
3631
+ message: `Not permitted to write this row in "${tableRef}": it does not satisfy the row-level security policy.${suffix}`,
3632
+ code
3633
+ } : {
3634
+ message: `Permission denied on "${tableRef}": the database role this server connects as lacks privileges on it.${suffix}`,
3214
3635
  code
3215
3636
  };
3216
3637
  case "28000": return {
@@ -3376,6 +3797,30 @@ var FetchService = class FetchService {
3376
3797
  return this.resolveOrderByField(table, orderBy, collection);
3377
3798
  }
3378
3799
  /**
3800
+ * The aggregate a sort key names, as an expression, or `undefined` if the
3801
+ * key is not one.
3802
+ *
3803
+ * `cursorId` builds the same expression pinned to the cursor row — see
3804
+ * {@link DrizzleConditionBuilder.buildRelationAggregateExpression}.
3805
+ *
3806
+ * A key that *parses* as an aggregate but names no relation, or a column
3807
+ * the target does not have, throws rather than falling through to the
3808
+ * column path. Falling through would report `min(applications.created_at)`
3809
+ * as an unknown column and list the columns of the wrong table.
3810
+ */
3811
+ resolveAggregateOrderTarget(table, orderBy, collection, collectionPath, cursorId) {
3812
+ const spec = parseRelationAggregateSort(orderBy);
3813
+ if (!spec) return void 0;
3814
+ if (!collection || !collectionPath) throw ApiError.badRequest(`Cannot sort by '${orderBy}': an aggregate sort needs the collection it is written against, and this query carries none.`, "ORDER_BY_FIELD_NOT_SORTABLE", { field: orderBy });
3815
+ const primaryKeys = getPrimaryKeys(collection, this.registry);
3816
+ const idColumn = primaryKeys.length === 1 ? table[primaryKeys[0].fieldName] : void 0;
3817
+ if (!idColumn) throw ApiError.badRequest(`Cannot sort by '${orderBy}' on collection '${collectionPath}': the subquery correlates on a single key column, and this collection has none or has a composite one.`, "ORDER_BY_FIELD_NOT_SORTABLE", {
3818
+ field: orderBy,
3819
+ collection: collectionPath
3820
+ });
3821
+ return DrizzleConditionBuilder.buildRelationAggregateExpression(spec, table, collection, this.registry, idColumn, collectionPath, cursorId);
3822
+ }
3823
+ /**
3379
3824
  * Resolve every sort key to the expression it orders by, in order of
3380
3825
  * significance.
3381
3826
  *
@@ -3384,9 +3829,19 @@ var FetchService = class FetchService {
3384
3829
  * unknown-field mode, where dropping is the configured answer, and dropping
3385
3830
  * one key of several still honours the ones that did resolve.
3386
3831
  */
3387
- resolveOrderKeys(table, keys, collection, searchString) {
3832
+ resolveOrderKeys(table, keys, collection, searchString, collectionPath, cursorId) {
3388
3833
  const resolved = [];
3389
3834
  for (const [field, direction] of keys) {
3835
+ const aggregate = this.resolveAggregateOrderTarget(table, field, collection, collectionPath);
3836
+ if (aggregate) {
3837
+ resolved.push({
3838
+ field,
3839
+ direction,
3840
+ target: aggregate,
3841
+ ...cursorId !== void 0 && { cursorTarget: this.resolveAggregateOrderTarget(table, field, collection, collectionPath, cursorId) }
3842
+ });
3843
+ continue;
3844
+ }
3390
3845
  const target = this.resolveOrderTarget(table, field, collection, searchString);
3391
3846
  if (target) resolved.push({
3392
3847
  field,
@@ -3406,9 +3861,18 @@ var FetchService = class FetchService {
3406
3861
  * to agree: they did not, and an ascending sort paged with `id >` against an
3407
3862
  * `ORDER BY … , id DESC`, so rows sharing a sort value were dropped from
3408
3863
  * every page after the first.
3864
+ *
3865
+ * Where the NULLs go is written out rather than inherited. Postgres already
3866
+ * defaults to `NULLS LAST` ascending and `NULLS FIRST` descending, so this
3867
+ * changes no query — but {@link buildKeysetComparison} encodes that exact
3868
+ * placement, and an invariant two functions depend on should be stated in
3869
+ * both rather than assumed in one. It matters most for the keys that are
3870
+ * *always* nullable: an aggregate over a relation is NULL for every row the
3871
+ * relation reaches nothing from, which is precisely the "nobody waiting"
3872
+ * end of a queue.
3409
3873
  */
3410
3874
  buildOrderExpressions(keys, idField) {
3411
- const expressions = keys.map(({ direction, target }) => direction === "asc" ? asc(target) : desc(target));
3875
+ const expressions = keys.map(({ direction, target }) => direction === "asc" ? sql`${target} ASC NULLS LAST` : sql`${target} DESC NULLS FIRST`);
3412
3876
  expressions.push(desc(idField));
3413
3877
  return expressions;
3414
3878
  }
@@ -3596,12 +4060,12 @@ var FetchService = class FetchService {
3596
4060
  if (keys) {
3597
4061
  if (keys.some(([field]) => field === FetchService.SCORE_FIELD)) throw ApiError.badRequest("Cursor pagination (`startAfter`) cannot be combined with `orderBy: \"_score\"`. Relevance is computed per query rather than stored, so it cannot key a cursor. Use `limit`/`offset` for relevance-ordered pages, or order by a column.", "SCORE_CURSOR_UNSUPPORTED", { field: FetchService.SCORE_FIELD });
3598
4062
  const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
3599
- const resolved = this.resolveOrderKeys(table, keys, collection);
3600
4063
  const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
4064
+ const resolved = this.resolveOrderKeys(table, keys, collection, void 0, collectionPath, startAfterId ?? void 0);
3601
4065
  if (resolved.length > 0 && startAfterId !== void 0) {
3602
4066
  const cursorValues = cursor.values;
3603
- const values = resolved.map(({ field }) => cursorValues && field in cursorValues ? cursorValues[field] : cursor[field]);
3604
- if (values.every((value) => value !== void 0)) return [this.buildKeysetComparison(resolved, values, idField, startAfterId)];
4067
+ const values = resolved.map(({ field, cursorTarget }) => cursorTarget ? null : cursorValues && field in cursorValues ? cursorValues[field] : cursor[field]);
4068
+ if (values.every((value, i) => resolved[i].cursorTarget || value !== void 0)) return [this.buildKeysetComparison(resolved, values, idField, startAfterId)];
3605
4069
  }
3606
4070
  } else {
3607
4071
  const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
@@ -3624,10 +4088,11 @@ var FetchService = class FetchService {
3624
4088
  */
3625
4089
  buildKeysetComparison(keys, values, idField, cursorId, index = 0) {
3626
4090
  if (index >= keys.length) return lt(idField, cursorId);
3627
- const { direction } = keys[index];
4091
+ const { direction, cursorTarget } = keys[index];
3628
4092
  const target = keys[index].target;
3629
4093
  const value = values[index];
3630
4094
  const rest = this.buildKeysetComparison(keys, values, idField, cursorId, index + 1);
4095
+ if (cursorTarget) return direction === "asc" ? or(and(isNull(cursorTarget), isNull(target), rest), and(isNotNull(cursorTarget), or(sql`${target} > ${cursorTarget}`, isNull(target), and(sql`${target} = ${cursorTarget}`, rest)))) : or(and(isNull(cursorTarget), or(isNotNull(target), and(isNull(target), rest))), and(isNotNull(cursorTarget), or(sql`${target} < ${cursorTarget}`, and(sql`${target} = ${cursorTarget}`, rest))));
3631
4096
  if (value === null) return direction === "asc" ? and(isNull(target), rest) : or(isNotNull(target), and(isNull(target), rest));
3632
4097
  return direction === "asc" ? or(gt(target, value), isNull(target), and(eq(target, value), rest)) : or(lt(target, value), and(eq(target, value), rest));
3633
4098
  }
@@ -4766,6 +5231,7 @@ var PersistService = class {
4766
5231
  if (pgError) {
4767
5232
  const { message, code } = pgErrorToFriendlyMessage(pgError, collectionSlug);
4768
5233
  if (/^2[23]/.test(code)) return code === "23505" ? ApiError.conflict(message, `PG_${code}`) : ApiError.badRequest(message, `PG_${code}`);
5234
+ if (isRowLevelSecurityDenial(error)) return ApiError.forbidden(message, "WRITE_DENIED");
4769
5235
  return new Error(message);
4770
5236
  }
4771
5237
  const causeMessage = extractCauseMessage(error);
@@ -10094,7 +10560,7 @@ function createBackupCron(config) {
10094
10560
  enabled: config.enabled ?? true,
10095
10561
  timeoutSeconds: 3600,
10096
10562
  async handler({ log }) {
10097
- const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-BH0Dzo_h.js").then((n) => n.r);
10563
+ const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-czK-OAuG.js").then((n) => n.r);
10098
10564
  const { destination } = config;
10099
10565
  if (destination.kind !== "local" && !config.storage) throw new Error(`Backup destination is ${destination.kind} but no storage controller was provided. Pass the backend's configured StorageController to createBackupCron({ storage }).`);
10100
10566
  log(`Starting backup of "${dbName}"…`);
@@ -13512,7 +13978,7 @@ function createPostgresBootstrapper(pgConfig) {
13512
13978
  * proved it can bootstrap with.
13513
13979
  */
13514
13980
  async ensureCollectionSchema(collections, driverResult, log) {
13515
- const { ensureCollectionTables } = await import("./ensure-collection-tables-DT2eq859.js");
13981
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-BHUjQ-z4.js");
13516
13982
  const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
13517
13983
  for (const failure of plan.failures) logger.warn(failure.kind === "comment-column" ? `🔍 [schema] Could not record the search fingerprint on "${failure.target}" — search works, but a later change to the \`search\` block will not be detected: ${failure.error}` : `🔗 [schema] Could not add foreign key "${failure.target}" — the column exists and the collection still serves, but rows are not policed by this constraint: ${failure.error}`);
13518
13984
  return { applied: plan.actions.length - plan.failures.length };
@@ -13533,7 +13999,7 @@ function createPostgresBootstrapper(pgConfig) {
13533
13999
  * stays RLS-enabled, so it denies rather than leaks.
13534
14000
  */
13535
14001
  async ensureCollectionPolicies(collections, driverResult, log) {
13536
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-DoHwhVf8.js");
14002
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-D5PtQLyR.js");
13537
14003
  const queryable = provisioningQueryable(driverResult);
13538
14004
  const outcome = await ensureCollectionPolicies(queryable, collections, log);
13539
14005
  for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
@@ -13542,7 +14008,7 @@ function createPostgresBootstrapper(pgConfig) {
13542
14008
  for (const u of outcome.unsecured.filter((u) => u.grantWithdrawn)) logger.error(`🔐 [rls] Could not enable row-level security on "${u.table}": ${u.error}. Its privileges have been revoked from ${REBASE_USER_ROLE}, so the table is unreachable rather than unprotected. Reads and writes to that collection will fail until RLS can be enabled.`);
13543
14009
  if (unrevoked.length > 0) throw new Error("Refusing to start: row-level security could not be enabled on " + unrevoked.map((u) => `"${u.table}" (${u.error})`).join(", ") + `, and the privileges granted to ${REBASE_USER_ROLE} could not be revoked either. The table would be served with no row filtering. Fix the database permissions (the connection role must own these tables, or be able to ALTER them) and boot again.`);
13544
14010
  try {
13545
- const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-69hYT8nr.js").then((n) => n.n);
14011
+ const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-B5Sajku6.js").then((n) => n.n);
13546
14012
  await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
13547
14013
  info: (m) => logger.info(m),
13548
14014
  warn: (m) => logger.warn(m)
@@ -13557,7 +14023,7 @@ function createPostgresBootstrapper(pgConfig) {
13557
14023
  },
13558
14024
  mountRoutes(app, basePath, driverResult) {},
13559
14025
  async initializeWebsockets(server, realtimeService, driver, config, adapter) {
13560
- const { createPostgresWebSocket } = await import("./websocket-D2jXv0Ds.js").then((n) => n.n);
14026
+ const { createPostgresWebSocket } = await import("./websocket-BVgDVO-V.js").then((n) => n.n);
13561
14027
  createPostgresWebSocket(server, realtimeService, driver, config, adapter);
13562
14028
  }
13563
14029
  };