@rebasepro/server-postgres 0.12.1-canary.gdd7172e → 0.12.1-canary.gddbd489
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/{ensure-collection-policies-BrUVgjz3.js → ensure-collection-policies-ViG8XiPn.js} +2 -2
- package/dist/{ensure-collection-policies-BrUVgjz3.js.map → ensure-collection-policies-ViG8XiPn.js.map} +1 -1
- package/dist/{ensure-collection-tables-Da2oGkX2.js → ensure-collection-tables-CBQdOETu.js} +2 -2
- package/dist/{ensure-collection-tables-Da2oGkX2.js.map → ensure-collection-tables-CBQdOETu.js.map} +1 -1
- package/dist/index.es.js +272 -226
- package/dist/index.es.js.map +1 -1
- package/dist/services/collection-helpers.d.ts +12 -1
- package/dist/{src-CzbghKwf.js → src-DlPBctw_.js} +30 -2
- package/dist/src-DlPBctw_.js.map +1 -0
- package/dist/utils/pg-error-utils.d.ts +16 -0
- package/package.json +6 -6
- package/src/schema/generate-drizzle-schema-logic.ts +19 -0
- package/src/services/FetchService.ts +14 -0
- package/src/services/collection-helpers.ts +29 -2
- package/src/utils/pg-error-utils.ts +19 -0
- package/dist/src-CzbghKwf.js.map +0 -1
|
@@ -24,6 +24,22 @@ export interface PostgresError extends Error {
|
|
|
24
24
|
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
25
25
|
*/
|
|
26
26
|
export declare function extractPgError(error: unknown): PostgresError | null;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the failure came back from Postgres rather than from building the
|
|
29
|
+
* query — which decides whether a fallback query is worth issuing.
|
|
30
|
+
*
|
|
31
|
+
* Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
|
|
32
|
+
* RLS). Once a statement raises, that transaction is aborted, and every later
|
|
33
|
+
* statement on it returns `25P02` — "current transaction is aborted, commands
|
|
34
|
+
* ignored until end of transaction block". So a retry after a database error
|
|
35
|
+
* cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
|
|
36
|
+
* for type uuid") with a generic one. Rethrow instead.
|
|
37
|
+
*
|
|
38
|
+
* A query the driver could not even build — a missing reciprocal relation, say
|
|
39
|
+
* — never reached Postgres, leaves the transaction usable, and is exactly what
|
|
40
|
+
* the fallback paths exist for.
|
|
41
|
+
*/
|
|
42
|
+
export declare function reachedDatabase(error: unknown): boolean;
|
|
27
43
|
/**
|
|
28
44
|
* Walk the error cause chain and return the deepest meaningful message.
|
|
29
45
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.1-canary.
|
|
4
|
+
"version": "0.12.1-canary.gddbd489",
|
|
5
5
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"execa": "^9.6.1",
|
|
48
48
|
"pg": "^8.22.0",
|
|
49
49
|
"ws": "^8.21.1",
|
|
50
|
-
"@rebasepro/
|
|
51
|
-
"@rebasepro/server": "0.12.1-canary.
|
|
52
|
-
"@rebasepro/
|
|
53
|
-
"@rebasepro/utils": "0.12.1-canary.
|
|
54
|
-
"@rebasepro/
|
|
50
|
+
"@rebasepro/codegen": "0.12.1-canary.gddbd489",
|
|
51
|
+
"@rebasepro/server": "0.12.1-canary.gddbd489",
|
|
52
|
+
"@rebasepro/common": "0.12.1-canary.gddbd489",
|
|
53
|
+
"@rebasepro/utils": "0.12.1-canary.gddbd489",
|
|
54
|
+
"@rebasepro/types": "0.12.1-canary.gddbd489"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@hono/node-server": "^2.0.12",
|
|
@@ -137,6 +137,25 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
|
|
|
137
137
|
let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
138
138
|
if (numProp.columnType) {
|
|
139
139
|
if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
|
|
140
|
+
// `bigint` and `bigserial` are the only pg-core builders that
|
|
141
|
+
// *require* a config argument: without `mode`, drizzle cannot
|
|
142
|
+
// know whether to hand back a `number` or a `bigint`, and the
|
|
143
|
+
// emitted call does not typecheck.
|
|
144
|
+
//
|
|
145
|
+
// This is why `schema.generated.ts` drifted. Regenerating it
|
|
146
|
+
// produced a file that would not compile, so the bigint lines
|
|
147
|
+
// were hand-patched — and every regeneration after that looked
|
|
148
|
+
// like a large, alarming diff nobody wanted to ship. The file
|
|
149
|
+
// then sat stale for long enough that a security fix to two RLS
|
|
150
|
+
// policies never reached production.
|
|
151
|
+
//
|
|
152
|
+
// `number` rather than `bigint`: these are counters and byte
|
|
153
|
+
// totals that every caller already treats as numbers, and
|
|
154
|
+
// switching the runtime type would be a breaking change to
|
|
155
|
+
// every consumer of the generated schema.
|
|
156
|
+
else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") {
|
|
157
|
+
baseType = `${numProp.columnType}("${colName}", { mode: "number" })`;
|
|
158
|
+
}
|
|
140
159
|
else baseType = `${numProp.columnType}("${colName}")`;
|
|
141
160
|
}
|
|
142
161
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
requirePrimaryKeys,
|
|
13
13
|
deriveRowAddress,
|
|
14
14
|
parseIdValues,
|
|
15
|
+
idCanAddressTable,
|
|
15
16
|
buildCompositeId,
|
|
16
17
|
COMPOSITE_ID_SEPARATOR
|
|
17
18
|
} from "./collection-helpers";
|
|
@@ -23,6 +24,7 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
23
24
|
import { toFlatRow, toRestRow, isJunctionRelation } from "./row-pipeline";
|
|
24
25
|
import { isNestedPath, resolveNestedPath, type NestedPathHop } from "./nested-path";
|
|
25
26
|
import { ApiError, logger } from "@rebasepro/server";
|
|
27
|
+
import { reachedDatabase } from "../utils/pg-error-utils";
|
|
26
28
|
|
|
27
29
|
/** Type-safe accessor for Drizzle's relational query API via dynamic table name */
|
|
28
30
|
type DbQueryAccessor = Record<string, RelationalQueryBuilder<any, any>> | undefined;
|
|
@@ -588,6 +590,11 @@ idColumn };
|
|
|
588
590
|
throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
589
591
|
}
|
|
590
592
|
|
|
593
|
+
// An address the key columns cannot hold names no row — the same answer
|
|
594
|
+
// as a well-formed id nobody has. Asking Postgres instead raises 22P02
|
|
595
|
+
// and aborts the transaction around this read.
|
|
596
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return undefined;
|
|
597
|
+
|
|
591
598
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
592
599
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
593
600
|
|
|
@@ -618,6 +625,7 @@ idColumn };
|
|
|
618
625
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
619
626
|
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.");
|
|
620
627
|
}
|
|
628
|
+
if (reachedDatabase(e)) throw e;
|
|
621
629
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
622
630
|
}
|
|
623
631
|
}
|
|
@@ -743,6 +751,7 @@ idColumn };
|
|
|
743
751
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
744
752
|
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.");
|
|
745
753
|
}
|
|
754
|
+
if (reachedDatabase(e)) throw e;
|
|
746
755
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
747
756
|
}
|
|
748
757
|
}
|
|
@@ -1173,6 +1182,7 @@ relatedTo: hop }, include
|
|
|
1173
1182
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1174
1183
|
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.");
|
|
1175
1184
|
}
|
|
1185
|
+
if (reachedDatabase(e)) throw e;
|
|
1176
1186
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
1177
1187
|
}
|
|
1178
1188
|
}
|
|
@@ -1246,6 +1256,9 @@ relatedTo: hop }, include
|
|
|
1246
1256
|
const idInfo = idInfoArray[0];
|
|
1247
1257
|
const idField = table[idInfo.fieldName as keyof typeof table] as AnyPgColumn;
|
|
1248
1258
|
|
|
1259
|
+
// See `fetchOne`: an unaddressable id is a 404, not a database error.
|
|
1260
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return null;
|
|
1261
|
+
|
|
1249
1262
|
const parsedIdObj = parseIdValues(id, idInfoArray);
|
|
1250
1263
|
const parsedId = parsedIdObj[idInfo.fieldName];
|
|
1251
1264
|
|
|
@@ -1279,6 +1292,7 @@ relatedTo: hop }, include
|
|
|
1279
1292
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
1280
1293
|
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.");
|
|
1281
1294
|
}
|
|
1295
|
+
if (reachedDatabase(e)) throw e;
|
|
1282
1296
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
1283
1297
|
}
|
|
1284
1298
|
}
|
|
@@ -7,9 +7,9 @@ import { logger } from "@rebasepro/server";
|
|
|
7
7
|
// Row identity is derived on both sides of the wire — the driver parses an
|
|
8
8
|
// incoming address into key columns, the admin derives one from a served row —
|
|
9
9
|
// so the implementation lives in `common` and both agree by construction.
|
|
10
|
-
export { buildCompositeId, parseIdValues, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
|
|
10
|
+
export { buildCompositeId, parseIdValues, isAddressableId, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
|
|
11
11
|
export type { PrimaryKeyInfo } from "@rebasepro/common";
|
|
12
|
-
import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys } from "@rebasepro/common";
|
|
12
|
+
import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys, isAddressableId } from "@rebasepro/common";
|
|
13
13
|
import type { PrimaryKeyInfo } from "@rebasepro/common";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -40,6 +40,33 @@ export function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether an address could name a row in this table, judged by the columns.
|
|
45
|
+
*
|
|
46
|
+
* {@link getPrimaryKeys} lets a config's `isId: "uuid"` win over the schema, so
|
|
47
|
+
* its `isUUID` is a claim rather than a fact — right for deriving addresses,
|
|
48
|
+
* wrong for refusing a query. Here the Drizzle column type decides, because it
|
|
49
|
+
* is what Postgres will enforce: a `uuid` column meets `/c/products/new` with
|
|
50
|
+
* `22P02`, which aborts the surrounding transaction and turns every later
|
|
51
|
+
* statement into an unrelated-looking `25P02`.
|
|
52
|
+
*/
|
|
53
|
+
export function idCanAddressTable(
|
|
54
|
+
id: string | number,
|
|
55
|
+
table: PgTable,
|
|
56
|
+
idInfoArray: PrimaryKeyInfo[]
|
|
57
|
+
): boolean {
|
|
58
|
+
const columnBacked = idInfoArray.map(info => {
|
|
59
|
+
const col = table[info.fieldName as keyof typeof table] as AnyPgColumn | undefined;
|
|
60
|
+
const meta = col ? getColumnMeta(col) : undefined;
|
|
61
|
+
// No column to ask (a key the schema does not carry) leaves the id
|
|
62
|
+
// addressable: refusing on a guess would 404 rows that do exist.
|
|
63
|
+
if (!meta?.columnType) return info;
|
|
64
|
+
return { ...info,
|
|
65
|
+
isUUID: meta.columnType === "PgUUID" };
|
|
66
|
+
});
|
|
67
|
+
return isAddressableId(id, columnBacked);
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
export function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig {
|
|
44
71
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
45
72
|
if (!collection) {
|
|
@@ -79,6 +79,25 @@ export function extractPgError(error: unknown): PostgresError | null {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether the failure came back from Postgres rather than from building the
|
|
84
|
+
* query — which decides whether a fallback query is worth issuing.
|
|
85
|
+
*
|
|
86
|
+
* Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
|
|
87
|
+
* RLS). Once a statement raises, that transaction is aborted, and every later
|
|
88
|
+
* statement on it returns `25P02` — "current transaction is aborted, commands
|
|
89
|
+
* ignored until end of transaction block". So a retry after a database error
|
|
90
|
+
* cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
|
|
91
|
+
* for type uuid") with a generic one. Rethrow instead.
|
|
92
|
+
*
|
|
93
|
+
* A query the driver could not even build — a missing reciprocal relation, say
|
|
94
|
+
* — never reached Postgres, leaves the transaction usable, and is exactly what
|
|
95
|
+
* the fallback paths exist for.
|
|
96
|
+
*/
|
|
97
|
+
export function reachedDatabase(error: unknown): boolean {
|
|
98
|
+
return extractPgError(error) !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* Walk the error cause chain and return the deepest meaningful message.
|
|
84
103
|
*/
|