@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
package/dist/index.es.js
CHANGED
|
@@ -2,7 +2,7 @@ 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";
|
|
@@ -228,6 +228,27 @@ function getColumnMeta(col) {
|
|
|
228
228
|
primary: typeof raw.primary === "boolean" ? raw.primary : void 0
|
|
229
229
|
};
|
|
230
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
|
+
}
|
|
231
252
|
function getCollectionByPath(collectionPath, registry) {
|
|
232
253
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
233
254
|
if (!collection) {
|
|
@@ -2772,6 +2793,247 @@ function assertWritableThrough(hop, path) {
|
|
|
2772
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");
|
|
2773
2794
|
}
|
|
2774
2795
|
//#endregion
|
|
2796
|
+
//#region src/utils/pg-error-utils.ts
|
|
2797
|
+
/**
|
|
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.
|
|
2804
|
+
*/
|
|
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;
|
|
2828
|
+
}
|
|
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;
|
|
2861
|
+
}
|
|
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
|
|
2933
|
+
};
|
|
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
|
|
2775
3037
|
//#region src/services/FetchService.ts
|
|
2776
3038
|
/**
|
|
2777
3039
|
* Service for handling all row read operations.
|
|
@@ -3086,6 +3348,7 @@ var FetchService = class {
|
|
|
3086
3348
|
const idInfo = idInfoArray[0];
|
|
3087
3349
|
const idField = table[idInfo.fieldName];
|
|
3088
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;
|
|
3089
3352
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
3090
3353
|
const tableName = getTableName(table);
|
|
3091
3354
|
const qb = this.getQueryBuilder(tableName);
|
|
@@ -3104,6 +3367,7 @@ var FetchService = class {
|
|
|
3104
3367
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3105
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.");
|
|
3106
3369
|
}
|
|
3370
|
+
if (reachedDatabase(e)) throw e;
|
|
3107
3371
|
logger.warn(`[FetchService] db.query.findFirst failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
3108
3372
|
}
|
|
3109
3373
|
const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
|
|
@@ -3157,6 +3421,7 @@ var FetchService = class {
|
|
|
3157
3421
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3158
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.");
|
|
3159
3423
|
}
|
|
3424
|
+
if (reachedDatabase(e)) throw e;
|
|
3160
3425
|
logger.warn(`[FetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select`, { error: e });
|
|
3161
3426
|
}
|
|
3162
3427
|
let vectorMeta;
|
|
@@ -3376,6 +3641,7 @@ var FetchService = class {
|
|
|
3376
3641
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3377
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.");
|
|
3378
3643
|
}
|
|
3644
|
+
if (reachedDatabase(e)) throw e;
|
|
3379
3645
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
3380
3646
|
}
|
|
3381
3647
|
const rows = await this.fetchRowsWithConditionsRaw(collectionPath, options);
|
|
@@ -3421,6 +3687,7 @@ var FetchService = class {
|
|
|
3421
3687
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
3422
3688
|
const idInfo = idInfoArray[0];
|
|
3423
3689
|
const idField = table[idInfo.fieldName];
|
|
3690
|
+
if (!idCanAddressTable(id, table, idInfoArray)) return null;
|
|
3424
3691
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
3425
3692
|
const tableName = getTableName(table);
|
|
3426
3693
|
const qb = this.getQueryBuilder(tableName);
|
|
@@ -3439,6 +3706,7 @@ var FetchService = class {
|
|
|
3439
3706
|
logger.error(`[FetchService] ResolvedRelation inference error for collection '${collectionPath}': ${e.message}`);
|
|
3440
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.");
|
|
3441
3708
|
}
|
|
3709
|
+
if (reachedDatabase(e)) throw e;
|
|
3442
3710
|
logger.warn(`[fetchOneForRest] db.query.findFirst failed for ${collectionPath}, falling back`, { error: e });
|
|
3443
3711
|
}
|
|
3444
3712
|
const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
|
|
@@ -3597,229 +3865,6 @@ var FetchService = class {
|
|
|
3597
3865
|
}
|
|
3598
3866
|
};
|
|
3599
3867
|
//#endregion
|
|
3600
|
-
//#region src/utils/pg-error-utils.ts
|
|
3601
|
-
/**
|
|
3602
|
-
* Shared PostgreSQL error extraction and user-friendly message formatting.
|
|
3603
|
-
*
|
|
3604
|
-
* Drizzle wraps native PG errors in a `.cause` chain. These utilities
|
|
3605
|
-
* unwrap that chain to get the real PostgreSQL error (identified by a
|
|
3606
|
-
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
3607
|
-
* a message that is safe and helpful to show to end-users.
|
|
3608
|
-
*/
|
|
3609
|
-
/**
|
|
3610
|
-
* Return the error when it is a deliberate 4xx, otherwise null.
|
|
3611
|
-
*
|
|
3612
|
-
* A thrown `ApiError` is a decision the server made about the request, not a
|
|
3613
|
-
* database failure: its message and code are already written for the client.
|
|
3614
|
-
*/
|
|
3615
|
-
function asClientFacingError(error) {
|
|
3616
|
-
if (!(error instanceof Error)) return null;
|
|
3617
|
-
const e = error;
|
|
3618
|
-
if (typeof e.statusCode !== "number" || e.statusCode < 400 || e.statusCode >= 500) return null;
|
|
3619
|
-
return e;
|
|
3620
|
-
}
|
|
3621
|
-
/**
|
|
3622
|
-
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
3623
|
-
* Drizzle wraps PG errors in a `cause` property — this function
|
|
3624
|
-
* recursively walks the chain until it finds an object with a PG
|
|
3625
|
-
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
3626
|
-
*/
|
|
3627
|
-
function extractPgError(error) {
|
|
3628
|
-
if (!error || typeof error !== "object") return null;
|
|
3629
|
-
if (!(error instanceof Error)) {
|
|
3630
|
-
if ("cause" in error && error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
3631
|
-
return null;
|
|
3632
|
-
}
|
|
3633
|
-
if ("code" in error && typeof error.code === "string" && /^[0-9A-Z]{5}$/.test(error.code)) return error;
|
|
3634
|
-
if (error.cause && typeof error.cause === "object") return extractPgError(error.cause);
|
|
3635
|
-
return null;
|
|
3636
|
-
}
|
|
3637
|
-
/**
|
|
3638
|
-
* Walk the error cause chain and return the deepest meaningful message.
|
|
3639
|
-
*/
|
|
3640
|
-
function extractCauseMessage(error) {
|
|
3641
|
-
if (!error || typeof error !== "object") return null;
|
|
3642
|
-
if (!(error instanceof Error)) return null;
|
|
3643
|
-
if (error.cause && typeof error.cause === "object") {
|
|
3644
|
-
const deeper = extractCauseMessage(error.cause);
|
|
3645
|
-
if (deeper) return deeper;
|
|
3646
|
-
if (error.cause instanceof Error && error.cause.message) return error.cause.message;
|
|
3647
|
-
}
|
|
3648
|
-
return null;
|
|
3649
|
-
}
|
|
3650
|
-
/**
|
|
3651
|
-
* Codes that mean "this connection will never work as configured".
|
|
3652
|
-
*
|
|
3653
|
-
* A wrong password or a database that does not exist is a settled fact about
|
|
3654
|
-
* the connection string, not a transient fault — retrying produces the same
|
|
3655
|
-
* answer forever.
|
|
3656
|
-
*/
|
|
3657
|
-
var UNRECOVERABLE_CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
3658
|
-
"28P01",
|
|
3659
|
-
"28000",
|
|
3660
|
-
"3D000",
|
|
3661
|
-
"42501"
|
|
3662
|
-
]);
|
|
3663
|
-
/**
|
|
3664
|
-
* Describe a failed connection attempt in terms a developer can act on.
|
|
3665
|
-
*
|
|
3666
|
-
* The error a caller catches is Drizzle's wrapper: its message is
|
|
3667
|
-
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
3668
|
-
* the sentence that says what is actually wrong — "password authentication
|
|
3669
|
-
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
3670
|
-
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
3671
|
-
* their `DATABASE_URL` nothing at all.
|
|
3672
|
-
*/
|
|
3673
|
-
function classifyConnectFailure(error) {
|
|
3674
|
-
const pgError = extractPgError(error);
|
|
3675
|
-
const reason = pgError?.message ?? extractCauseMessage(error) ?? (error instanceof Error ? error.message : String(error));
|
|
3676
|
-
return {
|
|
3677
|
-
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
3678
|
-
reason,
|
|
3679
|
-
code: pgError?.code
|
|
3680
|
-
};
|
|
3681
|
-
}
|
|
3682
|
-
/**
|
|
3683
|
-
* Detect whether an error is specifically a role-switching permission failure
|
|
3684
|
-
* (e.g. "permission denied to set role" or "must be member of role"),
|
|
3685
|
-
* as opposed to a table-level permission denial.
|
|
3686
|
-
*
|
|
3687
|
-
* This is used by the backend driver to auto-disable role switching when the
|
|
3688
|
-
* connection user lacks SET ROLE privileges, rather than surfacing a confusing
|
|
3689
|
-
* error to the Studio SQL Editor user.
|
|
3690
|
-
*/
|
|
3691
|
-
function isRoleSwitchingPermissionError(error) {
|
|
3692
|
-
const pgError = extractPgError(error);
|
|
3693
|
-
if (!pgError || pgError.code !== "42501") return false;
|
|
3694
|
-
const msg = pgError.message.toLowerCase();
|
|
3695
|
-
return msg.includes("set role") || msg.includes("member of role");
|
|
3696
|
-
}
|
|
3697
|
-
/**
|
|
3698
|
-
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
3699
|
-
*
|
|
3700
|
-
* @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
|
|
3701
|
-
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
3702
|
-
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
3703
|
-
*/
|
|
3704
|
-
function pgErrorToFriendlyMessage(pgError, context) {
|
|
3705
|
-
const detail = pgError.detail;
|
|
3706
|
-
const hint = pgError.hint;
|
|
3707
|
-
const constraint = pgError.constraint;
|
|
3708
|
-
const column = pgError.column;
|
|
3709
|
-
const table = pgError.table;
|
|
3710
|
-
const dataType = pgError.dataType;
|
|
3711
|
-
const pgMessage = pgError.message || "Unknown database error";
|
|
3712
|
-
const code = pgError.code || "UNKNOWN";
|
|
3713
|
-
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
3714
|
-
const tableRef = table ?? context;
|
|
3715
|
-
switch (pgError.code) {
|
|
3716
|
-
case "23503": return {
|
|
3717
|
-
message: detail ? `Foreign key constraint violated: ${detail}${suffix}` : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3718
|
-
code
|
|
3719
|
-
};
|
|
3720
|
-
case "23505": return {
|
|
3721
|
-
message: detail ? `Duplicate value: ${detail}${suffix}` : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3722
|
-
code
|
|
3723
|
-
};
|
|
3724
|
-
case "23502": return {
|
|
3725
|
-
message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
|
|
3726
|
-
code
|
|
3727
|
-
};
|
|
3728
|
-
case "23514": return {
|
|
3729
|
-
message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
3730
|
-
code
|
|
3731
|
-
};
|
|
3732
|
-
case "22P02": return {
|
|
3733
|
-
message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
|
|
3734
|
-
code
|
|
3735
|
-
};
|
|
3736
|
-
case "22001": return {
|
|
3737
|
-
message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
3738
|
-
code
|
|
3739
|
-
};
|
|
3740
|
-
case "22003": return {
|
|
3741
|
-
message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
3742
|
-
code
|
|
3743
|
-
};
|
|
3744
|
-
case "42703": return {
|
|
3745
|
-
message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
3746
|
-
code
|
|
3747
|
-
};
|
|
3748
|
-
case "42P01": return {
|
|
3749
|
-
message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
3750
|
-
code
|
|
3751
|
-
};
|
|
3752
|
-
case "42501": return {
|
|
3753
|
-
message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
|
|
3754
|
-
code
|
|
3755
|
-
};
|
|
3756
|
-
case "28000": return {
|
|
3757
|
-
message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
|
|
3758
|
-
code
|
|
3759
|
-
};
|
|
3760
|
-
default: {
|
|
3761
|
-
const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
|
|
3762
|
-
if (detail) parts.push(`Detail: ${detail}`);
|
|
3763
|
-
if (column) parts.push(`Column: ${column}`);
|
|
3764
|
-
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
3765
|
-
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
3766
|
-
if (hint) parts.push(`Hint: ${hint}`);
|
|
3767
|
-
return {
|
|
3768
|
-
message: parts.join(". "),
|
|
3769
|
-
code
|
|
3770
|
-
};
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
/**
|
|
3775
|
-
* Sanitize any error into a message safe and helpful for the client.
|
|
3776
|
-
*
|
|
3777
|
-
* A deliberate 4xx (`ApiError`) passes through untouched — the server already
|
|
3778
|
-
* decided what the client should read. Otherwise the PG error is extracted
|
|
3779
|
-
* from the Drizzle cause chain, falling back to a generic message that
|
|
3780
|
-
* doesn't leak SQL.
|
|
3781
|
-
*
|
|
3782
|
-
* @param error - The raw caught error
|
|
3783
|
-
* @param context - A human-readable context string (e.g. collection path)
|
|
3784
|
-
* @returns An object with `message` (user-friendly) and optional `code`
|
|
3785
|
-
* (the `ApiError` code, or the PG SQLSTATE).
|
|
3786
|
-
*/
|
|
3787
|
-
function sanitizeErrorForClient(error, context) {
|
|
3788
|
-
const clientError = asClientFacingError(error);
|
|
3789
|
-
if (clientError) {
|
|
3790
|
-
const line = `[API ${clientError.statusCode} ${clientError.code ?? "BAD_REQUEST"}] in "${context}": ${clientError.message}`;
|
|
3791
|
-
if (clientError.expected) logger.debug(line);
|
|
3792
|
-
else logger.warn(`⚠️ ${line}`);
|
|
3793
|
-
return {
|
|
3794
|
-
message: clientError.message,
|
|
3795
|
-
...clientError.code && { code: clientError.code }
|
|
3796
|
-
};
|
|
3797
|
-
}
|
|
3798
|
-
const pgError = extractPgError(error);
|
|
3799
|
-
if (pgError) {
|
|
3800
|
-
logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
|
|
3801
|
-
code: pgError.code,
|
|
3802
|
-
message: pgError.message,
|
|
3803
|
-
detail: pgError.detail,
|
|
3804
|
-
hint: pgError.hint,
|
|
3805
|
-
column: pgError.column,
|
|
3806
|
-
table: pgError.table,
|
|
3807
|
-
constraint: pgError.constraint,
|
|
3808
|
-
dataType: pgError.dataType,
|
|
3809
|
-
drizzleMessage: error instanceof Error ? error.message : String(error)
|
|
3810
|
-
});
|
|
3811
|
-
return pgErrorToFriendlyMessage(pgError, context);
|
|
3812
|
-
}
|
|
3813
|
-
logger.error(`Database error in "${context}" (no PG error extracted)`, {
|
|
3814
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3815
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
3816
|
-
cause: error instanceof Error && error.cause ? error.cause instanceof Error ? error.cause.message : String(error.cause) : void 0
|
|
3817
|
-
});
|
|
3818
|
-
const causeMessage = extractCauseMessage(error);
|
|
3819
|
-
if (causeMessage) return { message: `Database error in "${context}": ${causeMessage}` };
|
|
3820
|
-
return { message: `Could not load data for "${context}". Check server logs for details.` };
|
|
3821
|
-
}
|
|
3822
|
-
//#endregion
|
|
3823
3868
|
//#region src/services/PersistService.ts
|
|
3824
3869
|
/**
|
|
3825
3870
|
* Service for handling all row write operations.
|
|
@@ -5946,6 +5991,7 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5946
5991
|
const isId = isIdProperty(propName, prop, collection);
|
|
5947
5992
|
let baseType = numProp.validation?.integer || isId ? `integer("${colName}")` : `numeric("${colName}")`;
|
|
5948
5993
|
if (numProp.columnType) if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
|
|
5994
|
+
else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") baseType = `${numProp.columnType}("${colName}", { mode: "number" })`;
|
|
5949
5995
|
else baseType = `${numProp.columnType}("${colName}")`;
|
|
5950
5996
|
if ("isId" in numProp && numProp.isId === "increment") columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
|
|
5951
5997
|
else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
|
|
@@ -12225,7 +12271,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12225
12271
|
*/
|
|
12226
12272
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
12227
12273
|
const internals = driverResult.internals;
|
|
12228
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
12274
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-CBQdOETu.js");
|
|
12229
12275
|
const plan = await ensureCollectionTables({ async query(text) {
|
|
12230
12276
|
const result = await internals.db.execute(sql.raw(text));
|
|
12231
12277
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|
|
@@ -12250,7 +12296,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
12250
12296
|
*/
|
|
12251
12297
|
async ensureCollectionPolicies(collections, driverResult, log) {
|
|
12252
12298
|
const internals = driverResult.internals;
|
|
12253
|
-
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-
|
|
12299
|
+
const { ensureCollectionPolicies } = await import("./ensure-collection-policies-ViG8XiPn.js");
|
|
12254
12300
|
const outcome = await ensureCollectionPolicies({ async query(text) {
|
|
12255
12301
|
const result = await internals.db.execute(sql.raw(text));
|
|
12256
12302
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|