@rebasepro/server-postgres 0.14.2-canary.gf913b76 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auth-users-columns-DLwxDqYb.js → auth-users-columns-JJ8ngvy5.js} +16 -6
- package/dist/{auth-users-columns-DLwxDqYb.js.map → auth-users-columns-JJ8ngvy5.js.map} +1 -1
- package/dist/{backup-service-BH0Dzo_h.js → backup-service-czK-OAuG.js} +2 -2
- package/dist/{backup-service-BH0Dzo_h.js.map → backup-service-czK-OAuG.js.map} +1 -1
- package/dist/{ensure-collection-policies-CjPPtN0e.js → ensure-collection-policies-D5PtQLyR.js} +3 -3
- package/dist/{ensure-collection-policies-CjPPtN0e.js.map → ensure-collection-policies-D5PtQLyR.js.map} +1 -1
- package/dist/{ensure-collection-tables-Dnb7scJ1.js → ensure-collection-tables-BHUjQ-z4.js} +3 -3
- package/dist/{ensure-collection-tables-Dnb7scJ1.js.map → ensure-collection-tables-BHUjQ-z4.js.map} +1 -1
- package/dist/index.es.js +448 -17
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-69hYT8nr.js → rls-bootstrap-sql-B5Sajku6.js} +2 -2
- package/dist/{rls-bootstrap-sql-69hYT8nr.js.map → rls-bootstrap-sql-B5Sajku6.js.map} +1 -1
- package/dist/{rls-enforcement-BpsYVDGM.js → rls-enforcement-BDBfuTD4.js} +3 -3
- package/dist/{rls-enforcement-BpsYVDGM.js.map → rls-enforcement-BDBfuTD4.js.map} +1 -1
- package/dist/services/FetchService.d.ts +22 -0
- package/dist/{src-DCdn3Val.js → src-BBFsDaeA.js} +60 -2
- package/dist/src-BBFsDaeA.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +168 -1
- package/dist/{websocket-D2jXv0Ds.js → websocket-BVgDVO-V.js} +2 -2
- package/dist/{websocket-D2jXv0Ds.js.map → websocket-BVgDVO-V.js.map} +1 -1
- package/package.json +6 -6
- package/src/services/FetchService.ts +155 -15
- package/src/utils/drizzle-conditions.ts +594 -1
- 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-
|
|
7
|
-
import { c as isPostgresCollectionConfig, f as ALL_WHERE_FILTER_OPS, l as isRelationalCollectionConfig } from "./src-
|
|
8
|
-
import { t as createPostgresWebSocket } from "./websocket-
|
|
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-
|
|
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-
|
|
11
|
-
import { t as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-
|
|
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");
|
|
@@ -3410,6 +3797,30 @@ var FetchService = class FetchService {
|
|
|
3410
3797
|
return this.resolveOrderByField(table, orderBy, collection);
|
|
3411
3798
|
}
|
|
3412
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
|
+
/**
|
|
3413
3824
|
* Resolve every sort key to the expression it orders by, in order of
|
|
3414
3825
|
* significance.
|
|
3415
3826
|
*
|
|
@@ -3418,9 +3829,19 @@ var FetchService = class FetchService {
|
|
|
3418
3829
|
* unknown-field mode, where dropping is the configured answer, and dropping
|
|
3419
3830
|
* one key of several still honours the ones that did resolve.
|
|
3420
3831
|
*/
|
|
3421
|
-
resolveOrderKeys(table, keys, collection, searchString) {
|
|
3832
|
+
resolveOrderKeys(table, keys, collection, searchString, collectionPath, cursorId) {
|
|
3422
3833
|
const resolved = [];
|
|
3423
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
|
+
}
|
|
3424
3845
|
const target = this.resolveOrderTarget(table, field, collection, searchString);
|
|
3425
3846
|
if (target) resolved.push({
|
|
3426
3847
|
field,
|
|
@@ -3440,9 +3861,18 @@ var FetchService = class FetchService {
|
|
|
3440
3861
|
* to agree: they did not, and an ascending sort paged with `id >` against an
|
|
3441
3862
|
* `ORDER BY … , id DESC`, so rows sharing a sort value were dropped from
|
|
3442
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.
|
|
3443
3873
|
*/
|
|
3444
3874
|
buildOrderExpressions(keys, idField) {
|
|
3445
|
-
const expressions = keys.map(({ direction, target }) => direction === "asc" ?
|
|
3875
|
+
const expressions = keys.map(({ direction, target }) => direction === "asc" ? sql`${target} ASC NULLS LAST` : sql`${target} DESC NULLS FIRST`);
|
|
3446
3876
|
expressions.push(desc(idField));
|
|
3447
3877
|
return expressions;
|
|
3448
3878
|
}
|
|
@@ -3630,12 +4060,12 @@ var FetchService = class FetchService {
|
|
|
3630
4060
|
if (keys) {
|
|
3631
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 });
|
|
3632
4062
|
const collection = collectionPath ? getCollectionByPath(collectionPath, this.registry) : void 0;
|
|
3633
|
-
const resolved = this.resolveOrderKeys(table, keys, collection);
|
|
3634
4063
|
const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
|
|
4064
|
+
const resolved = this.resolveOrderKeys(table, keys, collection, void 0, collectionPath, startAfterId ?? void 0);
|
|
3635
4065
|
if (resolved.length > 0 && startAfterId !== void 0) {
|
|
3636
4066
|
const cursorValues = cursor.values;
|
|
3637
|
-
const values = resolved.map(({ field }) => cursorValues && field in cursorValues ? cursorValues[field] : cursor[field]);
|
|
3638
|
-
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)];
|
|
3639
4069
|
}
|
|
3640
4070
|
} else {
|
|
3641
4071
|
const startAfterId = cursor.id ?? cursor[idInfo.fieldName];
|
|
@@ -3658,10 +4088,11 @@ var FetchService = class FetchService {
|
|
|
3658
4088
|
*/
|
|
3659
4089
|
buildKeysetComparison(keys, values, idField, cursorId, index = 0) {
|
|
3660
4090
|
if (index >= keys.length) return lt(idField, cursorId);
|
|
3661
|
-
const { direction } = keys[index];
|
|
4091
|
+
const { direction, cursorTarget } = keys[index];
|
|
3662
4092
|
const target = keys[index].target;
|
|
3663
4093
|
const value = values[index];
|
|
3664
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))));
|
|
3665
4096
|
if (value === null) return direction === "asc" ? and(isNull(target), rest) : or(isNotNull(target), and(isNull(target), rest));
|
|
3666
4097
|
return direction === "asc" ? or(gt(target, value), isNull(target), and(eq(target, value), rest)) : or(lt(target, value), and(eq(target, value), rest));
|
|
3667
4098
|
}
|
|
@@ -10129,7 +10560,7 @@ function createBackupCron(config) {
|
|
|
10129
10560
|
enabled: config.enabled ?? true,
|
|
10130
10561
|
timeoutSeconds: 3600,
|
|
10131
10562
|
async handler({ log }) {
|
|
10132
|
-
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-
|
|
10563
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-czK-OAuG.js").then((n) => n.r);
|
|
10133
10564
|
const { destination } = config;
|
|
10134
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 }).`);
|
|
10135
10566
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -13547,7 +13978,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
13547
13978
|
* proved it can bootstrap with.
|
|
13548
13979
|
*/
|
|
13549
13980
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
13550
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
13981
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-BHUjQ-z4.js");
|
|
13551
13982
|
const plan = await ensureCollectionTables(provisioningQueryable(driverResult), collections, log);
|
|
13552
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}`);
|
|
13553
13984
|
return { applied: plan.actions.length - plan.failures.length };
|
|
@@ -13568,7 +13999,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
13568
13999
|
* stays RLS-enabled, so it denies rather than leaks.
|
|
13569
14000
|
*/
|
|
13570
14001
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
13571
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
14002
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-D5PtQLyR.js");
|
|
13572
14003
|
const queryable = provisioningQueryable(driverResult);
|
|
13573
14004
|
const outcome = await ensureCollectionPolicies(queryable, collections, log);
|
|
13574
14005
|
for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
|
|
@@ -13577,7 +14008,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
13577
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.`);
|
|
13578
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.`);
|
|
13579
14010
|
try {
|
|
13580
|
-
const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-
|
|
14011
|
+
const { dropLegacyAuthSchema } = await import("./rls-bootstrap-sql-B5Sajku6.js").then((n) => n.n);
|
|
13581
14012
|
await dropLegacyAuthSchema(async (text) => (await queryable.query(text)).rows, {
|
|
13582
14013
|
info: (m) => logger.info(m),
|
|
13583
14014
|
warn: (m) => logger.warn(m)
|
|
@@ -13592,7 +14023,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
13592
14023
|
},
|
|
13593
14024
|
mountRoutes(app, basePath, driverResult) {},
|
|
13594
14025
|
async initializeWebsockets(server, realtimeService, driver, config, adapter) {
|
|
13595
|
-
const { createPostgresWebSocket } = await import("./websocket-
|
|
14026
|
+
const { createPostgresWebSocket } = await import("./websocket-BVgDVO-V.js").then((n) => n.n);
|
|
13596
14027
|
createPostgresWebSocket(server, realtimeService, driver, config, adapter);
|
|
13597
14028
|
}
|
|
13598
14029
|
};
|