@rebasepro/server-postgres 0.12.1-canary.gf4240e3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 { C as updateDateAutoValues, D as mergeDeep, E as getPolicyNamesForRule, M as hasForeignKeyOnTarget, N as isManyToMany, O as camelCase, P as Vector, S as normalizeToEntityRelation, _ as buildCompositeId, a as getJunctionSecurityRules, b as createRelationRef, c as policyToPostgres, d as findRelation, f as getColumnName, g as resolveCollectionRelations, h as getTableVarName, i as getJunctionCollectionConfig, k as toSnakeCase, l as securityRuleToConditions, m as getTableName$1, n as CollectionRegistry, o as resolveJunctionSpecs, p as getEnumVarName, r as resolveStringColumnLength, s as getEffectiveSecurityRules, t as buildSdkData, u as findAnonymousGrants, v as getDeclaredPrimaryKeys, w as generateForeignKeyName, x as createRelationRefWithData, y as parseIdValues } from "./src-CzbghKwf.js";
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.
@@ -4220,19 +4265,34 @@ function validateIdentifier(value, label) {
4220
4265
  if (!/^[a-zA-Z0-9_-]+$/.test(value)) throw new Error(`Invalid ${label}: only letters, digits, underscores, and hyphens are allowed.`);
4221
4266
  }
4222
4267
  /**
4223
- * Sanitize a user-provided branch name to a safe PostgreSQL identifier.
4224
- * Only allows alphanumeric characters and underscores.
4268
+ * Postgres truncates identifiers at NAMEDATALEN-1 = 63 bytes, and does it
4269
+ * silently. The prefix comes out of the same budget.
4225
4270
  */
4226
- function sanitizeBranchName(name) {
4227
- return name.replace(/[^a-zA-Z0-9_]/g, "");
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.
4285
+ */
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.`);
4228
4289
  }
4229
4290
  /**
4230
4291
  * Convert a user-facing branch name to the actual PostgreSQL database name.
4231
4292
  */
4232
4293
  function toBranchDbName(name) {
4233
- const sanitized = sanitizeBranchName(name);
4234
- if (!sanitized) throw new Error("Branch name must contain at least one alphanumeric character.");
4235
- return `${BRANCH_DB_PREFIX}${sanitized}`;
4294
+ assertValidBranchName(name);
4295
+ return `${BRANCH_DB_PREFIX}${name}`;
4236
4296
  }
4237
4297
  var BranchService = class {
4238
4298
  db;
@@ -4269,9 +4329,8 @@ var BranchService = class {
4269
4329
  async createBranch(name, options) {
4270
4330
  if (options?.source) validateIdentifier(options.source, "source database name");
4271
4331
  const dbName = toBranchDbName(name);
4272
- const sanitizedName = sanitizeBranchName(name);
4273
4332
  const sourceDb = options?.source || this.poolManager.defaultDatabaseName;
4274
- if ((await this.db.execute(sql`SELECT name FROM rebase.branches WHERE name = ${sanitizedName} OR db_name = ${dbName}`)).rows.length > 0) throw new Error(`Branch "${sanitizedName}" already exists.`);
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.`);
4275
4334
  await this.poolManager.disconnectDatabase(sourceDb);
4276
4335
  const safeDbName = dbName.replace(/"/g, "\"\"");
4277
4336
  const safeSourceDb = sourceDb.replace(/"/g, "\"\"");
@@ -4282,10 +4341,10 @@ var BranchService = class {
4282
4341
  throw describeBranchDdlError(err, dbName);
4283
4342
  }
4284
4343
  const now = /* @__PURE__ */ new Date();
4285
- await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
4286
- VALUES (${sanitizedName}, ${dbName}, ${sourceDb}, ${now.toISOString()})`);
4344
+ await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
4345
+ VALUES (${name}, ${dbName}, ${sourceDb}, ${now.toISOString()})`);
4287
4346
  return {
4288
- name: sanitizedName,
4347
+ name,
4289
4348
  parentDatabase: sourceDb,
4290
4349
  createdAt: now
4291
4350
  };
