@rebasepro/server-postgres 0.12.1-canary.gdfba2a1 → 0.12.1-canary.ge1bb178
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/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +6 -1
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/{backup-service-DH9kPg-E.js → backup-service-CD8o_1Sl.js} +144 -11
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/{connection-B5Wndbr1.js → connection-BuZ97wsr.js} +57 -3
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
- package/dist/index.es.js +529 -142
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/services/FetchService.d.ts +4 -1
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +24 -1
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-DihrDFuP.js → src-CzbghKwf.js} +189 -101
- package/dist/src-CzbghKwf.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +19 -0
- package/dist/{websocket-BKcGvILX.js → websocket-B2LsrINK.js} +8 -6
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +192 -33
- package/src/auth/services.ts +6 -3
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +16 -3
- package/src/schema/generate-postgres-ddl-logic.ts +375 -16
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/FetchService.ts +76 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +40 -1
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +46 -0
- package/src/websocket.ts +18 -9
- package/dist/backup-service-DH9kPg-E.js.map +0 -1
- package/dist/connection-B5Wndbr1.js.map +0 -1
- package/dist/ensure-collection-tables-CIHSH962.js +0 -304
- package/dist/ensure-collection-tables-CIHSH962.js.map +0 -1
- package/dist/src-DihrDFuP.js.map +0 -1
- package/dist/websocket-BKcGvILX.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, r as createPostgresDatabaseConnection } from "./connection-
|
|
5
|
-
import { C as updateDateAutoValues, D as
|
|
4
|
+
import { a as guardPoolAgainstDirtyRelease, i as createReadReplicaConnection, n as createDirectDatabaseConnection, o as pinSearchPath, r as createPostgresDatabaseConnection } from "./connection-BuZ97wsr.js";
|
|
5
|
+
import { C as updateDateAutoValues, D as mergeDeep, E as getPolicyNamesForRule, M as hasForeignKeyOnTarget, N as isManyToMany, O as camelCase, P as Vector, S as normalizeToEntityRelation, _ as buildCompositeId, a as getJunctionSecurityRules, b as createRelationRef, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, k as toSnakeCase, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as generateForeignKeyName, x as createRelationRefWithData, y as parseIdValues } from "./src-CzbghKwf.js";
|
|
6
6
|
import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
|
|
7
|
-
import { t as
|
|
8
|
-
import {
|
|
7
|
+
import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
|
|
8
|
+
import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
|
|
9
|
+
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-CD8o_1Sl.js";
|
|
9
10
|
import { Client, Pool } from "pg";
|
|
10
11
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
11
12
|
import { ApiError, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
|
|
@@ -49,6 +50,39 @@ function resolveClientListLimit(rawLimit, opts = {}) {
|
|
|
49
50
|
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
50
51
|
}
|
|
51
52
|
//#endregion
|
|
53
|
+
//#region ../common/src/util/email.ts
|
|
54
|
+
/**
|
|
55
|
+
* Email normalization — one implementation, because the database enforces it.
|
|
56
|
+
*
|
|
57
|
+
* `ensureAuthTablesExist` puts a `UNIQUE INDEX ON users (lower(email))` on the
|
|
58
|
+
* auth table. That index decides what "the same address" means, and it does not
|
|
59
|
+
* trim: to Postgres, `' foo@bar.com'` and `'foo@bar.com'` are two addresses and
|
|
60
|
+
* both may exist. So every write that reaches the column has to agree with
|
|
61
|
+
* every read, exactly, or the two disagree in the one direction that matters —
|
|
62
|
+
* a row that exists and cannot be found.
|
|
63
|
+
*
|
|
64
|
+
* That is not hypothetical. The lookup path trimmed and the admin create paths
|
|
65
|
+
* did not, so a user created through `POST /api/data/users` or
|
|
66
|
+
* `POST /api/auth/admin/users` with a stray space was stored untrimmed,
|
|
67
|
+
* survived the unique index alongside the real address, and was unreachable by
|
|
68
|
+
* login forever after. The HTTP auth routes were unaffected only because Zod's
|
|
69
|
+
* `.email()` happens to reject surrounding whitespace — a guard on a different
|
|
70
|
+
* layer, for a different reason, that the admin paths do not sit behind.
|
|
71
|
+
*
|
|
72
|
+
* It lives in `common` because `server`, `server-postgres` and `server-mongo`
|
|
73
|
+
* all write this column and must agree exactly, and `common` is the only
|
|
74
|
+
* package all three already depend on.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Canonical form of an email address: trimmed, lower-cased.
|
|
78
|
+
*
|
|
79
|
+
* Non-strings pass through untouched, so this is safe to apply to a value out
|
|
80
|
+
* of a partial update payload whose type is not known yet.
|
|
81
|
+
*/
|
|
82
|
+
function normalizeEmail(email) {
|
|
83
|
+
return typeof email === "string" ? email.trim().toLowerCase() : email;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
52
86
|
//#region ../common/src/util/callbacks.ts
|
|
53
87
|
/**
|
|
54
88
|
* Helper function to recursively check if there are any callbacks in the properties.
|
|
@@ -281,6 +315,35 @@ function requirePrimaryKeys(collection, registry) {
|
|
|
281
315
|
return keys;
|
|
282
316
|
}
|
|
283
317
|
/**
|
|
318
|
+
* The column on the *source* table that a `hasOne`/`hasMany` link points at.
|
|
319
|
+
*
|
|
320
|
+
* `sourceKey` is authored when the two sides join on a natural key — an
|
|
321
|
+
* external identity id, a SKU — and left off when they join on the row id,
|
|
322
|
+
* which is the overwhelming majority. That makes `undefined` the only optional
|
|
323
|
+
* field on a resolved relation, so it gets exactly one reader: this function.
|
|
324
|
+
* Every consumer that needs the column asks here, and none of them re-derives
|
|
325
|
+
* "or else the primary key" for itself. That is the whole point — the fallback
|
|
326
|
+
* chains this codebase removed from relation resolution were dangerous because
|
|
327
|
+
* they were *duplicated* and could disagree, not because they existed.
|
|
328
|
+
*/
|
|
329
|
+
function sourceKeyField(relation, sourceCollection, registry) {
|
|
330
|
+
if (relation.sourceKey) return relation.sourceKey;
|
|
331
|
+
return requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Whether this link joins on something other than the source's primary key.
|
|
335
|
+
*
|
|
336
|
+
* Callers that hold a parent *id* — which is most of them, since an id is what
|
|
337
|
+
* a URL carries — must translate it to the source key's value before it can be
|
|
338
|
+
* compared with the target's foreign key. Those that hold the parent *row*, or
|
|
339
|
+
* that build a correlated subquery over the source table, can read the column
|
|
340
|
+
* directly and skip the lookup.
|
|
341
|
+
*/
|
|
342
|
+
function joinsOnNaturalKey(relation, sourceCollection, registry) {
|
|
343
|
+
if (!relation.sourceKey) return false;
|
|
344
|
+
return relation.sourceKey !== requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
284
347
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
285
348
|
* instead.
|
|
286
349
|
*
|
|
@@ -451,7 +514,9 @@ var DrizzleConditionBuilder = class {
|
|
|
451
514
|
case "hasMany": {
|
|
452
515
|
const fkColumn = targetTable[relation.foreignKeyOnTarget];
|
|
453
516
|
if (!fkColumn) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'.`);
|
|
454
|
-
return eq(fkColumn, parentId);
|
|
517
|
+
if (!relation.sourceKey) return eq(fkColumn, parentId);
|
|
518
|
+
const { table, idColumn } = parent();
|
|
519
|
+
return sql`${fkColumn} = (SELECT ${sql.identifier(relation.sourceKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
455
520
|
}
|
|
456
521
|
case "belongsTo": {
|
|
457
522
|
const { table, idColumn } = parent();
|
|
@@ -541,12 +606,16 @@ var DrizzleConditionBuilder = class {
|
|
|
541
606
|
column: foreignKey
|
|
542
607
|
};
|
|
543
608
|
}
|
|
544
|
-
if (relation && (hasForeignKeyOnTarget(relation) || isManyToMany(relation)) && registry && sourceIdColumn)
|
|
545
|
-
|
|
546
|
-
relation,
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
609
|
+
if (relation && (hasForeignKeyOnTarget(relation) || isManyToMany(relation)) && registry && sourceIdColumn) {
|
|
610
|
+
const correlationColumn = hasForeignKeyOnTarget(relation) && relation.sourceKey ? columnAt(relation.sourceKey) : sourceIdColumn;
|
|
611
|
+
if (!correlationColumn) throw new Error(`\`sourceKey: "${relation.sourceKey}"\` on relation '${relation.relationName}' is not a column on '${collectionPath}', so a filter on that relation has nothing to correlate against.`);
|
|
612
|
+
return {
|
|
613
|
+
kind: "relation",
|
|
614
|
+
relation,
|
|
615
|
+
registry,
|
|
616
|
+
sourceIdColumn: correlationColumn
|
|
617
|
+
};
|
|
618
|
+
}
|
|
550
619
|
}
|
|
551
620
|
for (const guess of [`${field}_id`, generateForeignKeyName(field)]) {
|
|
552
621
|
const foreignKey = columnAt(guess);
|
|
@@ -1345,8 +1414,9 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
1345
1414
|
} else if (hasForeignKeyOnTarget(relation) && db && registry) try {
|
|
1346
1415
|
const targetCollection = relation.target();
|
|
1347
1416
|
const targetTable = registry.getTable(getTableName$1(targetCollection));
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1417
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
1418
|
+
const currentId = relation.sourceKey ? data[relation.sourceKey] : buildCompositeId(data, pks);
|
|
1419
|
+
if (targetTable && currentId !== void 0 && currentId !== null && currentId !== "") {
|
|
1350
1420
|
const foreignKeyColumn = targetTable[relation.foreignKeyOnTarget];
|
|
1351
1421
|
if (foreignKeyColumn) {
|
|
1352
1422
|
const relatedRows = await db.select().from(targetTable).where(eq(foreignKeyColumn, currentId)).limit(relation.cardinality === "one" ? 1 : 100);
|
|
@@ -1614,6 +1684,33 @@ function normalizeDbValues(data, collection) {
|
|
|
1614
1684
|
//#endregion
|
|
1615
1685
|
//#region src/services/RelationService.ts
|
|
1616
1686
|
/**
|
|
1687
|
+
* The ids in a to-many relation write, whatever shape the caller sent.
|
|
1688
|
+
*
|
|
1689
|
+
* A membership list is written as either the related rows (`[{ id: 1 }]`, what
|
|
1690
|
+
* the admin UI sends back after reading them) or as bare keys (`[1]`, `["t-1"]`,
|
|
1691
|
+
* what anyone writing the API by hand sends). Only the first was read, via a
|
|
1692
|
+
* blind `.map(rel => rel.id)`, and a bare key therefore became `undefined`:
|
|
1693
|
+
* on a numeric-keyed target that surfaced as `Invalid numeric ID: undefined`,
|
|
1694
|
+
* and on a string-keyed one it did not surface at all — `String(undefined)`
|
|
1695
|
+
* wrote a junction row pointing at the literal `"undefined"`, which no read
|
|
1696
|
+
* would ever match. Both shapes are accepted here, in one place, because both
|
|
1697
|
+
* call sites had the same assumption.
|
|
1698
|
+
*
|
|
1699
|
+
* An element that carries no key is refused rather than skipped: dropping it
|
|
1700
|
+
* would silently write a shorter membership list than the caller asked for.
|
|
1701
|
+
*/
|
|
1702
|
+
function relationTargetIds(value, relationName, collectionSlug) {
|
|
1703
|
+
if (!Array.isArray(value)) return [];
|
|
1704
|
+
return value.map((element, index) => {
|
|
1705
|
+
if (typeof element === "string" || typeof element === "number") return element;
|
|
1706
|
+
if (element && typeof element === "object") {
|
|
1707
|
+
const id = element.id;
|
|
1708
|
+
if (typeof id === "string" || typeof id === "number") return id;
|
|
1709
|
+
}
|
|
1710
|
+
throw new Error(`Cannot write relation "${relationName}" on "${collectionSlug}": element ${index} carries no id. Pass either the related rows (\`[{ id: … }]\`) or their keys (\`[1, 2]\`), not ${element === null ? "null" : typeof element}.`);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1617
1714
|
* Typed wrapper for Drizzle dynamic query innerJoin.
|
|
1618
1715
|
* Drizzle's `$dynamic()` queries lose the `innerJoin` method from
|
|
1619
1716
|
* their static type, but it exists at runtime. This helper bridges
|
|
@@ -1691,8 +1788,67 @@ var RelationService = class {
|
|
|
1691
1788
|
* reading. Left alone it would silently match on the first key column and
|
|
1692
1789
|
* hand a tenant's rows to its neighbour — say so instead.
|
|
1693
1790
|
*/
|
|
1694
|
-
assertSingleKeyAddressable(parentCollection, parentPks, via) {
|
|
1695
|
-
if (
|
|
1791
|
+
assertSingleKeyAddressable(parentCollection, parentPks, via, relation) {
|
|
1792
|
+
if (relation && hasForeignKeyOnTarget(relation) && relation.sourceKey) return;
|
|
1793
|
+
if (parentPks.length > 1) throw new Error(`Relation on '${parentCollection.slug}' uses '${via}', a single foreign-key column, but '${parentCollection.slug}' is keyed on ${parentPks.map((k) => `'${k.fieldName}'`).join(" + ")}. One column cannot reference a composite key — give the relation a \`sourceKey\` naming a single unique column to point at, or express it with \`joinPath\`, whose \`on.from\`/\`on.to\` take every key column.`);
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* What the target's foreign key holds, for each of these parent rows.
|
|
1797
|
+
*
|
|
1798
|
+
* Ordinarily the parent's id, and then this is free. When the relation
|
|
1799
|
+
* declares a `sourceKey` the two are different values, and the mapping
|
|
1800
|
+
* between them lives in the source table — so it costs one SELECT, issued
|
|
1801
|
+
* once for the whole batch rather than per parent.
|
|
1802
|
+
*
|
|
1803
|
+
* Both directions come back because both are needed and deriving one from
|
|
1804
|
+
* the other by hand is how a batch loader ends up attributing a child to the
|
|
1805
|
+
* wrong parent: reads translate id → key to build the WHERE, and then
|
|
1806
|
+
* translate key → id to attribute each row that comes back.
|
|
1807
|
+
*/
|
|
1808
|
+
/**
|
|
1809
|
+
* The value a related row's foreign key must hold to belong to this parent.
|
|
1810
|
+
*
|
|
1811
|
+
* `undefined` when the parent's source key is null — which is not an error
|
|
1812
|
+
* here, only in the callers that were about to write it. Exposed for
|
|
1813
|
+
* {@link PersistService}, which stamps this onto a child created under a
|
|
1814
|
+
* nested path and would otherwise write the id and lose the row.
|
|
1815
|
+
*/
|
|
1816
|
+
async parentKeyValue(parentCollection, relation, parentId, db = this.db) {
|
|
1817
|
+
const { keyByParentId } = await this.resolveSourceKeys(parentCollection, relation, [parentId], db);
|
|
1818
|
+
return keyByParentId.get(String(parentId));
|
|
1819
|
+
}
|
|
1820
|
+
async resolveSourceKeys(parentCollection, relation, parentIds, db = this.db) {
|
|
1821
|
+
const keyByParentId = /* @__PURE__ */ new Map();
|
|
1822
|
+
const parentIdByKey = /* @__PURE__ */ new Map();
|
|
1823
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
1824
|
+
if (!joinsOnNaturalKey(relation, parentCollection, this.registry)) {
|
|
1825
|
+
for (const id of parentIds) {
|
|
1826
|
+
const value = parseIdValues(id, parentPks)[parentPks[0].fieldName];
|
|
1827
|
+
keyByParentId.set(String(id), value);
|
|
1828
|
+
parentIdByKey.set(String(value), id);
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
keyByParentId,
|
|
1832
|
+
parentIdByKey
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const field = sourceKeyField(relation, parentCollection, this.registry);
|
|
1836
|
+
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
1837
|
+
if (!parentTable[field]) throw new Error(`\`sourceKey: "${field}"\` on relation '${relation.relationName}' is not a column on '${parentCollection.slug}'. It names a column on the source table, not on the target.`);
|
|
1838
|
+
const rows = await db.select().from(parentTable).where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
1839
|
+
for (const row of rows) {
|
|
1840
|
+
const keyValue = row[field];
|
|
1841
|
+
if (keyValue === null || keyValue === void 0) continue;
|
|
1842
|
+
const parentId = buildCompositeId(row, parentPks);
|
|
1843
|
+
keyByParentId.set(String(parentId), keyValue);
|
|
1844
|
+
const existing = parentIdByKey.get(String(keyValue));
|
|
1845
|
+
if (existing !== void 0 && String(existing) !== String(parentId)) throw new Error(`\`sourceKey: "${field}"\` on relation '${relation.relationName}' is not unique on '${parentCollection.slug}': rows '${existing}' and '${parentId}' both hold '${keyValue}'. Add a unique constraint — a link that addresses more than one source row cannot say which one a related row belongs to.`);
|
|
1846
|
+
parentIdByKey.set(String(keyValue), parentId);
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
keyByParentId,
|
|
1850
|
+
parentIdByKey
|
|
1851
|
+
};
|
|
1696
1852
|
}
|
|
1697
1853
|
/**
|
|
1698
1854
|
* Fetch rows related to a parent row through a specific relation
|
|
@@ -1751,6 +1907,8 @@ var RelationService = class {
|
|
|
1751
1907
|
}
|
|
1752
1908
|
return rows;
|
|
1753
1909
|
}
|
|
1910
|
+
const matchValue = hasForeignKeyOnTarget(relation) ? (await this.resolveSourceKeys(parentCollection, relation, [parentId])).keyByParentId.get(String(parentId)) : parsedParentId;
|
|
1911
|
+
if (matchValue === void 0) return [];
|
|
1754
1912
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1755
1913
|
const additionalFilters = [];
|
|
1756
1914
|
if (options.searchString) {
|
|
@@ -1759,7 +1917,7 @@ var RelationService = class {
|
|
|
1759
1917
|
const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
|
|
1760
1918
|
if (searchCombined) additionalFilters.push(searchCombined);
|
|
1761
1919
|
}
|
|
1762
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
1920
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValue, targetTable, parentTable, parentIdCol, idField, this.registry, additionalFilters);
|
|
1763
1921
|
if (options.limit) query = query.limit(options.limit);
|
|
1764
1922
|
const results = await query;
|
|
1765
1923
|
if (!results || !Array.isArray(results)) return [];
|
|
@@ -1802,8 +1960,10 @@ var RelationService = class {
|
|
|
1802
1960
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
1803
1961
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1804
1962
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1963
|
+
const matchValue = hasForeignKeyOnTarget(relation) ? (await this.resolveSourceKeys(parentCollection, relation, [parentId])).keyByParentId.get(String(parentId)) : parsedParentId;
|
|
1964
|
+
if (matchValue === void 0) return 0;
|
|
1805
1965
|
let query = this.db.select({ count: sql`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
|
|
1806
|
-
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation,
|
|
1966
|
+
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation, matchValue, targetTable, parentTable, parentIdCol, targetIdField, this.registry, additionalFilters);
|
|
1807
1967
|
const result = await query;
|
|
1808
1968
|
return Number(result[0]?.count || 0);
|
|
1809
1969
|
}
|
|
@@ -1930,17 +2090,24 @@ var RelationService = class {
|
|
|
1930
2090
|
}
|
|
1931
2091
|
return resultMap;
|
|
1932
2092
|
}
|
|
1933
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
2093
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName, relation);
|
|
2094
|
+
const { keyByParentId, parentIdByKey } = hasForeignKeyOnTarget(relation) ? await this.resolveSourceKeys(parentCollection, relation, parentIds) : {
|
|
2095
|
+
keyByParentId: /* @__PURE__ */ new Map(),
|
|
2096
|
+
parentIdByKey: /* @__PURE__ */ new Map()
|
|
2097
|
+
};
|
|
2098
|
+
const matchValues = hasForeignKeyOnTarget(relation) ? [...keyByParentId.values()] : parsedParentIds;
|
|
2099
|
+
if (matchValues.length === 0) return /* @__PURE__ */ new Map();
|
|
1934
2100
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1935
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
2101
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValues, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
1936
2102
|
const results = await query;
|
|
1937
2103
|
const resultMap = /* @__PURE__ */ new Map();
|
|
1938
|
-
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
1939
2104
|
for (const row of results) {
|
|
1940
2105
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
if (
|
|
2106
|
+
if (!hasForeignKeyOnTarget(relation)) continue;
|
|
2107
|
+
const foreignKeyValue = targetRow[relation.foreignKeyOnTarget];
|
|
2108
|
+
if (foreignKeyValue === void 0 || foreignKeyValue === null) continue;
|
|
2109
|
+
const parentId = parentIdByKey.get(String(foreignKeyValue));
|
|
2110
|
+
if (parentId !== void 0) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
1944
2111
|
}
|
|
1945
2112
|
return resultMap;
|
|
1946
2113
|
}
|
|
@@ -2018,17 +2185,24 @@ var RelationService = class {
|
|
|
2018
2185
|
}
|
|
2019
2186
|
return resultMap;
|
|
2020
2187
|
}
|
|
2021
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
2188
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName, relation);
|
|
2189
|
+
const { keyByParentId, parentIdByKey } = hasForeignKeyOnTarget(relation) ? await this.resolveSourceKeys(parentCollection, relation, parentIds) : {
|
|
2190
|
+
keyByParentId: /* @__PURE__ */ new Map(),
|
|
2191
|
+
parentIdByKey: /* @__PURE__ */ new Map()
|
|
2192
|
+
};
|
|
2193
|
+
const matchValues = hasForeignKeyOnTarget(relation) ? [...keyByParentId.values()] : parsedParentIds;
|
|
2194
|
+
if (matchValues.length === 0) return /* @__PURE__ */ new Map();
|
|
2022
2195
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
2023
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
2196
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValues, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
2024
2197
|
const results = await query;
|
|
2025
2198
|
const resultMap = /* @__PURE__ */ new Map();
|
|
2026
|
-
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
2027
2199
|
for (const row of results) {
|
|
2028
2200
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
if (
|
|
2201
|
+
if (!hasForeignKeyOnTarget(relation)) continue;
|
|
2202
|
+
const foreignKeyValue = targetRow[relation.foreignKeyOnTarget];
|
|
2203
|
+
if (foreignKeyValue === void 0 || foreignKeyValue === null) continue;
|
|
2204
|
+
const parentId = parentIdByKey.get(String(foreignKeyValue));
|
|
2205
|
+
if (parentId !== void 0) {
|
|
2032
2206
|
const key = String(parentId);
|
|
2033
2207
|
const arr = resultMap.get(key) || [];
|
|
2034
2208
|
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
@@ -2045,7 +2219,7 @@ var RelationService = class {
|
|
|
2045
2219
|
for (const [key, value] of Object.entries(relationValues)) {
|
|
2046
2220
|
const relation = findRelation(resolvedRelations, key);
|
|
2047
2221
|
if (!relation || relation.cardinality !== "many") continue;
|
|
2048
|
-
const targetEntityIds =
|
|
2222
|
+
const targetEntityIds = relationTargetIds(value, key, collection.slug);
|
|
2049
2223
|
const targetCollection = relation.target();
|
|
2050
2224
|
if (relation.kind === "via") {
|
|
2051
2225
|
const parentTableName = getTableName$1(collection);
|
|
@@ -2127,14 +2301,13 @@ var RelationService = class {
|
|
|
2127
2301
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
2128
2302
|
continue;
|
|
2129
2303
|
}
|
|
2130
|
-
const
|
|
2131
|
-
|
|
2132
|
-
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
2304
|
+
const parentKeyValue = (await this.resolveSourceKeys(collection, relation, [id], tx)).keyByParentId.get(String(id));
|
|
2305
|
+
if (parentKeyValue === void 0) throw new Error(`Cannot write relation '${key}' on '${collection.slug}': row '${id}' has no value in \`sourceKey: "${sourceKeyField(relation, collection, this.registry)}"\`, so there is nothing for the related rows to point at.`);
|
|
2133
2306
|
if (targetEntityIds.length > 0) {
|
|
2134
2307
|
const parsedTargetIds = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
2135
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(and(eq(fkCol,
|
|
2136
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]:
|
|
2137
|
-
} else await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(fkCol,
|
|
2308
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(and(eq(fkCol, parentKeyValue), sql`${targetIdCol} NOT IN (${sql.join(parsedTargetIds)})`));
|
|
2309
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: parentKeyValue }).where(inArray(targetIdCol, parsedTargetIds));
|
|
2310
|
+
} else await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(fkCol, parentKeyValue));
|
|
2138
2311
|
} else logger.warn(`Many relation '${key}' in collection '${collection.slug}' lacks write configuration and will be skipped during save.`);
|
|
2139
2312
|
}
|
|
2140
2313
|
}
|
|
@@ -2149,8 +2322,7 @@ var RelationService = class {
|
|
|
2149
2322
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
2150
2323
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
2151
2324
|
const targetIdInfo = targetPks[0];
|
|
2152
|
-
|
|
2153
|
-
const sourceIdInfo = sourcePks[0];
|
|
2325
|
+
requirePrimaryKeys(sourceCollection, this.registry)[0];
|
|
2154
2326
|
if (relation.kind === "via") {
|
|
2155
2327
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
2156
2328
|
continue;
|
|
@@ -2172,13 +2344,14 @@ var RelationService = class {
|
|
|
2172
2344
|
logger.warn(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table for relation '${relation.relationName}'`);
|
|
2173
2345
|
continue;
|
|
2174
2346
|
}
|
|
2175
|
-
const
|
|
2176
|
-
if (
|
|
2347
|
+
const sourceKeyValue = (await this.resolveSourceKeys(sourceCollection, relation, [sourceEntityId], tx)).keyByParentId.get(String(sourceEntityId));
|
|
2348
|
+
if (sourceKeyValue === void 0) throw new Error(`Cannot write relation '${relation.relationName}' on '${sourceCollection.slug}': row '${sourceEntityId}' has no value in \`sourceKey: "${sourceKeyField(relation, sourceCollection, this.registry)}"\`, so there is nothing for the related row to point at.`);
|
|
2349
|
+
if (newValue === null || newValue === void 0) await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn, sourceKeyValue));
|
|
2177
2350
|
else {
|
|
2178
2351
|
const parsedNewTargetId = parseIdValues(newValue, targetPks)[targetIdInfo.fieldName];
|
|
2179
2352
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
2180
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn,
|
|
2181
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]:
|
|
2353
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn, sourceKeyValue));
|
|
2354
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: sourceKeyValue }).where(eq(targetIdField, parsedNewTargetId));
|
|
2182
2355
|
}
|
|
2183
2356
|
} catch (e) {
|
|
2184
2357
|
logger.warn(`Failed to update inverse relation '${relation.relationName}'`, { error: e });
|
|
@@ -2225,7 +2398,7 @@ var RelationService = class {
|
|
|
2225
2398
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
2226
2399
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
2227
2400
|
const targetIdInfo = targetPks[0];
|
|
2228
|
-
const newLinks = newValue.
|
|
2401
|
+
const newLinks = relationTargetIds(newValue, relation.relationName, sourceCollection.slug).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
2229
2402
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
2230
2403
|
[targetJunctionColumn.name]: targetId
|
|
2231
2404
|
}));
|
|
@@ -2269,7 +2442,7 @@ var RelationService = class {
|
|
|
2269
2442
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
2270
2443
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
2271
2444
|
const targetIdInfo = targetPks[0];
|
|
2272
|
-
const newLinks = newValue.
|
|
2445
|
+
const newLinks = relationTargetIds(newValue, relation.relationName, sourceCollection.slug).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
2273
2446
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
2274
2447
|
[targetJunctionColumn.name]: targetId
|
|
2275
2448
|
}));
|
|
@@ -2484,7 +2657,7 @@ function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
|
2484
2657
|
* expects real types. The row's own address is *not* among the columns — it is
|
|
2485
2658
|
* derived by the consumer from the collection's primary keys.
|
|
2486
2659
|
*/
|
|
2487
|
-
function
|
|
2660
|
+
function toFlatRow(row, collection, registry) {
|
|
2488
2661
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
2489
2662
|
const normalized = normalizeDbValues(row, collection);
|
|
2490
2663
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
@@ -2681,17 +2854,35 @@ var FetchService = class {
|
|
|
2681
2854
|
const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
|
|
2682
2855
|
const direct = columnAt(orderBy);
|
|
2683
2856
|
if (direct) return direct;
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
if (foreignKey) return foreignKey;
|
|
2689
|
-
}
|
|
2857
|
+
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : void 0;
|
|
2858
|
+
if (declaredRelation?.kind === "belongsTo") {
|
|
2859
|
+
const foreignKey = columnAt(declaredRelation.localKey);
|
|
2860
|
+
if (foreignKey) return foreignKey;
|
|
2690
2861
|
}
|
|
2691
2862
|
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
2692
2863
|
const foreignKey = columnAt(guess);
|
|
2693
2864
|
if (foreignKey) return foreignKey;
|
|
2694
2865
|
}
|
|
2866
|
+
const collectionName = collection?.slug ?? collection?.name;
|
|
2867
|
+
const onCollection = collectionName ? ` on collection '${collectionName}'` : "";
|
|
2868
|
+
if (declaredRelation && declaredRelation.kind !== "belongsTo") throw ApiError.badRequest(`Cannot sort by '${orderBy}'${onCollection}: it is a to-many relation (${declaredRelation.kind}), which has no single value per row to order by.`, "ORDER_BY_FIELD_NOT_SORTABLE", {
|
|
2869
|
+
field: orderBy,
|
|
2870
|
+
kind: declaredRelation.kind,
|
|
2871
|
+
...collectionName && { collection: collectionName }
|
|
2872
|
+
});
|
|
2873
|
+
if (getUnknownFilterFieldsMode() === "warn") {
|
|
2874
|
+
logger.warn(`Sorting by field '${orderBy}'${onCollection}, but it does not exist in the table — the ORDER BY was dropped and these rows are unsorted.`);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
let validFields = [];
|
|
2878
|
+
try {
|
|
2879
|
+
validFields = Object.keys(getTableColumns(table)).sort();
|
|
2880
|
+
} catch {}
|
|
2881
|
+
throw ApiError.badRequest(`Unknown orderBy field '${orderBy}'${onCollection}` + (validFields.length > 0 ? `. Valid fields: ${validFields.join(", ")}` : ""), "UNKNOWN_ORDER_BY_FIELD", {
|
|
2882
|
+
field: orderBy,
|
|
2883
|
+
...collectionName && { collection: collectionName },
|
|
2884
|
+
...validFields.length > 0 && { validFields }
|
|
2885
|
+
});
|
|
2695
2886
|
}
|
|
2696
2887
|
/**
|
|
2697
2888
|
* Build the `with` config for Drizzle's relational query API.
|
|
@@ -2905,7 +3096,7 @@ var FetchService = class {
|
|
|
2905
3096
|
with: withConfig
|
|
2906
3097
|
});
|
|
2907
3098
|
if (!row) return void 0;
|
|
2908
|
-
const flatRow =
|
|
3099
|
+
const flatRow = toFlatRow(row, collection, this.registry);
|
|
2909
3100
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
2910
3101
|
return flatRow;
|
|
2911
3102
|
} catch (e) {
|
|
@@ -2960,7 +3151,7 @@ var FetchService = class {
|
|
|
2960
3151
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
2961
3152
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
2962
3153
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0, scopeCondition);
|
|
2963
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
3154
|
+
return (await qb.findMany(queryOpts)).map((row) => toFlatRow(row, collection, this.registry));
|
|
2964
3155
|
} catch (e) {
|
|
2965
3156
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
2966
3157
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -3023,7 +3214,7 @@ var FetchService = class {
|
|
|
3023
3214
|
/**
|
|
3024
3215
|
* Fallback path used when db.query is unavailable.
|
|
3025
3216
|
*
|
|
3026
|
-
* The primary path runs the results through `
|
|
3217
|
+
* The primary path runs the results through `toFlatRow`, which maps
|
|
3027
3218
|
* relations from what drizzle already nested — no query per row. This one
|
|
3028
3219
|
* has no nesting to read, so it resolves relations itself, in batches.
|
|
3029
3220
|
*
|
|
@@ -3116,6 +3307,10 @@ var FetchService = class {
|
|
|
3116
3307
|
const filterConditions = this.buildFilterConditions(options.filter, table, effectivePath);
|
|
3117
3308
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
3118
3309
|
}
|
|
3310
|
+
if (options.logical) {
|
|
3311
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, effectivePath, this.filterContext(effectivePath, table));
|
|
3312
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
3313
|
+
}
|
|
3119
3314
|
if (allConditions.length > 0) {
|
|
3120
3315
|
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
3121
3316
|
if (finalCondition) query = query.where(finalCondition);
|
|
@@ -3453,6 +3648,38 @@ function extractCauseMessage(error) {
|
|
|
3453
3648
|
return null;
|
|
3454
3649
|
}
|
|
3455
3650
|
/**
|
|
3651
|
+
* Codes that mean "this connection will never work as configured".
|
|
3652
|
+
*
|
|
3653
|
+
* A wrong password or a database that does not exist is a settled fact about
|
|
3654
|
+
* the connection string, not a transient fault — retrying produces the same
|
|
3655
|
+
* answer forever.
|
|
3656
|
+
*/
|
|
3657
|
+
var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
3658
|
+
"28P01",
|
|
3659
|
+
"28000",
|
|
3660
|
+
"3D000",
|
|
3661
|
+
"42501"
|
|
3662
|
+
]);
|
|
3663
|
+
/**
|
|
3664
|
+
* Describe a failed connection attempt in terms a developer can act on.
|
|
3665
|
+
*
|
|
3666
|
+
* The error a caller catches is Drizzle's wrapper: its message is
|
|
3667
|
+
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
3668
|
+
* the sentence that says what is actually wrong — "password authentication
|
|
3669
|
+
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
3670
|
+
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
3671
|
+
* their `DATABASE_URL` nothing at all.
|
|
3672
|
+
*/
|
|
3673
|
+
function classifyConnectFailure(error) {
|
|
3674
|
+
const pgError = extractPgError(error);
|
|
3675
|
+
const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
|
|
3676
|
+
return {
|
|
3677
|
+
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
3678
|
+
reason,
|
|
3679
|
+
code: pgError?.code
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
/**
|
|
3456
3683
|
* Detect whether an error is specifically a role-switching permission failure
|
|
3457
3684
|
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
3458
3685
|
* as opposed to a table-level permission denial.
|
|
@@ -3715,10 +3942,11 @@ var PersistService = class {
|
|
|
3715
3942
|
} else {
|
|
3716
3943
|
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
3717
3944
|
if (targetColumnName) {
|
|
3718
|
-
const
|
|
3945
|
+
const parentKeyValue = hasForeignKeyOnTarget(hop.relation) ? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId) : parentIdForWrite();
|
|
3946
|
+
if (parentKeyValue === void 0) throw ApiError.badRequest(`Cannot create under "${collectionPath}": the parent row has no value in \`sourceKey\`, so the new row has nothing to point at.`);
|
|
3719
3947
|
const existingValue = effectiveValues[targetColumnName];
|
|
3720
|
-
if (existingValue !== void 0 && existingValue !== null && existingValue !==
|
|
3721
|
-
effectiveValues[targetColumnName] =
|
|
3948
|
+
if (existingValue !== void 0 && existingValue !== null && existingValue !== parentKeyValue) logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
|
|
3949
|
+
effectiveValues[targetColumnName] = parentKeyValue;
|
|
3722
3950
|
}
|
|
3723
3951
|
}
|
|
3724
3952
|
}
|
|
@@ -4996,9 +5224,10 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
4996
5224
|
async checkUniqueField(path, name, value, id, collection) {
|
|
4997
5225
|
return this.dataService.checkUniqueField(path, name, value, id, collection?.databaseId);
|
|
4998
5226
|
}
|
|
4999
|
-
async count({ path, collection, filter, searchString }) {
|
|
5227
|
+
async count({ path, collection, filter, logical, searchString }) {
|
|
5000
5228
|
return this.dataService.count(path, {
|
|
5001
5229
|
filter,
|
|
5230
|
+
logical,
|
|
5002
5231
|
searchString
|
|
5003
5232
|
});
|
|
5004
5233
|
}
|
|
@@ -5367,7 +5596,7 @@ var DatabasePoolManager = class {
|
|
|
5367
5596
|
const url = new URL(this.rootConnectionString);
|
|
5368
5597
|
url.pathname = `/${databaseName}`;
|
|
5369
5598
|
const pool = new Pool({
|
|
5370
|
-
connectionString: url.toString(),
|
|
5599
|
+
connectionString: pinSearchPath(url.toString()),
|
|
5371
5600
|
max: 10,
|
|
5372
5601
|
idleTimeoutMillis: 1e4,
|
|
5373
5602
|
allowExitOnIdle: true
|
|
@@ -5955,8 +6184,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5955
6184
|
const tableVarName = getTableVarName(tableName);
|
|
5956
6185
|
if (isJunction && relation && sourceCollection && isManyToMany(relation)) {
|
|
5957
6186
|
const targetCollection = relation.target();
|
|
5958
|
-
const
|
|
5959
|
-
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
6187
|
+
const tableCreator = "pgTable";
|
|
5960
6188
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
5961
6189
|
const { sourceColumn, targetColumn } = relation.through;
|
|
5962
6190
|
const refOptions = `{ onDelete: \"${relation.onDelete ?? "cascade"}\" }`;
|
|
@@ -6070,8 +6298,9 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
6070
6298
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
6071
6299
|
const otherTableVar = getTableVarName(getTableName$1(otherCollection));
|
|
6072
6300
|
const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
|
|
6301
|
+
const referencedKey = otherRel.sourceKey ? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey) : getPrimaryKeyName(otherCollection);
|
|
6073
6302
|
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
6074
|
-
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${
|
|
6303
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${referencedKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
6075
6304
|
emittedRelationNames.add(deduplicationKey);
|
|
6076
6305
|
}
|
|
6077
6306
|
}
|
|
@@ -7229,14 +7458,6 @@ function resolveChannelBusSetting(configured) {
|
|
|
7229
7458
|
return raw === "memory" ? { type: "memory" } : { type: "postgres" };
|
|
7230
7459
|
}
|
|
7231
7460
|
/**
|
|
7232
|
-
* @deprecated Use {@link resolveChannelBusSetting}, which also accepts a
|
|
7233
|
-
* supplied {@link ChannelBus} instance. Kept as a narrow alias so existing
|
|
7234
|
-
* config-only callers keep their exact types.
|
|
7235
|
-
*/
|
|
7236
|
-
function resolveChannelBusConfig(configured) {
|
|
7237
|
-
return resolveChannelBusSetting(configured);
|
|
7238
|
-
}
|
|
7239
|
-
/**
|
|
7240
7461
|
* Produce the bus a setting asks for.
|
|
7241
7462
|
*
|
|
7242
7463
|
* An instance is handed straight back — constructing it was the application's
|
|
@@ -7713,7 +7934,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7713
7934
|
if (this.driver) {
|
|
7714
7935
|
const collection = this.registry.getCollectionByPath(notifyPath);
|
|
7715
7936
|
const activeAuth = authContext || {
|
|
7716
|
-
uid: "
|
|
7937
|
+
uid: "anonymous",
|
|
7717
7938
|
roles: ["anon"]
|
|
7718
7939
|
};
|
|
7719
7940
|
return await this.db.transaction(async (tx) => {
|
|
@@ -7842,7 +8063,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
7842
8063
|
if (this.driver) {
|
|
7843
8064
|
const collection = this.registry.getCollectionByPath(notifyPath);
|
|
7844
8065
|
const activeAuth = authContext || {
|
|
7845
|
-
uid: "
|
|
8066
|
+
uid: "anonymous",
|
|
7846
8067
|
roles: ["anon"]
|
|
7847
8068
|
};
|
|
7848
8069
|
return await this.db.transaction(async (tx) => {
|
|
@@ -8847,7 +9068,7 @@ function createBackupCron(config) {
|
|
|
8847
9068
|
enabled: config.enabled ?? true,
|
|
8848
9069
|
timeoutSeconds: 3600,
|
|
8849
9070
|
async handler({ log }) {
|
|
8850
|
-
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-
|
|
9071
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-CD8o_1Sl.js").then((n) => n.r);
|
|
8851
9072
|
const { destination } = config;
|
|
8852
9073
|
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 }).`);
|
|
8853
9074
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -8978,6 +9199,11 @@ function findRelationDefects(collections, registry) {
|
|
|
8978
9199
|
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
8979
9200
|
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
8980
9201
|
});
|
|
9202
|
+
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
9203
|
+
...at,
|
|
9204
|
+
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
9205
|
+
fix: targetColumns.has(relation.sourceKey) ? `it is a column on the *target* table \`${targetTableName}\` — \`sourceKey\` names the column on this collection that the target's foreign key points at, so it must be one of: ${quote(sourceColumns)}` : `add the column, or set \`sourceKey\` to one of: ${quote(sourceColumns)}`
|
|
9206
|
+
});
|
|
8981
9207
|
break;
|
|
8982
9208
|
case "manyToMany": {
|
|
8983
9209
|
const { table, sourceColumn, targetColumn } = relation.through;
|
|
@@ -9729,21 +9955,6 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
9729
9955
|
}
|
|
9730
9956
|
//#endregion
|
|
9731
9957
|
//#region src/auth/services.ts
|
|
9732
|
-
function getColumnKey(table, ...keys) {
|
|
9733
|
-
if (!table) return void 0;
|
|
9734
|
-
for (const key of keys) {
|
|
9735
|
-
if (key in table) return key;
|
|
9736
|
-
const snake = toSnakeCase(key);
|
|
9737
|
-
if (snake in table) return snake;
|
|
9738
|
-
const camel = camelCase(key);
|
|
9739
|
-
if (camel in table) return camel;
|
|
9740
|
-
}
|
|
9741
|
-
}
|
|
9742
|
-
function getColumn(table, ...keys) {
|
|
9743
|
-
if (!table) return void 0;
|
|
9744
|
-
const key = getColumnKey(table, ...keys);
|
|
9745
|
-
return key ? table[key] : void 0;
|
|
9746
|
-
}
|
|
9747
9958
|
/**
|
|
9748
9959
|
* The single definition of what an email address looks like in storage.
|
|
9749
9960
|
*
|
|
@@ -9758,9 +9969,25 @@ function getColumn(table, ...keys) {
|
|
|
9758
9969
|
*
|
|
9759
9970
|
* Whitespace goes too: a trailing space survives the fold and reproduces the
|
|
9760
9971
|
* problem exactly.
|
|
9972
|
+
*
|
|
9973
|
+
* Re-exported rather than defined here: `@rebasepro/server` and
|
|
9974
|
+
* `@rebasepro/server-mongo` write this column too, and a second copy of this
|
|
9975
|
+
* rule is the defect it exists to prevent.
|
|
9761
9976
|
*/
|
|
9762
|
-
function
|
|
9763
|
-
|
|
9977
|
+
function getColumnKey(table, ...keys) {
|
|
9978
|
+
if (!table) return void 0;
|
|
9979
|
+
for (const key of keys) {
|
|
9980
|
+
if (key in table) return key;
|
|
9981
|
+
const snake = toSnakeCase(key);
|
|
9982
|
+
if (snake in table) return snake;
|
|
9983
|
+
const camel = camelCase(key);
|
|
9984
|
+
if (camel in table) return camel;
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
function getColumn(table, ...keys) {
|
|
9988
|
+
if (!table) return void 0;
|
|
9989
|
+
const key = getColumnKey(table, ...keys);
|
|
9990
|
+
return key ? table[key] : void 0;
|
|
9764
9991
|
}
|
|
9765
9992
|
/**
|
|
9766
9993
|
* PostgreSQL implementation of UserRepository.
|
|
@@ -11101,6 +11328,31 @@ function humanize(snakeName) {
|
|
|
11101
11328
|
return snakeName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
11102
11329
|
}
|
|
11103
11330
|
//#endregion
|
|
11331
|
+
//#region src/schema/introspect-db-types.ts
|
|
11332
|
+
/**
|
|
11333
|
+
* The PostgreSQL type → Rebase property type mapping.
|
|
11334
|
+
*
|
|
11335
|
+
* Split out of `introspect-db-logic` so that the structural analysis can use it
|
|
11336
|
+
* without importing the generator, which imports the analysis. Re-exported from
|
|
11337
|
+
* `introspect-db-logic` so existing callers keep their import path.
|
|
11338
|
+
*/
|
|
11339
|
+
/**
|
|
11340
|
+
* Map a PostgreSQL data type to a Rebase property type.
|
|
11341
|
+
*/
|
|
11342
|
+
function mapPgType(dataType) {
|
|
11343
|
+
const dt = dataType.toLowerCase();
|
|
11344
|
+
if (dt === "interval") return "string";
|
|
11345
|
+
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
11346
|
+
if (dt.includes("int") || dt.includes("numeric") || dt.includes("decimal") || dt.includes("serial") || dt === "real" || dt === "float4" || dt === "float8" || dt === "double precision" || dt === "money") return "number";
|
|
11347
|
+
if (dt.includes("bool")) return "boolean";
|
|
11348
|
+
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
11349
|
+
if (dt === "json" || dt === "jsonb") return "map";
|
|
11350
|
+
if (dt === "bytea") return "binary";
|
|
11351
|
+
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
11352
|
+
if (dt === "uuid") return "string";
|
|
11353
|
+
return "string";
|
|
11354
|
+
}
|
|
11355
|
+
//#endregion
|
|
11104
11356
|
//#region src/schema/introspect-db-logic.ts
|
|
11105
11357
|
var IRREGULAR_SINGULARS = {
|
|
11106
11358
|
people: "person",
|
|
@@ -11116,6 +11368,36 @@ var IRREGULAR_SINGULARS = {
|
|
|
11116
11368
|
criteria: "criterion",
|
|
11117
11369
|
phenomena: "phenomenon"
|
|
11118
11370
|
};
|
|
11371
|
+
/**
|
|
11372
|
+
* Plurals in "-ves" whose singular really ends in f/fe, and which of the two.
|
|
11373
|
+
*
|
|
11374
|
+
* A blanket "-ves" -> "-f" rule gets `knives` -> `knif`, and mangles every
|
|
11375
|
+
* ordinary "-ive" noun that happens to be plural along with it: `archives` ->
|
|
11376
|
+
* `archif`, `objectives` -> `objectif`. The set of English words that genuinely
|
|
11377
|
+
* swap f/fe for ves is small and closed, so it is listed rather than guessed —
|
|
11378
|
+
* anything else ending in "ves" drops the trailing 's' like any other plural.
|
|
11379
|
+
*
|
|
11380
|
+
* Matched on the whole word, not on the suffix: `olives` ends in `lives`.
|
|
11381
|
+
*/
|
|
11382
|
+
var VES_SINGULAR_ENDINGS = {
|
|
11383
|
+
calves: "f",
|
|
11384
|
+
dwarves: "f",
|
|
11385
|
+
elves: "f",
|
|
11386
|
+
halves: "f",
|
|
11387
|
+
hooves: "f",
|
|
11388
|
+
leaves: "f",
|
|
11389
|
+
loaves: "f",
|
|
11390
|
+
scarves: "f",
|
|
11391
|
+
selves: "f",
|
|
11392
|
+
sheaves: "f",
|
|
11393
|
+
shelves: "f",
|
|
11394
|
+
thieves: "f",
|
|
11395
|
+
wharves: "f",
|
|
11396
|
+
wolves: "f",
|
|
11397
|
+
knives: "fe",
|
|
11398
|
+
lives: "fe",
|
|
11399
|
+
wives: "fe"
|
|
11400
|
+
};
|
|
11119
11401
|
/** Words ending in 's' that are already singular. */
|
|
11120
11402
|
var UNCOUNTABLE = /* @__PURE__ */ new Set([
|
|
11121
11403
|
"status",
|
|
@@ -11154,7 +11436,7 @@ function singularize(word) {
|
|
|
11154
11436
|
if (UNCOUNTABLE.has(lower)) return word;
|
|
11155
11437
|
if (lower.endsWith("ices") && lower.length > 5) return word.slice(0, -4) + "ex";
|
|
11156
11438
|
if (lower.endsWith("ies") && lower.length > 3) return word.slice(0, -3) + "y";
|
|
11157
|
-
if (lower
|
|
11439
|
+
if (VES_SINGULAR_ENDINGS[lower]) return word.slice(0, -3) + VES_SINGULAR_ENDINGS[lower];
|
|
11158
11440
|
if (lower.endsWith("ches") || lower.endsWith("shes") || lower.endsWith("sses") || lower.endsWith("xes") || lower.endsWith("zes")) return word.slice(0, -2);
|
|
11159
11441
|
if (lower.endsWith("ses") && !lower.endsWith("sses")) return word.slice(0, -1);
|
|
11160
11442
|
if (lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us") && !lower.endsWith("is")) return word.slice(0, -1);
|
|
@@ -11175,22 +11457,6 @@ function getIconForTable(tableName) {
|
|
|
11175
11457
|
if (table.includes("comment") || table.includes("review") || table.includes("feedback")) return "MessageCircle";
|
|
11176
11458
|
return "Database";
|
|
11177
11459
|
}
|
|
11178
|
-
/**
|
|
11179
|
-
* Map a PostgreSQL data type to a Rebase property type.
|
|
11180
|
-
*/
|
|
11181
|
-
function mapPgType(dataType) {
|
|
11182
|
-
const dt = dataType.toLowerCase();
|
|
11183
|
-
if (dt === "interval") return "string";
|
|
11184
|
-
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
11185
|
-
if (dt.includes("int") || dt.includes("numeric") || dt.includes("decimal") || dt.includes("serial") || dt === "real" || dt === "float4" || dt === "float8" || dt === "double precision" || dt === "money") return "number";
|
|
11186
|
-
if (dt.includes("bool")) return "boolean";
|
|
11187
|
-
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
11188
|
-
if (dt === "json" || dt === "jsonb") return "map";
|
|
11189
|
-
if (dt === "bytea") return "binary";
|
|
11190
|
-
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
11191
|
-
if (dt === "uuid") return "string";
|
|
11192
|
-
return "string";
|
|
11193
|
-
}
|
|
11194
11460
|
function buildEnumMap(enumValues) {
|
|
11195
11461
|
const enumMap = /* @__PURE__ */ new Map();
|
|
11196
11462
|
for (const ev of enumValues) {
|
|
@@ -11210,6 +11476,20 @@ function buildTablesMap(tables, columns, pks, fks) {
|
|
|
11210
11476
|
});
|
|
11211
11477
|
return tablesMap;
|
|
11212
11478
|
}
|
|
11479
|
+
/**
|
|
11480
|
+
* Join tables, identified by column name.
|
|
11481
|
+
*
|
|
11482
|
+
* Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,
|
|
11483
|
+
* which asks the database instead: two single-column keys, unique together, no
|
|
11484
|
+
* payload column, nothing referencing the table. This rule folds away
|
|
11485
|
+
* `northwind.order_details` — which has the key shape and carries unit price,
|
|
11486
|
+
* quantity and discount — because it recognises `id`, `created_at` and
|
|
11487
|
+
* `updated_at` by name and calls everything else a foreign key.
|
|
11488
|
+
*
|
|
11489
|
+
* Still used by `./introspect-runtime`, which builds collections in memory from
|
|
11490
|
+
* a narrower set of catalog queries and has no unique-constraint or row-count
|
|
11491
|
+
* data to reason with.
|
|
11492
|
+
*/
|
|
11213
11493
|
function identifyJoinTables(tablesMap) {
|
|
11214
11494
|
const joinTables = /* @__PURE__ */ new Set();
|
|
11215
11495
|
for (const [tableName, meta] of tablesMap.entries()) if (meta.fks.length === 2) {
|
|
@@ -11332,11 +11612,27 @@ function buildProperties(meta, enumMap) {
|
|
|
11332
11612
|
return properties;
|
|
11333
11613
|
}
|
|
11334
11614
|
/**
|
|
11335
|
-
* Owning relations, derived from this table's foreign keys
|
|
11336
|
-
* `generateCollectionFile`
|
|
11337
|
-
*
|
|
11338
|
-
|
|
11339
|
-
|
|
11615
|
+
* Owning relations, derived from this table's foreign keys — the same shape
|
|
11616
|
+
* `generateCollectionFile` writes into a collection file: a `relation` property
|
|
11617
|
+
* whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
|
|
11618
|
+
*
|
|
11619
|
+
* The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
|
|
11620
|
+
* relations from `property.relation` and `resolveRelation` requires `target` to
|
|
11621
|
+
* be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
|
|
11622
|
+
* property with the slug as a bare string, which satisfies neither. Nothing
|
|
11623
|
+
* threw — the resolver simply skipped every such property and reported that the
|
|
11624
|
+
* collection had no relations. So an introspected BaaS collection had its FK
|
|
11625
|
+
* columns removed from `properties` (they "surface as relations") and then no
|
|
11626
|
+
* resolvable relation to surface as, which is why writing the FK column
|
|
11627
|
+
* directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
|
|
11628
|
+
* learns that column from the resolved relation's `localKey`.
|
|
11629
|
+
*
|
|
11630
|
+
* The thunk closes over the collections being built in this same pass rather
|
|
11631
|
+
* than importing a module, which is what a runtime introspection has instead of
|
|
11632
|
+
* generated files. It is called lazily, after the map is fully populated, so a
|
|
11633
|
+
* table may reference one introspected later.
|
|
11634
|
+
*/
|
|
11635
|
+
function buildRelations(meta, slugByTable, collectionBySlug) {
|
|
11340
11636
|
const relations = {};
|
|
11341
11637
|
for (const fk of meta.fks) {
|
|
11342
11638
|
const targetSlug = slugByTable.get(fk.foreign_table_name);
|
|
@@ -11346,10 +11642,11 @@ function buildRelations(meta, slugByTable) {
|
|
|
11346
11642
|
relations[key] = {
|
|
11347
11643
|
name: humanize(key),
|
|
11348
11644
|
type: "relation",
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11645
|
+
relation: {
|
|
11646
|
+
kind: "belongsTo",
|
|
11647
|
+
target: () => collectionBySlug.get(targetSlug),
|
|
11648
|
+
localKey: fk.column_name
|
|
11649
|
+
}
|
|
11353
11650
|
};
|
|
11354
11651
|
}
|
|
11355
11652
|
return relations;
|
|
@@ -11364,6 +11661,7 @@ function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }, pgSchema
|
|
|
11364
11661
|
const slugByTable = /* @__PURE__ */ new Map();
|
|
11365
11662
|
for (const tableName of tablesMap.keys()) if (!joinTables.has(tableName)) slugByTable.set(tableName, tableName);
|
|
11366
11663
|
const collections = [];
|
|
11664
|
+
const collectionBySlug = /* @__PURE__ */ new Map();
|
|
11367
11665
|
for (const [tableName, meta] of tablesMap) {
|
|
11368
11666
|
if (joinTables.has(tableName)) continue;
|
|
11369
11667
|
const collectionName = humanize(tableName);
|
|
@@ -11376,10 +11674,11 @@ function buildCollectionsFromSchema({ tablesMap, enumMap, joinTables }, pgSchema
|
|
|
11376
11674
|
icon: getIconForTable(tableName),
|
|
11377
11675
|
properties: {
|
|
11378
11676
|
...buildProperties(meta, enumMap),
|
|
11379
|
-
...buildRelations(meta, slugByTable)
|
|
11677
|
+
...buildRelations(meta, slugByTable, collectionBySlug)
|
|
11380
11678
|
}
|
|
11381
11679
|
};
|
|
11382
11680
|
collections.push(collection);
|
|
11681
|
+
collectionBySlug.set(tableName, collection);
|
|
11383
11682
|
}
|
|
11384
11683
|
return collections;
|
|
11385
11684
|
}
|
|
@@ -11569,6 +11868,32 @@ function isEconnrefused(err) {
|
|
|
11569
11868
|
* Implements the `BackendBootstrapper` interface for PostgreSQL.
|
|
11570
11869
|
*/
|
|
11571
11870
|
/**
|
|
11871
|
+
* Which table name the boot-time drift check should look for, for one collection.
|
|
11872
|
+
*
|
|
11873
|
+
* A declared `table` IS the table name, not a hint to be second-guessed. This
|
|
11874
|
+
* used to ask the registry whether it had indexed the declared name and fall
|
|
11875
|
+
* back to the SLUG when it had not — but "the registry does not know this table"
|
|
11876
|
+
* is exactly the condition the drift check exists to report, so the fallback
|
|
11877
|
+
* fired precisely when it was most harmful.
|
|
11878
|
+
*
|
|
11879
|
+
* A collection with `slug: "usage-daily", table: "usage_daily"` was reported as
|
|
11880
|
+
* missing table `usage-daily`: a name that does not exist, should never exist,
|
|
11881
|
+
* and that nobody can find by looking. Worse, the remediation the caller prints
|
|
11882
|
+
* says to run `rebase db push` — which would then CREATE that invented table
|
|
11883
|
+
* beside the correct one, the same "second copy" hazard the misplaced-schema
|
|
11884
|
+
* branch further down exists to prevent. Seen in production, where a correctly
|
|
11885
|
+
* migrated database reported drift on every boot.
|
|
11886
|
+
*
|
|
11887
|
+
* The slug is used only when nothing was declared, which is the config shape
|
|
11888
|
+
* where the slug genuinely is the table name.
|
|
11889
|
+
*
|
|
11890
|
+
* Exported for its own test: the caller needs a live pool and a real database,
|
|
11891
|
+
* and this is the part that was wrong.
|
|
11892
|
+
*/
|
|
11893
|
+
function resolveDriftCheckName(col, registeredTableNames) {
|
|
11894
|
+
return (isRelationalCollectionConfig(col) ? col.table : void 0) ?? registeredTableNames.find((k) => k === col.slug) ?? col.slug;
|
|
11895
|
+
}
|
|
11896
|
+
/**
|
|
11572
11897
|
* Default PostgreSQL bootstrapper.
|
|
11573
11898
|
*
|
|
11574
11899
|
* Use it to register Postgres with `initializeRebaseBackend()`:
|
|
@@ -11639,14 +11964,25 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11639
11964
|
logger.error(message);
|
|
11640
11965
|
throw new Error(`Cannot connect to PostgreSQL at ${hostInfo}: connection refused. Is the database running?`);
|
|
11641
11966
|
}
|
|
11642
|
-
|
|
11967
|
+
const { fatal, reason, code } = classifyConnectFailure(err);
|
|
11968
|
+
const detail = code ? ` [${code}]` : "";
|
|
11969
|
+
if (fatal) {
|
|
11970
|
+
logger.error(`
|
|
11971
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
11972
|
+
❌ PostgreSQL refused the connection
|
|
11973
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
11974
|
+
|
|
11975
|
+
${reason}${detail}\n\n The server is reachable, so this is the credentials or the\n database name in DATABASE_URL — check them in your .env.\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
|
11976
|
+
throw new Error(`PostgreSQL refused the connection: ${reason}${detail}`);
|
|
11977
|
+
}
|
|
11978
|
+
logger.error(`❌ Failed to connect to PostgreSQL: ${reason}${detail}`, { error: err });
|
|
11643
11979
|
logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
|
|
11644
11980
|
}
|
|
11645
11981
|
const realtimeService = new RealtimeService(schemaAwareDb, registry);
|
|
11646
11982
|
let readDb;
|
|
11647
11983
|
const readUrl = process.env.DATABASE_READ_URL;
|
|
11648
11984
|
if (readUrl && readUrl !== pgConfig.connectionString) try {
|
|
11649
|
-
const { createReadReplicaConnection } = await import("./connection-
|
|
11985
|
+
const { createReadReplicaConnection } = await import("./connection-BuZ97wsr.js").then((n) => n.t);
|
|
11650
11986
|
readDb = createReadReplicaConnection(readUrl, mergedSchema).db;
|
|
11651
11987
|
logger.info("📖 [PostgresBootstrapper] Read replica connection established");
|
|
11652
11988
|
} catch (err) {
|
|
@@ -11748,40 +12084,61 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11748
12084
|
try {
|
|
11749
12085
|
const registeredCollections = registry.getCollections();
|
|
11750
12086
|
if (registeredCollections.length > 0) {
|
|
11751
|
-
const schemasList = Array.from(new Set(registeredCollections.map((c) => "schema" in c && c.schema ? c.schema : "public"))).map((s) => `'${s}'`).join(",");
|
|
11752
12087
|
const result = await schemaAwareDb.execute(sql.raw(`
|
|
11753
12088
|
SELECT table_name, table_schema
|
|
11754
12089
|
FROM information_schema.tables
|
|
11755
|
-
WHERE table_schema IN (
|
|
12090
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
11756
12091
|
AND table_type = 'BASE TABLE'
|
|
11757
12092
|
`));
|
|
12093
|
+
const tablesByName = /* @__PURE__ */ new Map();
|
|
12094
|
+
for (const row of result.rows) {
|
|
12095
|
+
const schemas = tablesByName.get(row.table_name) ?? [];
|
|
12096
|
+
schemas.push(row.table_schema);
|
|
12097
|
+
tablesByName.set(row.table_name, schemas);
|
|
12098
|
+
}
|
|
11758
12099
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
11759
12100
|
const missing = [];
|
|
11760
12101
|
for (const col of registeredCollections) {
|
|
11761
12102
|
if (col.auth?.enabled) continue;
|
|
11762
12103
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
11763
|
-
const
|
|
11764
|
-
const tableName = registry.hasTableForCollection(declaredTable ?? col.slug) ? declaredTable ?? col.slug : col.slug;
|
|
11765
|
-
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
12104
|
+
const checkName = resolveDriftCheckName(col, registry.getTableNames());
|
|
11766
12105
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
11767
12106
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
11768
12107
|
slug: col.slug,
|
|
11769
|
-
table: fullCheckName
|
|
12108
|
+
table: fullCheckName,
|
|
12109
|
+
foundIn: (tablesByName.get(checkName) ?? []).filter((s) => s !== schemaName)
|
|
11770
12110
|
});
|
|
11771
12111
|
}
|
|
11772
12112
|
if (missing.length > 0) {
|
|
11773
|
-
const lines = missing.map((m) => ` • collection "${m.slug}" → table "${m.table}"`);
|
|
12113
|
+
const lines = missing.map((m) => ` • collection "${m.slug}" → table "${m.table}"` + (m.foundIn.length > 0 ? ` — but a table of that name exists in ${m.foundIn.map((s) => `"${s}"`).join(", ")}` : ""));
|
|
12114
|
+
const misplaced = missing.filter((m) => m.foundIn.length > 0);
|
|
12115
|
+
const misplacedHelp = misplaced.length === 0 ? [] : [
|
|
12116
|
+
" Those tables exist — in the wrong schema. Unqualified SQL run by a",
|
|
12117
|
+
" role whose name matches a schema (`rebase` is both, by default)",
|
|
12118
|
+
" resolves through search_path's \"$user\" and creates there. Move them:",
|
|
12119
|
+
...misplaced.map((m) => ` ALTER TABLE "${m.foundIn[0]}"."${m.table.split(".").pop()}" SET SCHEMA "${m.table.includes(".") ? m.table.split(".")[0] : "public"}";`),
|
|
12120
|
+
" and qualify the SQL that created them, or pin search_path in DATABASE_URL",
|
|
12121
|
+
" (`?options=-c%20search_path%3Dpublic`).",
|
|
12122
|
+
""
|
|
12123
|
+
];
|
|
11774
12124
|
logger.warn([
|
|
11775
12125
|
"",
|
|
11776
|
-
"
|
|
11777
|
-
|
|
11778
|
-
"
|
|
11779
|
-
...
|
|
11780
|
-
"
|
|
11781
|
-
"
|
|
11782
|
-
"
|
|
11783
|
-
"
|
|
11784
|
-
"
|
|
12126
|
+
"⚠️ SCHEMA DRIFT — the database is missing tables this backend serves:",
|
|
12127
|
+
...lines,
|
|
12128
|
+
"",
|
|
12129
|
+
...misplacedHelp,
|
|
12130
|
+
" This runtime applies the collection schema at boot unless",
|
|
12131
|
+
" REBASE_MIGRATE_ON_BOOT=none. Check the \"Collection schema\" / \"policies\"",
|
|
12132
|
+
" log lines above — this drift means that step was off, skipped, or failed.",
|
|
12133
|
+
" • Managed cloud: redeploy with REBASE_MIGRATE_ON_BOOT unset or",
|
|
12134
|
+
" \"ensure\"; the runtime applies the schema to the tenant DB.",
|
|
12135
|
+
" If the log above says the driver does not implement collection-table",
|
|
12136
|
+
" creation, THIS driver is too old to do it. A driver is installed from",
|
|
12137
|
+
" your bundle's dependencies, not supplied by the platform image, so a",
|
|
12138
|
+
" newer runtime will not update it: bump \"@rebasepro/server-postgres\"",
|
|
12139
|
+
" in your project's package.json and redeploy.",
|
|
12140
|
+
" • Self-host: run `rebase db push` (dev) or `rebase db migrate` (prod)",
|
|
12141
|
+
" against DATABASE_URL.",
|
|
11785
12142
|
""
|
|
11786
12143
|
].join("\n"));
|
|
11787
12144
|
}
|
|
@@ -11868,18 +12225,46 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
11868
12225
|
*/
|
|
11869
12226
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
11870
12227
|
const internals = driverResult.internals;
|
|
11871
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
11872
|
-
|
|
12228
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-Da2oGkX2.js");
|
|
12229
|
+
const plan = await ensureCollectionTables({ async query(text) {
|
|
12230
|
+
const result = await internals.db.execute(sql.raw(text));
|
|
12231
|
+
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
12232
|
+
} }, collections, log);
|
|
12233
|
+
for (const failure of plan.failures) logger.warn(`🔗 [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}`);
|
|
12234
|
+
return { applied: plan.actions.length - plan.failures.length };
|
|
12235
|
+
},
|
|
12236
|
+
/**
|
|
12237
|
+
* Apply the collections' RLS policies — ENABLE ROW LEVEL SECURITY and the
|
|
12238
|
+
* `securityRules` compiled to `CREATE POLICY` — so a freshly provisioned
|
|
12239
|
+
* database serves data instead of denying every user-context read.
|
|
12240
|
+
*
|
|
12241
|
+
* The companion to {@link ensureCollectionSchema}: that creates the
|
|
12242
|
+
* tables, this makes them servable. Boot runs it *after* auth
|
|
12243
|
+
* initialization, because the generated policies call `auth.uid()` /
|
|
12244
|
+
* `auth.roles()`, and `CREATE POLICY` validates those functions exist.
|
|
12245
|
+
*
|
|
12246
|
+
* Runs through the same drizzle handle, one statement at a time (that
|
|
12247
|
+
* handle speaks the extended query protocol, which rejects multi-command
|
|
12248
|
+
* strings). Failures are per-table and non-fatal: a table left un-policed
|
|
12249
|
+
* stays RLS-enabled, so it denies rather than leaks.
|
|
12250
|
+
*/
|
|
12251
|
+
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12252
|
+
const internals = driverResult.internals;
|
|
12253
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-BrUVgjz3.js");
|
|
12254
|
+
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
11873
12255
|
const result = await internals.db.execute(sql.raw(text));
|
|
11874
12256
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
11875
|
-
} }, collections, log)
|
|
12257
|
+
} }, collections, log);
|
|
12258
|
+
for (const skip of outcome.skipped) logger.warn(`🔐 [rls] Policies not applied to "${skip.table}": ${skip.reason}`);
|
|
12259
|
+
for (const failure of outcome.failures) logger.warn(`🔐 [rls] Could not fully apply policies to "${failure.table}" — it stays locked (denies) until this is resolved: ${failure.error}`);
|
|
12260
|
+
return { applied: outcome.policiesApplied };
|
|
11876
12261
|
},
|
|
11877
12262
|
getAdmin(driverResult) {
|
|
11878
12263
|
return driverResult.internals.driver.admin;
|
|
11879
12264
|
},
|
|
11880
12265
|
mountRoutes(app, basePath, driverResult) {},
|
|
11881
12266
|
async initializeWebsockets(server, realtimeService, driver, config, adapter) {
|
|
11882
|
-
const { createPostgresWebSocket } = await import("./websocket-
|
|
12267
|
+
const { createPostgresWebSocket } = await import("./websocket-B2LsrINK.js").then((n) => n.n);
|
|
11883
12268
|
createPostgresWebSocket(server, realtimeService, driver, config, adapter);
|
|
11884
12269
|
}
|
|
11885
12270
|
};
|
|
@@ -11905,9 +12290,11 @@ function createPostgresAdapter(pgConfig) {
|
|
|
11905
12290
|
async initializeHistory(config, driverResult) {
|
|
11906
12291
|
if (bootstrapper.initializeHistory) return bootstrapper.initializeHistory(config, driverResult);
|
|
11907
12292
|
},
|
|
11908
|
-
initializeWebsockets(server, realtimeService, driver, config) {
|
|
11909
|
-
if (bootstrapper.initializeWebsockets) return bootstrapper.initializeWebsockets(server, realtimeService, driver, config);
|
|
12293
|
+
initializeWebsockets(server, realtimeService, driver, config, adapter) {
|
|
12294
|
+
if (bootstrapper.initializeWebsockets) return bootstrapper.initializeWebsockets(server, realtimeService, driver, config, adapter);
|
|
11910
12295
|
},
|
|
12296
|
+
ensureCollectionSchema: bootstrapper.ensureCollectionSchema ? (collections, driverResult, log) => bootstrapper.ensureCollectionSchema(collections, driverResult, log) : void 0,
|
|
12297
|
+
ensureCollectionPolicies: bootstrapper.ensureCollectionPolicies ? (collections, driverResult, log) => bootstrapper.ensureCollectionPolicies(collections, driverResult, log) : void 0,
|
|
11911
12298
|
getAdmin(driverResult) {
|
|
11912
12299
|
if (bootstrapper.getAdmin) return bootstrapper.getAdmin(driverResult);
|
|
11913
12300
|
},
|
|
@@ -11917,6 +12304,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
11917
12304
|
};
|
|
11918
12305
|
}
|
|
11919
12306
|
//#endregion
|
|
11920
|
-
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, CHANNEL_BUS_NOTIFY_CHANNEL, DEFAULT_BATCH_WINDOW_MS, DatabasePoolManager, DrizzleConditionBuilder, MemoryChannelBus, PG_NOTIFY_MAX_PAYLOAD_BYTES, PostgresBackendDriver, PostgresChannelBus, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, applyGlobals, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgDumpallGlobalsArgs, buildPgRestoreArgs, buildPgRestoreListArgs, checkToolServerCompatibility, configureUnknownFilterFields, createAuthSchema, createBackupCron, createChannelBus, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, frameByteLength, generateSchema, getServerVersionMajor, getUnknownFilterFieldsMode, globalsFileForDump, guardPoolAgainstDirtyRelease, isChannelBusInstance, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseChannelBusFrame, parseChannelBusPayload, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations,
|
|
12307
|
+
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, CHANNEL_BUS_NOTIFY_CHANNEL, DEFAULT_BATCH_WINDOW_MS, DatabasePoolManager, DrizzleConditionBuilder, MemoryChannelBus, PG_NOTIFY_MAX_PAYLOAD_BYTES, PostgresBackendDriver, PostgresChannelBus, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, applyGlobals, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgDumpallGlobalsArgs, buildPgRestoreArgs, buildPgRestoreListArgs, buildRowSecurityPgOptions, checkToolServerCompatibility, configureUnknownFilterFields, createAuthSchema, createBackupCron, createChannelBus, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, diagnoseRowSecurityDumpFailure, ensureDatabaseExists, frameByteLength, generateSchema, getServerVersionMajor, getUnknownFilterFieldsMode, globalsFileForDump, guardPoolAgainstDirtyRelease, isChannelBusInstance, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseChannelBusFrame, parseChannelBusPayload, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, pinSearchPath, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveChannelBusSetting, resolveConnectionString, resolveDriftCheckName, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, splitGlobalsStatements, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, validateDump, withDatabaseName };
|
|
11921
12308
|
|
|
11922
12309
|
//# sourceMappingURL=index.es.js.map
|