@rebasepro/server-postgres 0.12.1-canary.g4e7bcbf → 0.12.1-canary.g52d71ee
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/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-DLb2drIH.js → backup-service-CD8o_1Sl.js} +136 -10
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/{ensure-collection-policies-Dv21KDMJ.js → ensure-collection-policies-ViG8XiPn.js} +2 -2
- package/dist/{ensure-collection-policies-Dv21KDMJ.js.map → ensure-collection-policies-ViG8XiPn.js.map} +1 -1
- package/dist/{ensure-collection-tables-CT6xHB1d.js → ensure-collection-tables-CBQdOETu.js} +2 -2
- package/dist/{ensure-collection-tables-CT6xHB1d.js.map → ensure-collection-tables-CBQdOETu.js.map} +1 -1
- package/dist/index.es.js +768 -537
- package/dist/index.es.js.map +1 -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/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/{src-BkdpiQdw.js → src-DlPBctw_.js} +72 -6
- package/dist/src-DlPBctw_.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 +16 -0
- package/package.json +6 -6
- 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.ts +11 -4
- package/src/collections/validate-relations.ts +15 -0
- package/src/data-transformer.ts +9 -3
- package/src/schema/generate-drizzle-schema-logic.ts +26 -1
- 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 +364 -68
- 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/services/BranchService.ts +66 -28
- package/src/services/FetchService.ts +14 -0
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +211 -45
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +69 -3
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +19 -0
- package/dist/backup-service-DLb2drIH.js.map +0 -1
- package/dist/src-BkdpiQdw.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -2,11 +2,11 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
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
|
-
import {
|
|
5
|
+
import { A as toSnakeCase, C as normalizeToEntityRelation, D as getPolicyNamesForRule, F as Vector, N as hasForeignKeyOnTarget, O as mergeDeep, P as isManyToMany, S as createRelationRefWithData, T as generateForeignKeyName, _ as buildCompositeId, a as getJunctionSecurityRules, b as parseIdValues, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, k as camelCase, 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 updateDateAutoValues, x as createRelationRef, y as isAddressableId } from "./src-DlPBctw_.js";
|
|
6
6
|
import { n as isPostgresCollectionConfig, r as isRelationalCollectionConfig } from "./src-DoU9yPqq.js";
|
|
7
7
|
import { t as ANONYMOUS_USER_ID } from "./policy-CeA1JcxP.js";
|
|
8
8
|
import { t as createPostgresWebSocket } from "./websocket-B2LsrINK.js";
|
|
9
|
-
import { A as
|
|
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";
|
|
10
10
|
import { Client, Pool } from "pg";
|
|
11
11
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
12
12
|
import { ApiError, createEmailService, loadCollectionsFromDirectory, logger } from "@rebasepro/server";
|
|
@@ -50,6 +50,39 @@ function resolveClientListLimit(rawLimit, opts = {}) {
|
|
|
50
50
|
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
51
51
|
}
|
|
52
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
|
|
53
86
|
//#region ../common/src/util/callbacks.ts
|
|
54
87
|
/**
|
|
55
88
|
* Helper function to recursively check if there are any callbacks in the properties.
|
|
@@ -195,6 +228,27 @@ function getColumnMeta(col) {
|
|
|
195
228
|
primary: typeof raw.primary === "boolean" ? raw.primary : void 0
|
|
196
229
|
};
|
|
197
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether an address could name a row in this table, judged by the columns.
|
|
233
|
+
*
|
|
234
|
+
* {@link getPrimaryKeys} lets a config's `isId: "uuid"` win over the schema, so
|
|
235
|
+
* its `isUUID` is a claim rather than a fact — right for deriving addresses,
|
|
236
|
+
* wrong for refusing a query. Here the Drizzle column type decides, because it
|
|
237
|
+
* is what Postgres will enforce: a `uuid` column meets `/c/products/new` with
|
|
238
|
+
* `22P02`, which aborts the surrounding transaction and turns every later
|
|
239
|
+
* statement into an unrelated-looking `25P02`.
|
|
240
|
+
*/
|
|
241
|
+
function idCanAddressTable(id, table, idInfoArray) {
|
|
242
|
+
return isAddressableId(id, idInfoArray.map((info) => {
|
|
243
|
+
const col = table[info.fieldName];
|
|
244
|
+
const meta = col ? getColumnMeta(col) : void 0;
|
|
245
|
+
if (!meta?.columnType) return info;
|
|
246
|
+
return {
|
|
247
|
+
...info,
|
|
248
|
+
isUUID: meta.columnType === "PgUUID"
|
|
249
|
+
};
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
198
252
|
function getCollectionByPath(collectionPath, registry) {
|
|
199
253
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
200
254
|
if (!collection) {
|
|
@@ -282,6 +336,35 @@ function requirePrimaryKeys(collection, registry) {
|
|
|
282
336
|
return keys;
|
|
283
337
|
}
|
|
284
338
|
/**
|
|
339
|
+
* The column on the *source* table that a `hasOne`/`hasMany` link points at.
|
|
340
|
+
*
|
|
341
|
+
* `sourceKey` is authored when the two sides join on a natural key — an
|
|
342
|
+
* external identity id, a SKU — and left off when they join on the row id,
|
|
343
|
+
* which is the overwhelming majority. That makes `undefined` the only optional
|
|
344
|
+
* field on a resolved relation, so it gets exactly one reader: this function.
|
|
345
|
+
* Every consumer that needs the column asks here, and none of them re-derives
|
|
346
|
+
* "or else the primary key" for itself. That is the whole point — the fallback
|
|
347
|
+
* chains this codebase removed from relation resolution were dangerous because
|
|
348
|
+
* they were *duplicated* and could disagree, not because they existed.
|
|
349
|
+
*/
|
|
350
|
+
function sourceKeyField(relation, sourceCollection, registry) {
|
|
351
|
+
if (relation.sourceKey) return relation.sourceKey;
|
|
352
|
+
return requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Whether this link joins on something other than the source's primary key.
|
|
356
|
+
*
|
|
357
|
+
* Callers that hold a parent *id* — which is most of them, since an id is what
|
|
358
|
+
* a URL carries — must translate it to the source key's value before it can be
|
|
359
|
+
* compared with the target's foreign key. Those that hold the parent *row*, or
|
|
360
|
+
* that build a correlated subquery over the source table, can read the column
|
|
361
|
+
* directly and skip the lookup.
|
|
362
|
+
*/
|
|
363
|
+
function joinsOnNaturalKey(relation, sourceCollection, registry) {
|
|
364
|
+
if (!relation.sourceKey) return false;
|
|
365
|
+
return relation.sourceKey !== requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
285
368
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
286
369
|
* instead.
|
|
287
370
|
*
|
|
@@ -452,7 +535,9 @@ var DrizzleConditionBuilder = class {
|
|
|
452
535
|
case "hasMany": {
|
|
453
536
|
const fkColumn = targetTable[relation.foreignKeyOnTarget];
|
|
454
537
|
if (!fkColumn) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'.`);
|
|
455
|
-
return eq(fkColumn, parentId);
|
|
538
|
+
if (!relation.sourceKey) return eq(fkColumn, parentId);
|
|
539
|
+
const { table, idColumn } = parent();
|
|
540
|
+
return sql`${fkColumn} = (SELECT ${sql.identifier(relation.sourceKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
456
541
|
}
|
|
457
542
|
case "belongsTo": {
|
|
458
543
|
const { table, idColumn } = parent();
|
|
@@ -542,12 +627,16 @@ var DrizzleConditionBuilder = class {
|
|
|
542
627
|
column: foreignKey
|
|
543
628
|
};
|
|
544
629
|
}
|
|
545
|
-
if (relation && (hasForeignKeyOnTarget(relation) || isManyToMany(relation)) && registry && sourceIdColumn)
|
|
546
|
-
|
|
547
|
-
relation,
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
630
|
+
if (relation && (hasForeignKeyOnTarget(relation) || isManyToMany(relation)) && registry && sourceIdColumn) {
|
|
631
|
+
const correlationColumn = hasForeignKeyOnTarget(relation) && relation.sourceKey ? columnAt(relation.sourceKey) : sourceIdColumn;
|
|
632
|
+
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.`);
|
|
633
|
+
return {
|
|
634
|
+
kind: "relation",
|
|
635
|
+
relation,
|
|
636
|
+
registry,
|
|
637
|
+
sourceIdColumn: correlationColumn
|
|
638
|
+
};
|
|
639
|
+
}
|
|
551
640
|
}
|
|
552
641
|
for (const guess of [`${field}_id`, generateForeignKeyName(field)]) {
|
|
553
642
|
const foreignKey = columnAt(guess);
|
|
@@ -1346,8 +1435,9 @@ async function parseDataFromServer(data, collection, db, registry) {
|
|
|
1346
1435
|
} else if (hasForeignKeyOnTarget(relation) && db && registry) try {
|
|
1347
1436
|
const targetCollection = relation.target();
|
|
1348
1437
|
const targetTable = registry.getTable(getTableName$1(targetCollection));
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1438
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
1439
|
+
const currentId = relation.sourceKey ? data[relation.sourceKey] : buildCompositeId(data, pks);
|
|
1440
|
+
if (targetTable && currentId !== void 0 && currentId !== null && currentId !== "") {
|
|
1351
1441
|
const foreignKeyColumn = targetTable[relation.foreignKeyOnTarget];
|
|
1352
1442
|
if (foreignKeyColumn) {
|
|
1353
1443
|
const relatedRows = await db.select().from(targetTable).where(eq(foreignKeyColumn, currentId)).limit(relation.cardinality === "one" ? 1 : 100);
|
|
@@ -1719,8 +1809,67 @@ var RelationService = class {
|
|
|
1719
1809
|
* reading. Left alone it would silently match on the first key column and
|
|
1720
1810
|
* hand a tenant's rows to its neighbour — say so instead.
|
|
1721
1811
|
*/
|
|
1722
|
-
assertSingleKeyAddressable(parentCollection, parentPks, via) {
|
|
1723
|
-
if (
|
|
1812
|
+
assertSingleKeyAddressable(parentCollection, parentPks, via, relation) {
|
|
1813
|
+
if (relation && hasForeignKeyOnTarget(relation) && relation.sourceKey) return;
|
|
1814
|
+
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.`);
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* What the target's foreign key holds, for each of these parent rows.
|
|
1818
|
+
*
|
|
1819
|
+
* Ordinarily the parent's id, and then this is free. When the relation
|
|
1820
|
+
* declares a `sourceKey` the two are different values, and the mapping
|
|
1821
|
+
* between them lives in the source table — so it costs one SELECT, issued
|
|
1822
|
+
* once for the whole batch rather than per parent.
|
|
1823
|
+
*
|
|
1824
|
+
* Both directions come back because both are needed and deriving one from
|
|
1825
|
+
* the other by hand is how a batch loader ends up attributing a child to the
|
|
1826
|
+
* wrong parent: reads translate id → key to build the WHERE, and then
|
|
1827
|
+
* translate key → id to attribute each row that comes back.
|
|
1828
|
+
*/
|
|
1829
|
+
/**
|
|
1830
|
+
* The value a related row's foreign key must hold to belong to this parent.
|
|
1831
|
+
*
|
|
1832
|
+
* `undefined` when the parent's source key is null — which is not an error
|
|
1833
|
+
* here, only in the callers that were about to write it. Exposed for
|
|
1834
|
+
* {@link PersistService}, which stamps this onto a child created under a
|
|
1835
|
+
* nested path and would otherwise write the id and lose the row.
|
|
1836
|
+
*/
|
|
1837
|
+
async parentKeyValue(parentCollection, relation, parentId, db = this.db) {
|
|
1838
|
+
const { keyByParentId } = await this.resolveSourceKeys(parentCollection, relation, [parentId], db);
|
|
1839
|
+
return keyByParentId.get(String(parentId));
|
|
1840
|
+
}
|
|
1841
|
+
async resolveSourceKeys(parentCollection, relation, parentIds, db = this.db) {
|
|
1842
|
+
const keyByParentId = /* @__PURE__ */ new Map();
|
|
1843
|
+
const parentIdByKey = /* @__PURE__ */ new Map();
|
|
1844
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
1845
|
+
if (!joinsOnNaturalKey(relation, parentCollection, this.registry)) {
|
|
1846
|
+
for (const id of parentIds) {
|
|
1847
|
+
const value = parseIdValues(id, parentPks)[parentPks[0].fieldName];
|
|
1848
|
+
keyByParentId.set(String(id), value);
|
|
1849
|
+
parentIdByKey.set(String(value), id);
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
keyByParentId,
|
|
1853
|
+
parentIdByKey
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const field = sourceKeyField(relation, parentCollection, this.registry);
|
|
1857
|
+
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
1858
|
+
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.`);
|
|
1859
|
+
const rows = await db.select().from(parentTable).where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
1860
|
+
for (const row of rows) {
|
|
1861
|
+
const keyValue = row[field];
|
|
1862
|
+
if (keyValue === null || keyValue === void 0) continue;
|
|
1863
|
+
const parentId = buildCompositeId(row, parentPks);
|
|
1864
|
+
keyByParentId.set(String(parentId), keyValue);
|
|
1865
|
+
const existing = parentIdByKey.get(String(keyValue));
|
|
1866
|
+
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.`);
|
|
1867
|
+
parentIdByKey.set(String(keyValue), parentId);
|
|
1868
|
+
}
|
|
1869
|
+
return {
|
|
1870
|
+
keyByParentId,
|
|
1871
|
+
parentIdByKey
|
|
1872
|
+
};
|
|
1724
1873
|
}
|
|
1725
1874
|
/**
|
|
1726
1875
|
* Fetch rows related to a parent row through a specific relation
|
|
@@ -1779,6 +1928,8 @@ var RelationService = class {
|
|
|
1779
1928
|
}
|
|
1780
1929
|
return rows;
|
|
1781
1930
|
}
|
|
1931
|
+
const matchValue = hasForeignKeyOnTarget(relation) ? (await this.resolveSourceKeys(parentCollection, relation, [parentId])).keyByParentId.get(String(parentId)) : parsedParentId;
|
|
1932
|
+
if (matchValue === void 0) return [];
|
|
1782
1933
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1783
1934
|
const additionalFilters = [];
|
|
1784
1935
|
if (options.searchString) {
|
|
@@ -1787,7 +1938,7 @@ var RelationService = class {
|
|
|
1787
1938
|
const searchCombined = DrizzleConditionBuilder.combineConditionsWithOr(searchConditions);
|
|
1788
1939
|
if (searchCombined) additionalFilters.push(searchCombined);
|
|
1789
1940
|
}
|
|
1790
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
1941
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValue, targetTable, parentTable, parentIdCol, idField, this.registry, additionalFilters);
|
|
1791
1942
|
if (options.limit) query = query.limit(options.limit);
|
|
1792
1943
|
const results = await query;
|
|
1793
1944
|
if (!results || !Array.isArray(results)) return [];
|
|
@@ -1830,8 +1981,10 @@ var RelationService = class {
|
|
|
1830
1981
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
1831
1982
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1832
1983
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1984
|
+
const matchValue = hasForeignKeyOnTarget(relation) ? (await this.resolveSourceKeys(parentCollection, relation, [parentId])).keyByParentId.get(String(parentId)) : parsedParentId;
|
|
1985
|
+
if (matchValue === void 0) return 0;
|
|
1833
1986
|
let query = this.db.select({ count: sql`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
|
|
1834
|
-
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation,
|
|
1987
|
+
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation, matchValue, targetTable, parentTable, parentIdCol, targetIdField, this.registry, additionalFilters);
|
|
1835
1988
|
const result = await query;
|
|
1836
1989
|
return Number(result[0]?.count || 0);
|
|
1837
1990
|
}
|
|
@@ -1958,17 +2111,24 @@ var RelationService = class {
|
|
|
1958
2111
|
}
|
|
1959
2112
|
return resultMap;
|
|
1960
2113
|
}
|
|
1961
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
2114
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName, relation);
|
|
2115
|
+
const { keyByParentId, parentIdByKey } = hasForeignKeyOnTarget(relation) ? await this.resolveSourceKeys(parentCollection, relation, parentIds) : {
|
|
2116
|
+
keyByParentId: /* @__PURE__ */ new Map(),
|
|
2117
|
+
parentIdByKey: /* @__PURE__ */ new Map()
|
|
2118
|
+
};
|
|
2119
|
+
const matchValues = hasForeignKeyOnTarget(relation) ? [...keyByParentId.values()] : parsedParentIds;
|
|
2120
|
+
if (matchValues.length === 0) return /* @__PURE__ */ new Map();
|
|
1962
2121
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
1963
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
2122
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValues, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
1964
2123
|
const results = await query;
|
|
1965
2124
|
const resultMap = /* @__PURE__ */ new Map();
|
|
1966
|
-
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
1967
2125
|
for (const row of results) {
|
|
1968
2126
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (
|
|
2127
|
+
if (!hasForeignKeyOnTarget(relation)) continue;
|
|
2128
|
+
const foreignKeyValue = targetRow[relation.foreignKeyOnTarget];
|
|
2129
|
+
if (foreignKeyValue === void 0 || foreignKeyValue === null) continue;
|
|
2130
|
+
const parentId = parentIdByKey.get(String(foreignKeyValue));
|
|
2131
|
+
if (parentId !== void 0) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
1972
2132
|
}
|
|
1973
2133
|
return resultMap;
|
|
1974
2134
|
}
|
|
@@ -2046,17 +2206,24 @@ var RelationService = class {
|
|
|
2046
2206
|
}
|
|
2047
2207
|
return resultMap;
|
|
2048
2208
|
}
|
|
2049
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName);
|
|
2209
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, hasForeignKeyOnTarget(relation) ? relation.foreignKeyOnTarget : relation.relationName, relation);
|
|
2210
|
+
const { keyByParentId, parentIdByKey } = hasForeignKeyOnTarget(relation) ? await this.resolveSourceKeys(parentCollection, relation, parentIds) : {
|
|
2211
|
+
keyByParentId: /* @__PURE__ */ new Map(),
|
|
2212
|
+
parentIdByKey: /* @__PURE__ */ new Map()
|
|
2213
|
+
};
|
|
2214
|
+
const matchValues = hasForeignKeyOnTarget(relation) ? [...keyByParentId.values()] : parsedParentIds;
|
|
2215
|
+
if (matchValues.length === 0) return /* @__PURE__ */ new Map();
|
|
2050
2216
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
2051
|
-
query = applyDynamicRelationQuery(query, query, relation,
|
|
2217
|
+
query = applyDynamicRelationQuery(query, query, relation, matchValues, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
2052
2218
|
const results = await query;
|
|
2053
2219
|
const resultMap = /* @__PURE__ */ new Map();
|
|
2054
|
-
const parentIdSet = new Set(parsedParentIds.map(String));
|
|
2055
2220
|
for (const row of results) {
|
|
2056
2221
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
if (
|
|
2222
|
+
if (!hasForeignKeyOnTarget(relation)) continue;
|
|
2223
|
+
const foreignKeyValue = targetRow[relation.foreignKeyOnTarget];
|
|
2224
|
+
if (foreignKeyValue === void 0 || foreignKeyValue === null) continue;
|
|
2225
|
+
const parentId = parentIdByKey.get(String(foreignKeyValue));
|
|
2226
|
+
if (parentId !== void 0) {
|
|
2060
2227
|
const key = String(parentId);
|
|
2061
2228
|
const arr = resultMap.get(key) || [];
|
|
2062
2229
|
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
@@ -2155,14 +2322,13 @@ var RelationService = class {
|
|
|
2155
2322
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
2156
2323
|
continue;
|
|
2157
2324
|
}
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
2325
|
+
const parentKeyValue = (await this.resolveSourceKeys(collection, relation, [id], tx)).keyByParentId.get(String(id));
|
|
2326
|
+
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.`);
|
|
2161
2327
|
if (targetEntityIds.length > 0) {
|
|
2162
2328
|
const parsedTargetIds = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]);
|
|
2163
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(and(eq(fkCol,
|
|
2164
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]:
|
|
2165
|
-
} else await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(fkCol,
|
|
2329
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(and(eq(fkCol, parentKeyValue), sql`${targetIdCol} NOT IN (${sql.join(parsedTargetIds)})`));
|
|
2330
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: parentKeyValue }).where(inArray(targetIdCol, parsedTargetIds));
|
|
2331
|
+
} else await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(fkCol, parentKeyValue));
|
|
2166
2332
|
} else logger.warn(`Many relation '${key}' in collection '${collection.slug}' lacks write configuration and will be skipped during save.`);
|
|
2167
2333
|
}
|
|
2168
2334
|
}
|
|
@@ -2177,8 +2343,7 @@ var RelationService = class {
|
|
|
2177
2343
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
2178
2344
|
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
2179
2345
|
const targetIdInfo = targetPks[0];
|
|
2180
|
-
|
|
2181
|
-
const sourceIdInfo = sourcePks[0];
|
|
2346
|
+
requirePrimaryKeys(sourceCollection, this.registry)[0];
|
|
2182
2347
|
if (relation.kind === "via") {
|
|
2183
2348
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
2184
2349
|
continue;
|
|
@@ -2200,13 +2365,14 @@ var RelationService = class {
|
|
|
2200
2365
|
logger.warn(`Foreign key column '${relation.foreignKeyOnTarget}' not found in target table for relation '${relation.relationName}'`);
|
|
2201
2366
|
continue;
|
|
2202
2367
|
}
|
|
2203
|
-
const
|
|
2204
|
-
if (
|
|
2368
|
+
const sourceKeyValue = (await this.resolveSourceKeys(sourceCollection, relation, [sourceEntityId], tx)).keyByParentId.get(String(sourceEntityId));
|
|
2369
|
+
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.`);
|
|
2370
|
+
if (newValue === null || newValue === void 0) await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn, sourceKeyValue));
|
|
2205
2371
|
else {
|
|
2206
2372
|
const parsedNewTargetId = parseIdValues(newValue, targetPks)[targetIdInfo.fieldName];
|
|
2207
2373
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
2208
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn,
|
|
2209
|
-
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]:
|
|
2374
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: null }).where(eq(foreignKeyColumn, sourceKeyValue));
|
|
2375
|
+
await tx.update(targetTable).set({ [relation.foreignKeyOnTarget]: sourceKeyValue }).where(eq(targetIdField, parsedNewTargetId));
|
|
2210
2376
|
}
|
|
2211
2377
|
} catch (e) {
|
|
2212
2378
|
logger.warn(`Failed to update inverse relation '${relation.relationName}'`, { error: e });
|
|
@@ -2627,91 +2793,332 @@ function assertWritableThrough(hop, path) {
|
|
|
2627
2793
|
throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
|
|
2628
2794
|
}
|
|
2629
2795
|
//#endregion
|
|
2630
|
-
//#region src/
|
|
2796
|
+
//#region src/utils/pg-error-utils.ts
|
|
2631
2797
|
/**
|
|
2632
|
-
*
|
|
2633
|
-
*
|
|
2798
|
+
* Shared PostgreSQL error extraction and user-friendly message formatting.
|
|
2799
|
+
*
|
|
2800
|
+
* Drizzle wraps native PG errors in a `.cause` chain. These utilities
|
|
2801
|
+
* unwrap that chain to get the real PostgreSQL error (identified by a
|
|
2802
|
+
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
2803
|
+
* a message that is safe and helpful to show to end-users.
|
|
2634
2804
|
*/
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2805
|
+
/**
|
|
2806
|
+
* Return the error when it is a deliberate 4xx, otherwise null.
|
|
2807
|
+
*
|
|
2808
|
+
* A thrown `ApiError` is a decision the server made about the request, not a
|
|
2809
|
+
* database failure: its message and code are already written for the client.
|
|
2810
|
+
*/
|
|
2811
|
+
function asClientFacingError(error) {
|
|
2812
|
+
if (!(error instanceof Error)) return null;
|
|
2813
|
+
const e = error;
|
|
2814
|
+
if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
|
|
2815
|
+
return e;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
2819
|
+
* Drizzle wraps PG errors in a `cause` property — this function
|
|
2820
|
+
* recursively walks the chain until it finds an object with a PG
|
|
2821
|
+
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
2822
|
+
*/
|
|
2823
|
+
function extractPgError(error) {
|
|
2824
|
+
if (!error || typeof error !== "object") return null;
|
|
2825
|
+
if (!(error instanceof Error)) {
|
|
2826
|
+
if ("cause" in error && error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
2827
|
+
return null;
|
|
2643
2828
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2829
|
+
if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) return error;
|
|
2830
|
+
if (error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Whether the failure came back from Postgres rather than from building the
|
|
2835
|
+
* query — which decides whether a fallback query is worth issuing.
|
|
2836
|
+
*
|
|
2837
|
+
* Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
|
|
2838
|
+
* RLS). Once a statement raises, that transaction is aborted, and every later
|
|
2839
|
+
* statement on it returns `25P02` — "current transaction is aborted, commands
|
|
2840
|
+
* ignored until end of transaction block". So a retry after a database error
|
|
2841
|
+
* cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
|
|
2842
|
+
* for type uuid") with a generic one. Rethrow instead.
|
|
2843
|
+
*
|
|
2844
|
+
* A query the driver could not even build — a missing reciprocal relation, say
|
|
2845
|
+
* — never reached Postgres, leaves the transaction usable, and is exactly what
|
|
2846
|
+
* the fallback paths exist for.
|
|
2847
|
+
*/
|
|
2848
|
+
function reachedDatabase(error) {
|
|
2849
|
+
return extractPgError(error) !== null;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Walk the error cause chain and return the deepest meaningful message.
|
|
2853
|
+
*/
|
|
2854
|
+
function extractCauseMessage(error) {
|
|
2855
|
+
if (!error || typeof error !== "object") return null;
|
|
2856
|
+
if (!(error instanceof Error)) return null;
|
|
2857
|
+
if (error.cause && typeof error.cause === "object") {
|
|
2858
|
+
const deeper = extractCauseMessage(error.cause);
|
|
2859
|
+
if (deeper) return deeper;
|
|
2860
|
+
if (error.cause instanceof Error && error.cause.message) return error.cause.message;
|
|
2650
2861
|
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2862
|
+
return null;
|
|
2863
|
+
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Codes that mean "this connection will never work as configured".
|
|
2866
|
+
*
|
|
2867
|
+
* A wrong password or a database that does not exist is a settled fact about
|
|
2868
|
+
* the connection string, not a transient fault — retrying produces the same
|
|
2869
|
+
* answer forever.
|
|
2870
|
+
*/
|
|
2871
|
+
var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
2872
|
+
"28P01",
|
|
2873
|
+
"28000",
|
|
2874
|
+
"3D000",
|
|
2875
|
+
"42501"
|
|
2876
|
+
]);
|
|
2877
|
+
/**
|
|
2878
|
+
* Describe a failed connection attempt in terms a developer can act on.
|
|
2879
|
+
*
|
|
2880
|
+
* The error a caller catches is Drizzle's wrapper: its message is
|
|
2881
|
+
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
2882
|
+
* the sentence that says what is actually wrong — "password authentication
|
|
2883
|
+
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
2884
|
+
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
2885
|
+
* their `DATABASE_URL` nothing at all.
|
|
2886
|
+
*/
|
|
2887
|
+
function classifyConnectFailure(error) {
|
|
2888
|
+
const pgError = extractPgError(error);
|
|
2889
|
+
const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
|
|
2890
|
+
return {
|
|
2891
|
+
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
2892
|
+
reason,
|
|
2893
|
+
code: pgError?.code
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Detect whether an error is specifically a role-switching permission failure
|
|
2898
|
+
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
2899
|
+
* as opposed to a table-level permission denial.
|
|
2900
|
+
*
|
|
2901
|
+
* This is used by the backend driver to auto-disable role switching when the
|
|
2902
|
+
* connection user lacks SET ROLE privileges, rather than surfacing a confusing
|
|
2903
|
+
* error to the Studio SQL Editor user.
|
|
2904
|
+
*/
|
|
2905
|
+
function isRoleSwitchingPermissionError(error) {
|
|
2906
|
+
const pgError = extractPgError(error);
|
|
2907
|
+
if (!pgError || pgError.code !== "42501") return false;
|
|
2908
|
+
const msg = pgError.message.toLowerCase();
|
|
2909
|
+
return msg.includes("set role") || msg.includes("member of role");
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
2913
|
+
*
|
|
2914
|
+
* @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
|
|
2915
|
+
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
2916
|
+
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
2917
|
+
*/
|
|
2918
|
+
function pgErrorToFriendlyMessage(pgError, context) {
|
|
2919
|
+
const detail = pgError.detail;
|
|
2920
|
+
const hint = pgError.hint;
|
|
2921
|
+
const constraint = pgError.constraint;
|
|
2922
|
+
const column = pgError.column;
|
|
2923
|
+
const table = pgError.table;
|
|
2924
|
+
const dataType = pgError.dataType;
|
|
2925
|
+
const pgMessage = pgError.message || "Unknown database error";
|
|
2926
|
+
const code = pgError.code || "UNKNOWN";
|
|
2927
|
+
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
2928
|
+
const tableRef = table ?? context;
|
|
2929
|
+
switch (pgError.code) {
|
|
2930
|
+
case "23503": return {
|
|
2931
|
+
message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
2932
|
+
code
|
|
2673
2933
|
};
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
return
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2934
|
+
case "23505": return {
|
|
2935
|
+
message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
2936
|
+
code
|
|
2937
|
+
};
|
|
2938
|
+
case "23502": return {
|
|
2939
|
+
message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
|
|
2940
|
+
code
|
|
2941
|
+
};
|
|
2942
|
+
case "23514": return {
|
|
2943
|
+
message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
2944
|
+
code
|
|
2945
|
+
};
|
|
2946
|
+
case "22P02": return {
|
|
2947
|
+
message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
|
|
2948
|
+
code
|
|
2949
|
+
};
|
|
2950
|
+
case "22001": return {
|
|
2951
|
+
message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
2952
|
+
code
|
|
2953
|
+
};
|
|
2954
|
+
case "22003": return {
|
|
2955
|
+
message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
2956
|
+
code
|
|
2957
|
+
};
|
|
2958
|
+
case "42703": return {
|
|
2959
|
+
message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
2960
|
+
code
|
|
2961
|
+
};
|
|
2962
|
+
case "42P01": return {
|
|
2963
|
+
message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
2964
|
+
code
|
|
2965
|
+
};
|
|
2966
|
+
case "42501": return {
|
|
2967
|
+
message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
|
|
2968
|
+
code
|
|
2969
|
+
};
|
|
2970
|
+
case "28000": return {
|
|
2971
|
+
message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
|
|
2972
|
+
code
|
|
2973
|
+
};
|
|
2974
|
+
default: {
|
|
2975
|
+
const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
|
|
2976
|
+
if (detail) parts.push(`Detail: ${detail}`);
|
|
2977
|
+
if (column) parts.push(`Column: ${column}`);
|
|
2978
|
+
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
2979
|
+
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
2980
|
+
if (hint) parts.push(`Hint: ${hint}`);
|
|
2981
|
+
return {
|
|
2982
|
+
message: parts.join(". "),
|
|
2983
|
+
code
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* Sanitize any error into a message safe and helpful for the client.
|
|
2990
|
+
*
|
|
2991
|
+
* A deliberate 4xx (`ApiError`) passes through untouched — the server already
|
|
2992
|
+
* decided what the client should read. Otherwise the PG error is extracted
|
|
2993
|
+
* from the Drizzle cause chain, falling back to a generic message that
|
|
2994
|
+
* doesn't leak SQL.
|
|
2995
|
+
*
|
|
2996
|
+
* @param error - The raw caught error
|
|
2997
|
+
* @param context - A human-readable context string (e.g. collection path)
|
|
2998
|
+
* @returns An object with `message` (user-friendly) and optional `code`
|
|
2999
|
+
* (the `ApiError` code, or the PG SQLSTATE).
|
|
3000
|
+
*/
|
|
3001
|
+
function sanitizeErrorForClient(error, context) {
|
|
3002
|
+
const clientError = asClientFacingError(error);
|
|
3003
|
+
if (clientError) {
|
|
3004
|
+
const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
|
|
3005
|
+
if (clientError.expected) logger.debug(line);
|
|
3006
|
+
else logger.warn(`⚠️ ${line}`);
|
|
3007
|
+
return {
|
|
3008
|
+
message: clientError.message,
|
|
3009
|
+
...clientError.code && { code: clientError.code }
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
const pgError = extractPgError(error);
|
|
3013
|
+
if (pgError) {
|
|
3014
|
+
logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
|
|
3015
|
+
code: pgError.code,
|
|
3016
|
+
message: pgError.message,
|
|
3017
|
+
detail: pgError.detail,
|
|
3018
|
+
hint: pgError.hint,
|
|
3019
|
+
column: pgError.column,
|
|
3020
|
+
table: pgError.table,
|
|
3021
|
+
constraint: pgError.constraint,
|
|
3022
|
+
dataType: pgError.dataType,
|
|
3023
|
+
drizzleMessage: error instanceof Error ? error.message : String(error)
|
|
3024
|
+
});
|
|
3025
|
+
return pgErrorToFriendlyMessage(pgError, context);
|
|
3026
|
+
}
|
|
3027
|
+
logger.error(`Database error in "${context}" (no PG error extracted)`, {
|
|
3028
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3029
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
3030
|
+
cause: error instanceof Error && error.cause ? error.cause instanceof Error ? error.cause.message : String(error.cause) : void 0
|
|
3031
|
+
});
|
|
3032
|
+
const causeMessage = extractCauseMessage(error);
|
|
3033
|
+
if (causeMessage) return { message: `Database error in "${context}": ${causeMessage}` };
|
|
3034
|
+
return { message: `Could not load data for "${context}". Check server logs for details.` };
|
|
3035
|
+
}
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/services/FetchService.ts
|
|
3038
|
+
/**
|
|
3039
|
+
* Service for handling all row read operations.
|
|
3040
|
+
* Handles fetching, searching, counting, and filtering rows.
|
|
3041
|
+
*/
|
|
3042
|
+
var FetchService = class {
|
|
3043
|
+
db;
|
|
3044
|
+
registry;
|
|
3045
|
+
relationService;
|
|
3046
|
+
constructor(db, registry) {
|
|
3047
|
+
this.db = db;
|
|
3048
|
+
this.registry = registry;
|
|
3049
|
+
this.relationService = new RelationService(db, registry);
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Get the relational query builder for a given table name.
|
|
3053
|
+
* Safely narrows the DrizzleClient union type to access db.query[tableName].
|
|
3054
|
+
*/
|
|
3055
|
+
getQueryBuilder(tableName) {
|
|
3056
|
+
return this.db.query?.[tableName];
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* The context the condition builder needs to compile a filter key that is
|
|
3060
|
+
* not a column name outright.
|
|
3061
|
+
*
|
|
3062
|
+
* Two such keys. An owning relation's key resolves through the collection's
|
|
3063
|
+
* relations to its foreign-key column; a relation whose link lives on the
|
|
3064
|
+
* target table or in a junction resolves to a correlated `EXISTS`, which
|
|
3065
|
+
* needs the registry to reach that other table and this table's key column
|
|
3066
|
+
* to correlate back.
|
|
3067
|
+
*
|
|
3068
|
+
* Looked up rather than passed: every read path already has the path, only
|
|
3069
|
+
* some have the collection, and a path that names no registered collection
|
|
3070
|
+
* (a nested/derived one) is not an error here — the builder simply falls
|
|
3071
|
+
* back to guessing the default key shapes, and a relation filter it cannot
|
|
3072
|
+
* compile stays unresolvable and so fails closed.
|
|
3073
|
+
*/
|
|
3074
|
+
filterContext(collectionPath, table) {
|
|
3075
|
+
const collection = this.registry.getCollectionByPath(collectionPath) ?? void 0;
|
|
3076
|
+
return {
|
|
3077
|
+
collection,
|
|
3078
|
+
registry: this.registry,
|
|
3079
|
+
sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : void 0
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* The table column this collection's rows are keyed by, or `undefined`.
|
|
3084
|
+
*
|
|
3085
|
+
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
3086
|
+
* resolvable key is not an error on the filter path — it only means the
|
|
3087
|
+
* relation filters that would correlate on it cannot be compiled, which
|
|
3088
|
+
* the builder already handles by failing that field closed.
|
|
3089
|
+
*/
|
|
3090
|
+
resolveIdColumn(collection, table) {
|
|
3091
|
+
const [idInfo] = getPrimaryKeys(collection, this.registry);
|
|
3092
|
+
if (!idInfo) return void 0;
|
|
3093
|
+
return table[idInfo.fieldName];
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Build filter conditions from FilterValues
|
|
3097
|
+
* Delegates to DrizzleConditionBuilder.buildFilterConditions
|
|
3098
|
+
*/
|
|
3099
|
+
buildFilterConditions(filter, table, collectionPath) {
|
|
3100
|
+
return DrizzleConditionBuilder.buildFilterConditions(filter, table, collectionPath, this.filterContext(collectionPath, table));
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Resolves the correct Drizzle column for sorting.
|
|
3104
|
+
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
3105
|
+
*
|
|
3106
|
+
* The relation's own `localKey` is the authority for that foreign key, not
|
|
3107
|
+
* `<field>_id`. The default local key comes from `generateForeignKeyName`,
|
|
3108
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
3109
|
+
* `users` → `user_id` — and an author can override it outright. A wrong
|
|
3110
|
+
* guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
|
|
3111
|
+
* come back in whatever order Postgres pleases: paging over that repeats
|
|
3112
|
+
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
3113
|
+
* caller that hands over no collection to resolve against.
|
|
3114
|
+
*/
|
|
3115
|
+
resolveOrderByField(table, orderBy, collection) {
|
|
3116
|
+
const columnAt = (key) => (key in table ? table[key] : void 0) || void 0;
|
|
3117
|
+
const direct = columnAt(orderBy);
|
|
3118
|
+
if (direct) return direct;
|
|
3119
|
+
const declaredRelation = collection ? resolveCollectionRelations(collection)[orderBy] : void 0;
|
|
3120
|
+
if (declaredRelation?.kind === "belongsTo") {
|
|
3121
|
+
const foreignKey = columnAt(declaredRelation.localKey);
|
|
2715
3122
|
if (foreignKey) return foreignKey;
|
|
2716
3123
|
}
|
|
2717
3124
|
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
@@ -2941,6 +3348,7 @@ var FetchService = class {
|
|
|
2941
3348
|
const idInfo = idInfoArray[0];
|
|
2942
3349
|
const idField = table[idInfo.fieldName];
|
|
2943
3350
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
3351
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return void 0;
|
|
2944
3352
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
2945
3353
|
const tableName = getTableName(table);
|
|
2946
3354
|
const qb = this.getQueryBuilder(tableName);
|
|
@@ -2959,6 +3367,7 @@ var FetchService = class {
|
|
|
2959
3367
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
2960
3368
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
2961
3369
|
}
|
|
3370
|
+
if (reachedDatabase(e)) throw e;
|
|
2962
3371
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
2963
3372
|
}
|
|
2964
3373
|
const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
|
|
@@ -3012,6 +3421,7 @@ var FetchService = class {
|
|
|
3012
3421
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3013
3422
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
3014
3423
|
}
|
|
3424
|
+
if (reachedDatabase(e)) throw e;
|
|
3015
3425
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
3016
3426
|
}
|
|
3017
3427
|
let vectorMeta;
|
|
@@ -3231,6 +3641,7 @@ var FetchService = class {
|
|
|
3231
3641
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3232
3642
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
3233
3643
|
}
|
|
3644
|
+
if (reachedDatabase(e)) throw e;
|
|
3234
3645
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
3235
3646
|
}
|
|
3236
3647
|
const rows = await this.fetchRowsWithConditionsRaw(collectionPath, options);
|
|
@@ -3276,6 +3687,7 @@ var FetchService = class {
|
|
|
3276
3687
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
3277
3688
|
const idInfo = idInfoArray[0];
|
|
3278
3689
|
const idField = table[idInfo.fieldName];
|
|
3690
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return null;
|
|
3279
3691
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
3280
3692
|
const tableName = getTableName(table);
|
|
3281
3693
|
const qb = this.getQueryBuilder(tableName);
|
|
@@ -3294,6 +3706,7 @@ var FetchService = class {
|
|
|
3294
3706
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3295
3707
|
logger.error("Hint: This usually means a relation in your drizzle schema is missing a reciprocal 'one()' or 'many()' definition. Run 'rebase schema generate' to fix this.");
|
|
3296
3708
|
}
|
|
3709
|
+
if (reachedDatabase(e)) throw e;
|
|
3297
3710
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
3298
3711
|
}
|
|
3299
3712
|
const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
|
|
@@ -3323,357 +3736,134 @@ var FetchService = class {
|
|
|
3323
3736
|
logger.warn(`[include] Failed to load relation '${key}'`, { error: e });
|
|
3324
3737
|
}
|
|
3325
3738
|
}
|
|
3326
|
-
return flatEntity;
|
|
3327
|
-
}
|
|
3328
|
-
/**
|
|
3329
|
-
* Fetch raw rows without any relation processing (for REST fast path)
|
|
3330
|
-
*/
|
|
3331
|
-
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
3332
|
-
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
3333
|
-
const table = getTableForCollection(collection, this.registry);
|
|
3334
|
-
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
3335
|
-
let vectorMeta;
|
|
3336
|
-
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
3337
|
-
let query = vectorMeta ? this.db.select({
|
|
3338
|
-
table_row: table,
|
|
3339
|
-
_distance: vectorMeta.distanceSelect
|
|
3340
|
-
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
3341
|
-
const allConditions = [];
|
|
3342
|
-
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
3343
|
-
if (options.searchString) {
|
|
3344
|
-
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
3345
|
-
if (searchConditions.length === 0) return [];
|
|
3346
|
-
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
3347
|
-
}
|
|
3348
|
-
if (options.filter) {
|
|
3349
|
-
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3350
|
-
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
3351
|
-
}
|
|
3352
|
-
if (vectorMeta?.filter) allConditions.push(vectorMeta.filter);
|
|
3353
|
-
if (allConditions.length > 0) {
|
|
3354
|
-
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
3355
|
-
if (finalCondition) query = query.where(finalCondition);
|
|
3356
|
-
}
|
|
3357
|
-
const orderExpressions = [];
|
|
3358
|
-
if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
|
|
3359
|
-
else if (options.orderBy) {
|
|
3360
|
-
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3361
|
-
if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
3362
|
-
}
|
|
3363
|
-
orderExpressions.push(desc(idField));
|
|
3364
|
-
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
3365
|
-
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
3366
|
-
if (limitValue) query = query.limit(limitValue);
|
|
3367
|
-
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
3368
|
-
const rawResults = await query;
|
|
3369
|
-
if (vectorMeta) return rawResults.map((r) => ({
|
|
3370
|
-
...r.table_row,
|
|
3371
|
-
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
3372
|
-
}));
|
|
3373
|
-
return rawResults;
|
|
3374
|
-
}
|
|
3375
|
-
/**
|
|
3376
|
-
* Check if the Drizzle instance has the relational query API available
|
|
3377
|
-
* for a given collection path.
|
|
3378
|
-
* Note: Primary path now uses inline `getQueryBuilder()` checks.
|
|
3379
|
-
*/
|
|
3380
|
-
hasDrizzleQueryAPI(collectionPath) {
|
|
3381
|
-
if (!this.getQueryBuilder("__probe__")) return false;
|
|
3382
|
-
const tableName = getTableName(getTableForCollection(getCollectionByPath(collectionPath, this.registry), this.registry));
|
|
3383
|
-
return !!this.getQueryBuilder(tableName);
|
|
3384
|
-
}
|
|
3385
|
-
/**
|
|
3386
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
3387
|
-
* for efficient JOIN-based relation loading.
|
|
3388
|
-
* Returns null if the API is not available or the query fails.
|
|
3389
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
3390
|
-
*/
|
|
3391
|
-
async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
|
|
3392
|
-
try {
|
|
3393
|
-
const table = getTableForCollection(collection, this.registry);
|
|
3394
|
-
const tableName = getTableName(table);
|
|
3395
|
-
const queryTarget = this.getQueryBuilder(tableName);
|
|
3396
|
-
if (!queryTarget?.findMany) return null;
|
|
3397
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
3398
|
-
const withConfig = {};
|
|
3399
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
|
|
3400
|
-
const drizzleRelName = relation.relationName || key;
|
|
3401
|
-
withConfig[drizzleRelName] = true;
|
|
3402
|
-
}
|
|
3403
|
-
const queryOpts = { with: withConfig };
|
|
3404
|
-
if (options.limit) queryOpts.limit = options.limit;
|
|
3405
|
-
if (options.filter) {
|
|
3406
|
-
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3407
|
-
if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
|
|
3408
|
-
}
|
|
3409
|
-
if (options.orderBy) {
|
|
3410
|
-
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3411
|
-
if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
3412
|
-
}
|
|
3413
|
-
return (await queryTarget.findMany(queryOpts)).map((row) => {
|
|
3414
|
-
const flat = {};
|
|
3415
|
-
for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
|
|
3416
|
-
const keys = Object.keys(item);
|
|
3417
|
-
const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
3418
|
-
if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
|
|
3419
|
-
return { ...item };
|
|
3420
|
-
});
|
|
3421
|
-
else if (typeof v === "object" && v !== null) flat[k] = { ...v };
|
|
3422
|
-
else flat[k] = v;
|
|
3423
|
-
return flat;
|
|
3424
|
-
});
|
|
3425
|
-
} catch (e) {
|
|
3426
|
-
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
3427
|
-
return null;
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
/**
|
|
3431
|
-
* Fallback path used when db.query is unavailable.
|
|
3432
|
-
* The primary path uses db.query.findMany with `with` config, which
|
|
3433
|
-
* loads all relations in a single query.
|
|
3434
|
-
*
|
|
3435
|
-
* Batch fetch many-to-many related rows for multiple parent IDs.
|
|
3436
|
-
* Groups results by parent ID to avoid N+1.
|
|
3437
|
-
*/
|
|
3438
|
-
async batchFetchManyRelatedRows(parentCollectionPath, parentIds, relationKey) {
|
|
3439
|
-
if (parentIds.length === 0) return /* @__PURE__ */ new Map();
|
|
3440
|
-
const relation = resolveCollectionRelations(getCollectionByPath(parentCollectionPath, this.registry))[relationKey];
|
|
3441
|
-
if (!relation) {
|
|
3442
|
-
logger.warn(`[batchFetchManyRelatedRows] ResolvedRelation '${relationKey}' not found, skipping`);
|
|
3443
|
-
return /* @__PURE__ */ new Map();
|
|
3444
|
-
}
|
|
3445
|
-
const entityMap = await this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
|
|
3446
|
-
const flatMap = /* @__PURE__ */ new Map();
|
|
3447
|
-
for (const [key, rows] of entityMap) flatMap.set(key, rows.map((e) => ({
|
|
3448
|
-
...e.values,
|
|
3449
|
-
id: e.id
|
|
3450
|
-
})));
|
|
3451
|
-
return flatMap;
|
|
3452
|
-
}
|
|
3453
|
-
};
|
|
3454
|
-
//#endregion
|
|
3455
|
-
//#region src/utils/pg-error-utils.ts
|
|
3456
|
-
/**
|
|
3457
|
-
* Shared PostgreSQL error extraction and user-friendly message formatting.
|
|
3458
|
-
*
|
|
3459
|
-
* Drizzle wraps native PG errors in a `.cause` chain. These utilities
|
|
3460
|
-
* unwrap that chain to get the real PostgreSQL error (identified by a
|
|
3461
|
-
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
3462
|
-
* a message that is safe and helpful to show to end-users.
|
|
3463
|
-
*/
|
|
3464
|
-
/**
|
|
3465
|
-
* Return the error when it is a deliberate 4xx, otherwise null.
|
|
3466
|
-
*
|
|
3467
|
-
* A thrown `ApiError` is a decision the server made about the request, not a
|
|
3468
|
-
* database failure: its message and code are already written for the client.
|
|
3469
|
-
*/
|
|
3470
|
-
function asClientFacingError(error) {
|
|
3471
|
-
if (!(error instanceof Error)) return null;
|
|
3472
|
-
const e = error;
|
|
3473
|
-
if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
|
|
3474
|
-
return e;
|
|
3475
|
-
}
|
|
3476
|
-
/**
|
|
3477
|
-
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
3478
|
-
* Drizzle wraps PG errors in a `cause` property — this function
|
|
3479
|
-
* recursively walks the chain until it finds an object with a PG
|
|
3480
|
-
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
3481
|
-
*/
|
|
3482
|
-
function extractPgError(error) {
|
|
3483
|
-
if (!error || typeof error !== "object") return null;
|
|
3484
|
-
if (!(error instanceof Error)) {
|
|
3485
|
-
if ("cause" in error && error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
3486
|
-
return null;
|
|
3487
|
-
}
|
|
3488
|
-
if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) return error;
|
|
3489
|
-
if (error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
3490
|
-
return null;
|
|
3491
|
-
}
|
|
3492
|
-
/**
|
|
3493
|
-
* Walk the error cause chain and return the deepest meaningful message.
|
|
3494
|
-
*/
|
|
3495
|
-
function extractCauseMessage(error) {
|
|
3496
|
-
if (!error || typeof error !== "object") return null;
|
|
3497
|
-
if (!(error instanceof Error)) return null;
|
|
3498
|
-
if (error.cause && typeof error.cause === "object") {
|
|
3499
|
-
const deeper = extractCauseMessage(error.cause);
|
|
3500
|
-
if (deeper) return deeper;
|
|
3501
|
-
if (error.cause instanceof Error && error.cause.message) return error.cause.message;
|
|
3502
|
-
}
|
|
3503
|
-
return null;
|
|
3504
|
-
}
|
|
3505
|
-
/**
|
|
3506
|
-
* Codes that mean "this connection will never work as configured".
|
|
3507
|
-
*
|
|
3508
|
-
* A wrong password or a database that does not exist is a settled fact about
|
|
3509
|
-
* the connection string, not a transient fault — retrying produces the same
|
|
3510
|
-
* answer forever.
|
|
3511
|
-
*/
|
|
3512
|
-
var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
3513
|
-
"28P01",
|
|
3514
|
-
"28000",
|
|
3515
|
-
"3D000",
|
|
3516
|
-
"42501"
|
|
3517
|
-
]);
|
|
3518
|
-
/**
|
|
3519
|
-
* Describe a failed connection attempt in terms a developer can act on.
|
|
3520
|
-
*
|
|
3521
|
-
* The error a caller catches is Drizzle's wrapper: its message is
|
|
3522
|
-
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
3523
|
-
* the sentence that says what is actually wrong — "password authentication
|
|
3524
|
-
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
3525
|
-
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
3526
|
-
* their `DATABASE_URL` nothing at all.
|
|
3527
|
-
*/
|
|
3528
|
-
function classifyConnectFailure(error) {
|
|
3529
|
-
const pgError = extractPgError(error);
|
|
3530
|
-
const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
|
|
3531
|
-
return {
|
|
3532
|
-
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
3533
|
-
reason,
|
|
3534
|
-
code: pgError?.code
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
/**
|
|
3538
|
-
* Detect whether an error is specifically a role-switching permission failure
|
|
3539
|
-
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
3540
|
-
* as opposed to a table-level permission denial.
|
|
3541
|
-
*
|
|
3542
|
-
* This is used by the backend driver to auto-disable role switching when the
|
|
3543
|
-
* connection user lacks SET ROLE privileges, rather than surfacing a confusing
|
|
3544
|
-
* error to the Studio SQL Editor user.
|
|
3545
|
-
*/
|
|
3546
|
-
function isRoleSwitchingPermissionError(error) {
|
|
3547
|
-
const pgError = extractPgError(error);
|
|
3548
|
-
if (!pgError || pgError.code !== "42501") return false;
|
|
3549
|
-
const msg = pgError.message.toLowerCase();
|
|
3550
|
-
return msg.includes("set role") || msg.includes("member of role");
|
|
3551
|
-
}
|
|
3552
|
-
/**
|
|
3553
|
-
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
3554
|
-
*
|
|
3555
|
-
* @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
|
|
3556
|
-
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
3557
|
-
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
3558
|
-
*/
|
|
3559
|
-
function pgErrorToFriendlyMessage(pgError, context) {
|
|
3560
|
-
const detail = pgError.detail;
|
|
3561
|
-
const hint = pgError.hint;
|
|
3562
|
-
const constraint = pgError.constraint;
|
|
3563
|
-
const column = pgError.column;
|
|
3564
|
-
const table = pgError.table;
|
|
3565
|
-
const dataType = pgError.dataType;
|
|
3566
|
-
const pgMessage = pgError.message || "Unknown database error";
|
|
3567
|
-
const code = pgError.code || "UNKNOWN";
|
|
3568
|
-
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
3569
|
-
const tableRef = table ?? context;
|
|
3570
|
-
switch (pgError.code) {
|
|
3571
|
-
case "23503": return {
|
|
3572
|
-
message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3573
|
-
code
|
|
3574
|
-
};
|
|
3575
|
-
case "23505": return {
|
|
3576
|
-
message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3577
|
-
code
|
|
3578
|
-
};
|
|
3579
|
-
case "23502": return {
|
|
3580
|
-
message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
|
|
3581
|
-
code
|
|
3582
|
-
};
|
|
3583
|
-
case "23514": return {
|
|
3584
|
-
message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3585
|
-
code
|
|
3586
|
-
};
|
|
3587
|
-
case "22P02": return {
|
|
3588
|
-
message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
|
|
3589
|
-
code
|
|
3590
|
-
};
|
|
3591
|
-
case "22001": return {
|
|
3592
|
-
message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
3593
|
-
code
|
|
3594
|
-
};
|
|
3595
|
-
case "22003": return {
|
|
3596
|
-
message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
3597
|
-
code
|
|
3598
|
-
};
|
|
3599
|
-
case "42703": return {
|
|
3600
|
-
message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
3601
|
-
code
|
|
3602
|
-
};
|
|
3603
|
-
case "42P01": return {
|
|
3604
|
-
message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
3605
|
-
code
|
|
3606
|
-
};
|
|
3607
|
-
case "42501": return {
|
|
3608
|
-
message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
|
|
3609
|
-
code
|
|
3610
|
-
};
|
|
3611
|
-
case "28000": return {
|
|
3612
|
-
message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
|
|
3613
|
-
code
|
|
3614
|
-
};
|
|
3615
|
-
default: {
|
|
3616
|
-
const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
|
|
3617
|
-
if (detail) parts.push(`Detail: ${detail}`);
|
|
3618
|
-
if (column) parts.push(`Column: ${column}`);
|
|
3619
|
-
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
3620
|
-
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
3621
|
-
if (hint) parts.push(`Hint: ${hint}`);
|
|
3622
|
-
return {
|
|
3623
|
-
message: parts.join(". "),
|
|
3624
|
-
code
|
|
3625
|
-
};
|
|
3626
|
-
}
|
|
3739
|
+
return flatEntity;
|
|
3627
3740
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Fetch raw rows without any relation processing (for REST fast path)
|
|
3743
|
+
*/
|
|
3744
|
+
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
3745
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
3746
|
+
const table = getTableForCollection(collection, this.registry);
|
|
3747
|
+
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
3748
|
+
let vectorMeta;
|
|
3749
|
+
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
3750
|
+
let query = vectorMeta ? this.db.select({
|
|
3751
|
+
table_row: table,
|
|
3752
|
+
_distance: vectorMeta.distanceSelect
|
|
3753
|
+
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
3754
|
+
const allConditions = [];
|
|
3755
|
+
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
3756
|
+
if (options.searchString) {
|
|
3757
|
+
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
3758
|
+
if (searchConditions.length === 0) return [];
|
|
3759
|
+
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
3760
|
+
}
|
|
3761
|
+
if (options.filter) {
|
|
3762
|
+
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3763
|
+
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
3764
|
+
}
|
|
3765
|
+
if (vectorMeta?.filter) allConditions.push(vectorMeta.filter);
|
|
3766
|
+
if (allConditions.length > 0) {
|
|
3767
|
+
const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
|
|
3768
|
+
if (finalCondition) query = query.where(finalCondition);
|
|
3769
|
+
}
|
|
3770
|
+
const orderExpressions = [];
|
|
3771
|
+
if (vectorMeta) orderExpressions.push(asc(vectorMeta.orderBy));
|
|
3772
|
+
else if (options.orderBy) {
|
|
3773
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3774
|
+
if (orderByField) orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
|
|
3775
|
+
}
|
|
3776
|
+
orderExpressions.push(desc(idField));
|
|
3777
|
+
if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
|
|
3778
|
+
const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
|
|
3779
|
+
if (limitValue) query = query.limit(limitValue);
|
|
3780
|
+
if (options.offset && options.offset > 0) query = query.offset(options.offset);
|
|
3781
|
+
const rawResults = await query;
|
|
3782
|
+
if (vectorMeta) return rawResults.map((r) => ({
|
|
3783
|
+
...r.table_row,
|
|
3784
|
+
_distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
|
|
3785
|
+
}));
|
|
3786
|
+
return rawResults;
|
|
3652
3787
|
}
|
|
3653
|
-
|
|
3654
|
-
if
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
constraint: pgError.constraint,
|
|
3663
|
-
dataType: pgError.dataType,
|
|
3664
|
-
drizzleMessage: error instanceof Error ? error.message : String(error)
|
|
3665
|
-
});
|
|
3666
|
-
return pgErrorToFriendlyMessage(pgError, context);
|
|
3788
|
+
/**
|
|
3789
|
+
* Check if the Drizzle instance has the relational query API available
|
|
3790
|
+
* for a given collection path.
|
|
3791
|
+
* Note: Primary path now uses inline `getQueryBuilder()` checks.
|
|
3792
|
+
*/
|
|
3793
|
+
hasDrizzleQueryAPI(collectionPath) {
|
|
3794
|
+
if (!this.getQueryBuilder("__probe__")) return false;
|
|
3795
|
+
const tableName = getTableName(getTableForCollection(getCollectionByPath(collectionPath, this.registry), this.registry));
|
|
3796
|
+
return !!this.getQueryBuilder(tableName);
|
|
3667
3797
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3798
|
+
/**
|
|
3799
|
+
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
3800
|
+
* for efficient JOIN-based relation loading.
|
|
3801
|
+
* Returns null if the API is not available or the query fails.
|
|
3802
|
+
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
3803
|
+
*/
|
|
3804
|
+
async fetchWithDrizzleQuery(collectionPath, collection, options, include, idInfo, idInfoArray) {
|
|
3805
|
+
try {
|
|
3806
|
+
const table = getTableForCollection(collection, this.registry);
|
|
3807
|
+
const tableName = getTableName(table);
|
|
3808
|
+
const queryTarget = this.getQueryBuilder(tableName);
|
|
3809
|
+
if (!queryTarget?.findMany) return null;
|
|
3810
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
3811
|
+
const withConfig = {};
|
|
3812
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) if (include[0] === "*" || include.includes(key)) {
|
|
3813
|
+
const drizzleRelName = relation.relationName || key;
|
|
3814
|
+
withConfig[drizzleRelName] = true;
|
|
3815
|
+
}
|
|
3816
|
+
const queryOpts = { with: withConfig };
|
|
3817
|
+
if (options.limit) queryOpts.limit = options.limit;
|
|
3818
|
+
if (options.filter) {
|
|
3819
|
+
const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
|
|
3820
|
+
if (filterConditions.length > 0) queryOpts.where = and(...filterConditions);
|
|
3821
|
+
}
|
|
3822
|
+
if (options.orderBy) {
|
|
3823
|
+
const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
|
|
3824
|
+
if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
3825
|
+
}
|
|
3826
|
+
return (await queryTarget.findMany(queryOpts)).map((row) => {
|
|
3827
|
+
const flat = {};
|
|
3828
|
+
for (const [k, v] of Object.entries(row)) if (Array.isArray(v)) flat[k] = v.map((item) => {
|
|
3829
|
+
const keys = Object.keys(item);
|
|
3830
|
+
const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
3831
|
+
if (nestedObj && keys.length <= 3) return { ...item[nestedObj] };
|
|
3832
|
+
return { ...item };
|
|
3833
|
+
});
|
|
3834
|
+
else if (typeof v === "object" && v !== null) flat[k] = { ...v };
|
|
3835
|
+
else flat[k] = v;
|
|
3836
|
+
return flat;
|
|
3837
|
+
});
|
|
3838
|
+
} catch (e) {
|
|
3839
|
+
logger.warn(`[include] Drizzle relational query failed for '${collectionPath}', falling back`, { error: e });
|
|
3840
|
+
return null;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3844
|
+
* Fallback path used when db.query is unavailable.
|
|
3845
|
+
* The primary path uses db.query.findMany with `with` config, which
|
|
3846
|
+
* loads all relations in a single query.
|
|
3847
|
+
*
|
|
3848
|
+
* Batch fetch many-to-many related rows for multiple parent IDs.
|
|
3849
|
+
* Groups results by parent ID to avoid N+1.
|
|
3850
|
+
*/
|
|
3851
|
+
async batchFetchManyRelatedRows(parentCollectionPath, parentIds, relationKey) {
|
|
3852
|
+
if (parentIds.length === 0) return /* @__PURE__ */ new Map();
|
|
3853
|
+
const relation = resolveCollectionRelations(getCollectionByPath(parentCollectionPath, this.registry))[relationKey];
|
|
3854
|
+
if (!relation) {
|
|
3855
|
+
logger.warn(`[batchFetchManyRelatedRows] ResolvedRelation '${relationKey}' not found, skipping`);
|
|
3856
|
+
return /* @__PURE__ */ new Map();
|
|
3857
|
+
}
|
|
3858
|
+
const entityMap = await this.relationService.batchFetchRelatedEntitiesMany(parentCollectionPath, parentIds, relationKey, relation);
|
|
3859
|
+
const flatMap = /* @__PURE__ */ new Map();
|
|
3860
|
+
for (const [key, rows] of entityMap) flatMap.set(key, rows.map((e) => ({
|
|
3861
|
+
...e.values,
|
|
3862
|
+
id: e.id
|
|
3863
|
+
})));
|
|
3864
|
+
return flatMap;
|
|
3865
|
+
}
|
|
3866
|
+
};
|
|
3677
3867
|
//#endregion
|
|
3678
3868
|
//#region src/services/PersistService.ts
|
|
3679
3869
|
/**
|
|
@@ -3797,10 +3987,11 @@ var PersistService = class {
|
|
|
3797
3987
|
} else {
|
|
3798
3988
|
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
3799
3989
|
if (targetColumnName) {
|
|
3800
|
-
const
|
|
3990
|
+
const parentKeyValue = hasForeignKeyOnTarget(hop.relation) ? await this.relationService.parentKeyValue(hop.parentCollection, hop.relation, hop.parentId) : parentIdForWrite();
|
|
3991
|
+
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.`);
|
|
3801
3992
|
const existingValue = effectiveValues[targetColumnName];
|
|
3802
|
-
if (existingValue !== void 0 && existingValue !== null && existingValue !==
|
|
3803
|
-
effectiveValues[targetColumnName] =
|
|
3993
|
+
if (existingValue !== void 0 && existingValue !== null && existingValue !== parentKeyValue) logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent key '${parentKeyValue}'.`);
|
|
3994
|
+
effectiveValues[targetColumnName] = parentKeyValue;
|
|
3804
3995
|
}
|
|
3805
3996
|
}
|
|
3806
3997
|
}
|
|
@@ -4074,19 +4265,34 @@ function validateIdentifier(value, label) {
|
|
|
4074
4265
|
if (!/^[a-zA-Z0-9_-]+$/.test(value)) throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);
|
|
4075
4266
|
}
|
|
4076
4267
|
/**
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4268
|
+
* Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it
|
|
4269
|
+
* silently. The prefix comes out of the same budget.
|
|
4270
|
+
*/
|
|
4271
|
+
var MAX_BRANCH_NAME_LENGTH = 60;
|
|
4272
|
+
/**
|
|
4273
|
+
* Check a user-provided branch name, and otherwise leave it exactly as given.
|
|
4274
|
+
*
|
|
4275
|
+
* This used to strip everything outside [a-zA-Z0-9_], so `my-feature` was
|
|
4276
|
+
* quietly created as `myfeature`: the name you typed was not the name `list`
|
|
4277
|
+
* gave back. Nothing ever needed that. Every identifier this service builds is
|
|
4278
|
+
* double-quoted (see `CREATE DATABASE` below), which is what makes a hyphen
|
|
4279
|
+
* safe, and `validateIdentifier` has always accepted hyphens for the `--from`
|
|
4280
|
+
* source database — the two disagreed about the same character class.
|
|
4281
|
+
*
|
|
4282
|
+
* Refusing a name we cannot represent is better than representing a different
|
|
4283
|
+
* one, which is also why the length is checked here rather than left to
|
|
4284
|
+
* Postgres, whose answer to an over-long identifier is a silent rename.
|
|
4079
4285
|
*/
|
|
4080
|
-
function
|
|
4081
|
-
|
|
4286
|
+
function assertValidBranchName(name) {
|
|
4287
|
+
validateIdentifier(name, "branch name");
|
|
4288
|
+
if (name.length > MAX_BRANCH_NAME_LENGTH) throw new Error(`Branch name "${name}" is too long: ${name.length} characters, maximum ${MAX_BRANCH_NAME_LENGTH}. Postgres truncates identifiers past 63 bytes, which would give the branch a name you did not choose.`);
|
|
4082
4289
|
}
|
|
4083
4290
|
/**
|
|
4084
4291
|
* Convert a user-facing branch name to the actual PostgreSQL database name.
|
|
4085
4292
|
*/
|
|
4086
4293
|
function toBranchDbName(name) {
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
return `${BRANCH_DB_PREFIX}${sanitized}`;
|
|
4294
|
+
assertValidBranchName(name);
|
|
4295
|
+
return `${BRANCH_DB_PREFIX}${name}`;
|
|
4090
4296
|
}
|
|
4091
4297
|
var BranchService = class {
|
|
4092
4298
|
db;
|
|
@@ -4123,9 +4329,8 @@ var BranchService = class {
|
|
|
4123
4329
|
async createBranch(name, options) {
|
|
4124
4330
|
if (options?.source) validateIdentifier(options.source, "source database name");
|
|
4125
4331
|
const dbName = toBranchDbName(name);
|
|
4126
|
-
const sanitizedName = sanitizeBranchName(name);
|
|
4127
4332
|
const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
|
|
4128
|
-
if ((await this.db.execute(sql`SELECT name FROM rebase.branches WHERE name = ${
|
|
4333
|
+
if ((await this.db.execute(sql`SELECT name FROM rebase.branches WHERE name = ${name} OR db_name = ${dbName}`)).rows.length > 0) throw new Error(`Branch "${name}" already exists.`);
|
|
4129
4334
|
await this.poolManager.disconnectDatabase(sourceDb);
|
|
4130
4335
|
const safeDbName = dbName.replace(/"/g, "\"\"");
|
|
4131
4336
|
const safeSourceDb = sourceDb.replace(/"/g, "\"\"");
|
|
@@ -4136,10 +4341,10 @@ var BranchService = class {
|
|
|
4136
4341
|
throw describeBranchDdlError(err, dbName);
|
|
4137
4342
|
}
|
|
4138
4343
|
const now = /* @__PURE__ */ new Date();
|
|
4139
|
-
await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
|
|
4140
|
-
VALUES (${
|
|
4344
|
+
await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
|
|
4345
|
+
VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`);
|
|
4141
4346
|
return {
|
|
4142
|
-
name
|
|
4347
|
+
name,
|
|
4143
4348
|
parentDatabase: sourceDb,
|
|
4144
4349
|
createdAt: now
|
|
4145
4350
|
};
|
|
@@ -4149,19 +4354,21 @@ var BranchService = class {
|
|
|
4149
4354
|
* Cannot delete the main/default database.
|
|
4150
4355
|
*/
|
|
4151
4356
|
async deleteBranch(name) {
|
|
4152
|
-
|
|
4153
|
-
|
|
4357
|
+
assertValidBranchName(name);
|
|
4358
|
+
if (toBranchDbName(name) === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
|
|
4359
|
+
const existingRows = (await this.db.execute(sql`SELECT db_name FROM rebase.branches WHERE name = ${name}`)).rows;
|
|
4360
|
+
if (existingRows.length === 0) throw new Error(`Branch "${name}" not found.`);
|
|
4361
|
+
const dbName = existingRows[0].db_name;
|
|
4154
4362
|
if (dbName === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
|
|
4155
|
-
if ((await this.db.execute(sql`SELECT db_name FROM rebase.branches WHERE name = ${sanitizedName}`)).rows.length === 0) throw new Error(`Branch "${sanitizedName}" not found.`);
|
|
4156
4363
|
await this.poolManager.disconnectDatabase(dbName);
|
|
4157
4364
|
const safeDbName = dbName.replace(/"/g, "\"\"");
|
|
4158
4365
|
try {
|
|
4159
4366
|
await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
|
|
4160
4367
|
} catch (err) {
|
|
4161
|
-
if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${
|
|
4368
|
+
if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${name}": the database has active connections. Close other clients and try again.`);
|
|
4162
4369
|
throw describeBranchDdlError(err, dbName);
|
|
4163
4370
|
}
|
|
4164
|
-
await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${
|
|
4371
|
+
await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
|
|
4165
4372
|
}
|
|
4166
4373
|
/**
|
|
4167
4374
|
* List all branches recorded in the metadata table.
|
|
@@ -4188,20 +4395,21 @@ var BranchService = class {
|
|
|
4188
4395
|
* Get info about a specific branch.
|
|
4189
4396
|
*/
|
|
4190
4397
|
async getBranchInfo(name) {
|
|
4191
|
-
|
|
4398
|
+
assertValidBranchName(name);
|
|
4192
4399
|
const rows = (await this.db.execute(sql`
|
|
4193
|
-
SELECT
|
|
4400
|
+
SELECT
|
|
4194
4401
|
b.name,
|
|
4402
|
+
b.db_name,
|
|
4195
4403
|
b.parent_db,
|
|
4196
4404
|
b.created_at
|
|
4197
4405
|
FROM rebase.branches b
|
|
4198
|
-
WHERE b.name = ${
|
|
4406
|
+
WHERE b.name = ${name}
|
|
4199
4407
|
`)).rows;
|
|
4200
4408
|
if (rows.length === 0) return void 0;
|
|
4201
4409
|
const row = rows[0];
|
|
4202
4410
|
let sizeBytes;
|
|
4203
4411
|
try {
|
|
4204
|
-
const dbName =
|
|
4412
|
+
const dbName = row.db_name;
|
|
4205
4413
|
const sizeRows = (await this.db.execute(sql`SELECT pg_database_size(${dbName}) as size_bytes`)).rows;
|
|
4206
4414
|
if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) sizeBytes = Number(sizeRows[0].size_bytes);
|
|
4207
4415
|
} catch {}
|
|
@@ -5800,6 +6008,7 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5800
6008
|
const isId = isIdProperty(propName, prop, collection);
|
|
5801
6009
|
let baseType = numProp.validation?.integer || isId ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
5802
6010
|
if (numProp.columnType) if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
|
|
6011
|
+
else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") baseType = `${numProp.columnType}("${colName}", { mode: "number" })`;
|
|
5803
6012
|
else baseType = `${numProp.columnType}("${colName}")`;
|
|
5804
6013
|
if ("isId" in numProp && numProp.isId === "increment") columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
|
|
5805
6014
|
else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
|
|
@@ -6152,8 +6361,9 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
6152
6361
|
if (!emittedRelationNames.has(deduplicationKey)) {
|
|
6153
6362
|
const otherTableVar = getTableVarName(getTableName$1(otherCollection));
|
|
6154
6363
|
const drizzleFieldKey = resolvePropertyKeyForColumn(collection, otherRel.foreignKeyOnTarget);
|
|
6364
|
+
const referencedKey = otherRel.sourceKey ? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey) : getPrimaryKeyName(otherCollection);
|
|
6155
6365
|
const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
|
|
6156
|
-
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${
|
|
6366
|
+
tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${referencedKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
|
|
6157
6367
|
emittedRelationNames.add(deduplicationKey);
|
|
6158
6368
|
}
|
|
6159
6369
|
}
|
|
@@ -7311,14 +7521,6 @@ function resolveChannelBusSetting(configured) {
|
|
|
7311
7521
|
return raw === "memory" ? { type: "memory" } : { type: "postgres" };
|
|
7312
7522
|
}
|
|
7313
7523
|
/**
|
|
7314
|
-
* @deprecated Use {@link resolveChannelBusSetting}, which also accepts a
|
|
7315
|
-
* supplied {@link ChannelBus} instance. Kept as a narrow alias so existing
|
|
7316
|
-
* config-only callers keep their exact types.
|
|
7317
|
-
*/
|
|
7318
|
-
function resolveChannelBusConfig(configured) {
|
|
7319
|
-
return resolveChannelBusSetting(configured);
|
|
7320
|
-
}
|
|
7321
|
-
/**
|
|
7322
7524
|
* Produce the bus a setting asks for.
|
|
7323
7525
|
*
|
|
7324
7526
|
* An instance is handed straight back — constructing it was the application's
|
|
@@ -8929,7 +9131,7 @@ function createBackupCron(config) {
|
|
|
8929
9131
|
enabled: config.enabled ?? true,
|
|
8930
9132
|
timeoutSeconds: 3600,
|
|
8931
9133
|
async handler({ log }) {
|
|
8932
|
-
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-
|
|
9134
|
+
const { createDump, pruneBackups, uploadBackup, validateDump } = await import("./backup-service-CD8o_1Sl.js").then((n) => n.r);
|
|
8933
9135
|
const { destination } = config;
|
|
8934
9136
|
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 }).`);
|
|
8935
9137
|
log(`Starting backup of "${dbName}"…`);
|
|
@@ -9060,6 +9262,11 @@ function findRelationDefects(collections, registry) {
|
|
|
9060
9262
|
problem: `\`foreignKeyOnTarget: "${relation.foreignKeyOnTarget}"\` is not a column on the target table \`${targetTableName}\``,
|
|
9061
9263
|
fix: `add the column, or set \`foreignKeyOnTarget\` to one of: ${quote(targetColumns)}`
|
|
9062
9264
|
});
|
|
9265
|
+
if (relation.sourceKey && !sourceColumns.has(relation.sourceKey)) defects.push({
|
|
9266
|
+
...at,
|
|
9267
|
+
problem: `\`sourceKey: "${relation.sourceKey}"\` is not a column on \`${sourceTableName}\``,
|
|
9268
|
+
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)}`
|
|
9269
|
+
});
|
|
9063
9270
|
break;
|
|
9064
9271
|
case "manyToMany": {
|
|
9065
9272
|
const { table, sourceColumn, targetColumn } = relation.through;
|
|
@@ -9811,21 +10018,6 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
9811
10018
|
}
|
|
9812
10019
|
//#endregion
|
|
9813
10020
|
//#region src/auth/services.ts
|
|
9814
|
-
function getColumnKey(table, ...keys) {
|
|
9815
|
-
if (!table) return void 0;
|
|
9816
|
-
for (const key of keys) {
|
|
9817
|
-
if (key in table) return key;
|
|
9818
|
-
const snake = toSnakeCase(key);
|
|
9819
|
-
if (snake in table) return snake;
|
|
9820
|
-
const camel = camelCase(key);
|
|
9821
|
-
if (camel in table) return camel;
|
|
9822
|
-
}
|
|
9823
|
-
}
|
|
9824
|
-
function getColumn(table, ...keys) {
|
|
9825
|
-
if (!table) return void 0;
|
|
9826
|
-
const key = getColumnKey(table, ...keys);
|
|
9827
|
-
return key ? table[key] : void 0;
|
|
9828
|
-
}
|
|
9829
10021
|
/**
|
|
9830
10022
|
* The single definition of what an email address looks like in storage.
|
|
9831
10023
|
*
|
|
@@ -9840,9 +10032,25 @@ function getColumn(table, ...keys) {
|
|
|
9840
10032
|
*
|
|
9841
10033
|
* Whitespace goes too: a trailing space survives the fold and reproduces the
|
|
9842
10034
|
* problem exactly.
|
|
10035
|
+
*
|
|
10036
|
+
* Re-exported rather than defined here: `@rebasepro/server` and
|
|
10037
|
+
* `@rebasepro/server-mongo` write this column too, and a second copy of this
|
|
10038
|
+
* rule is the defect it exists to prevent.
|
|
9843
10039
|
*/
|
|
9844
|
-
function
|
|
9845
|
-
|
|
10040
|
+
function getColumnKey(table, ...keys) {
|
|
10041
|
+
if (!table) return void 0;
|
|
10042
|
+
for (const key of keys) {
|
|
10043
|
+
if (key in table) return key;
|
|
10044
|
+
const snake = toSnakeCase(key);
|
|
10045
|
+
if (snake in table) return snake;
|
|
10046
|
+
const camel = camelCase(key);
|
|
10047
|
+
if (camel in table) return camel;
|
|
10048
|
+
}
|
|
10049
|
+
}
|
|
10050
|
+
function getColumn(table, ...keys) {
|
|
10051
|
+
if (!table) return void 0;
|
|
10052
|
+
const key = getColumnKey(table, ...keys);
|
|
10053
|
+
return key ? table[key] : void 0;
|
|
9846
10054
|
}
|
|
9847
10055
|
/**
|
|
9848
10056
|
* PostgreSQL implementation of UserRepository.
|
|
@@ -11183,6 +11391,31 @@ function humanize(snakeName) {
|
|
|
11183
11391
|
return snakeName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
11184
11392
|
}
|
|
11185
11393
|
//#endregion
|
|
11394
|
+
//#region src/schema/introspect-db-types.ts
|
|
11395
|
+
/**
|
|
11396
|
+
* The PostgreSQL type → Rebase property type mapping.
|
|
11397
|
+
*
|
|
11398
|
+
* Split out of `introspect-db-logic` so that the structural analysis can use it
|
|
11399
|
+
* without importing the generator, which imports the analysis. Re-exported from
|
|
11400
|
+
* `introspect-db-logic` so existing callers keep their import path.
|
|
11401
|
+
*/
|
|
11402
|
+
/**
|
|
11403
|
+
* Map a PostgreSQL data type to a Rebase property type.
|
|
11404
|
+
*/
|
|
11405
|
+
function mapPgType(dataType) {
|
|
11406
|
+
const dt = dataType.toLowerCase();
|
|
11407
|
+
if (dt === "interval") return "string";
|
|
11408
|
+
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
11409
|
+
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";
|
|
11410
|
+
if (dt.includes("bool")) return "boolean";
|
|
11411
|
+
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
11412
|
+
if (dt === "json" || dt === "jsonb") return "map";
|
|
11413
|
+
if (dt === "bytea") return "binary";
|
|
11414
|
+
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
11415
|
+
if (dt === "uuid") return "string";
|
|
11416
|
+
return "string";
|
|
11417
|
+
}
|
|
11418
|
+
//#endregion
|
|
11186
11419
|
//#region src/schema/introspect-db-logic.ts
|
|
11187
11420
|
var IRREGULAR_SINGULARS = {
|
|
11188
11421
|
people: "person",
|
|
@@ -11287,22 +11520,6 @@ function getIconForTable(tableName) {
|
|
|
11287
11520
|
if (table.includes("comment") || table.includes("review") || table.includes("feedback")) return "MessageCircle";
|
|
11288
11521
|
return "Database";
|
|
11289
11522
|
}
|
|
11290
|
-
/**
|
|
11291
|
-
* Map a PostgreSQL data type to a Rebase property type.
|
|
11292
|
-
*/
|
|
11293
|
-
function mapPgType(dataType) {
|
|
11294
|
-
const dt = dataType.toLowerCase();
|
|
11295
|
-
if (dt === "interval") return "string";
|
|
11296
|
-
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
11297
|
-
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";
|
|
11298
|
-
if (dt.includes("bool")) return "boolean";
|
|
11299
|
-
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
11300
|
-
if (dt === "json" || dt === "jsonb") return "map";
|
|
11301
|
-
if (dt === "bytea") return "binary";
|
|
11302
|
-
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
11303
|
-
if (dt === "uuid") return "string";
|
|
11304
|
-
return "string";
|
|
11305
|
-
}
|
|
11306
11523
|
function buildEnumMap(enumValues) {
|
|
11307
11524
|
const enumMap = /* @__PURE__ */ new Map();
|
|
11308
11525
|
for (const ev of enumValues) {
|
|
@@ -11322,6 +11539,20 @@ function buildTablesMap(tables, columns, pks, fks) {
|
|
|
11322
11539
|
});
|
|
11323
11540
|
return tablesMap;
|
|
11324
11541
|
}
|
|
11542
|
+
/**
|
|
11543
|
+
* Join tables, identified by column name.
|
|
11544
|
+
*
|
|
11545
|
+
* Superseded for the CLI by `classifyTables` in `./introspect-db-structure`,
|
|
11546
|
+
* which asks the database instead: two single-column keys, unique together, no
|
|
11547
|
+
* payload column, nothing referencing the table. This rule folds away
|
|
11548
|
+
* `northwind.order_details` — which has the key shape and carries unit price,
|
|
11549
|
+
* quantity and discount — because it recognises `id`, `created_at` and
|
|
11550
|
+
* `updated_at` by name and calls everything else a foreign key.
|
|
11551
|
+
*
|
|
11552
|
+
* Still used by `./introspect-runtime`, which builds collections in memory from
|
|
11553
|
+
* a narrower set of catalog queries and has no unique-constraint or row-count
|
|
11554
|
+
* data to reason with.
|
|
11555
|
+
*/
|
|
11325
11556
|
function identifyJoinTables(tablesMap) {
|
|
11326
11557
|
const joinTables = /* @__PURE__ */ new Set();
|
|
11327
11558
|
for (const [tableName, meta] of tablesMap.entries()) if (meta.fks.length === 2) {
|
|
@@ -12057,7 +12288,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12057
12288
|
*/
|
|
12058
12289
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
12059
12290
|
const internals = driverResult.internals;
|
|
12060
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
12291
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CBQdOETu.js");
|
|
12061
12292
|
const plan = await ensureCollectionTables({ async query(text) {
|
|
12062
12293
|
const result = await internals.db.execute(sql.raw(text));
|
|
12063
12294
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12082,7 +12313,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12082
12313
|
*/
|
|
12083
12314
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12084
12315
|
const internals = driverResult.internals;
|
|
12085
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
12316
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-ViG8XiPn.js");
|
|
12086
12317
|
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
12087
12318
|
const result = await internals.db.execute(sql.raw(text));
|
|
12088
12319
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12136,6 +12367,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
12136
12367
|
};
|
|
12137
12368
|
}
|
|
12138
12369
|
//#endregion
|
|
12139
|
-
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, pinSearchPath, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations,
|
|
12370
|
+
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 };
|
|
12140
12371
|
|
|
12141
12372
|
//# sourceMappingURL=index.es.js.map
|