@@ -4295,19 +4354,21 @@ var BranchService = class {
4295
4354
  * Cannot delete the main/default database.
4296
4355
  */
4297
4356
  async deleteBranch(name) {
4298
- const sanitizedName = sanitizeBranchName(name);
4299
- const dbName = toBranchDbName(name);
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;
4300
4362
  if (dbName === this.poolManager.defaultDatabaseName) throw new Error("Cannot delete the main database.");
4301
- 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.`);
4302
4363
  await this.poolManager.disconnectDatabase(dbName);
4303
4364
  const safeDbName = dbName.replace(/"/g, "\"\"");
4304
4365
  try {
4305
4366
  await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
4306
4367
  } catch (err) {
4307
- if (extractPgError(err)?.code === PG_OBJECT_IN_USE) throw new Error(`Cannot delete branch "${sanitizedName}": the database has active connections. Close other clients and try again.`);
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.`);
4308
4369
  throw describeBranchDdlError(err, dbName);
4309
4370
  }
4310
- await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${sanitizedName}`);
4371
+ await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${name}`);
4311
4372
  }
4312
4373
  /**
4313
4374
  * List all branches recorded in the metadata table.
@@ -4334,20 +4395,21 @@ var BranchService = class {
4334
4395
  * Get info about a specific branch.
4335
4396
  */
4336
4397
  async getBranchInfo(name) {
4337
- const sanitizedName = sanitizeBranchName(name);
4398
+ assertValidBranchName(name);
4338
4399
  const rows = (await this.db.execute(sql`
4339
- SELECT
4400
+ SELECT
4340
4401
  b.name,
4402
+ b.db_name,
4341
4403
  b.parent_db,
4342
4404
  b.created_at
4343
4405
  FROM rebase.branches b
4344
- WHERE b.name = ${sanitizedName}
4406
+ WHERE b.name = ${name}
4345
4407
  `)).rows;
4346
4408
  if (rows.length === 0) return void 0;
4347
4409
  const row = rows[0];
4348
4410
  let sizeBytes;
4349
4411
  try {
4350
- const dbName = toBranchDbName(sanitizedName);
4412
+ const dbName = row.db_name;
4351
4413
  const sizeRows = (await this.db.execute(sql`SELECT pg_database_size(${dbName}) as size_bytes`)).rows;
4352
4414
  if (sizeRows.length > 0 && sizeRows[0].size_bytes != null) sizeBytes = Number(sizeRows[0].size_bytes);
4353
4415
  } catch {}
@@ -5946,6 +6008,7 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
5946
6008
  const isId = isIdProperty(propName, prop, collection);
5947
6009
  let baseType = numProp.validation?.integer || isId ? `integer("${colName}")` : `numeric("${colName}")`;
5948
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" })`;
5949
6012
  else baseType = `${numProp.columnType}("${colName}")`;
5950
6013
  if ("isId" in numProp && numProp.isId === "increment") columnDefinition = `${baseType}.generatedByDefaultAsIdentity()`;
5951
6014
  else if ("isId" in numProp && typeof numProp.isId === "string" && numProp.isId !== "manual") {
@@ -12225,7 +12288,7 @@ function createPostgresBootstrapper(pgConfig) {
12225
12288
  */
12226
12289
  async ensureCollectionSchema(collections, driverResult, log) {
12227
12290
  const internals = driverResult.internals;
12228
- const { ensureCollectionTables } = await import("./ensure-collection-tables-Da2oGkX2.js");
12291
+ const { ensureCollectionTables } = await import("./ensure-collection-tables-CBQdOETu.js");
12229
12292
  const plan = await ensureCollectionTables({ async query(text) {
12230
12293
  const result = await internals.db.execute(sql.raw(text));
12231
12294
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
@@ -12250,7 +12313,7 @@ function createPostgresBootstrapper(pgConfig) {
12250
12313
  */
12251
12314
  async ensureCollectionPolicies(collections, driverResult, log) {
12252
12315
  const internals = driverResult.internals;
12253
- const { ensureCollectionPolicies } = await import("./ensure-collection-policies-BrUVgjz3.js");
12316
+ const { ensureCollectionPolicies } = await import("./ensure-collection-policies-ViG8XiPn.js");
12254
12317
  const outcome = await ensureCollectionPolicies({ async query(text) {
12255
12318
  const result = await internals.db.execute(sql.raw(text));
12256
12319
  return { rows: result.rows ?? (Array.isArray(result) ? result : []) };