@rebasepro/server-postgres 0.0.1-canary.4829d6e

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.
Files changed (121) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +86 -0
  3. package/dist/PostgresAdapter.d.ts +6 -0
  4. package/dist/PostgresBackendDriver.d.ts +150 -0
  5. package/dist/PostgresBootstrapper.d.ts +51 -0
  6. package/dist/auth/ensure-tables.d.ts +10 -0
  7. package/dist/auth/services.d.ts +250 -0
  8. package/dist/backup/backup-cli.d.ts +3 -0
  9. package/dist/backup/backup-cron.d.ts +53 -0
  10. package/dist/backup/backup-service.d.ts +85 -0
  11. package/dist/backup/index.d.ts +12 -0
  12. package/dist/backup/pg-tools.d.ts +110 -0
  13. package/dist/backup/retention.d.ts +35 -0
  14. package/dist/chunk-DSJWtz9O.js +40 -0
  15. package/dist/cli-errors.d.ts +42 -0
  16. package/dist/cli-helpers.d.ts +7 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/collections/PostgresCollectionRegistry.d.ts +47 -0
  19. package/dist/connection.d.ts +65 -0
  20. package/dist/data-transformer.d.ts +55 -0
  21. package/dist/databasePoolManager.d.ts +20 -0
  22. package/dist/history/HistoryService.d.ts +71 -0
  23. package/dist/history/ensure-history-table.d.ts +7 -0
  24. package/dist/index.d.ts +15 -0
  25. package/dist/index.es.js +23535 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/interfaces.d.ts +18 -0
  28. package/dist/schema/auth-bootstrap-sql.d.ts +24 -0
  29. package/dist/schema/auth-default-policies.d.ts +10 -0
  30. package/dist/schema/auth-schema.d.ts +2376 -0
  31. package/dist/schema/doctor-cli.d.ts +2 -0
  32. package/dist/schema/doctor.d.ts +58 -0
  33. package/dist/schema/dynamic-tables.d.ts +31 -0
  34. package/dist/schema/generate-drizzle-schema-logic.d.ts +2 -0
  35. package/dist/schema/generate-drizzle-schema.d.ts +1 -0
  36. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -0
  37. package/dist/schema/generate-postgres-ddl.d.ts +1 -0
  38. package/dist/schema/introspect-db-inference.d.ts +5 -0
  39. package/dist/schema/introspect-db-logic.d.ts +118 -0
  40. package/dist/schema/introspect-db.d.ts +1 -0
  41. package/dist/schema/introspect-runtime.d.ts +57 -0
  42. package/dist/schema/test-schema.d.ts +24 -0
  43. package/dist/security/policy-drift.d.ts +57 -0
  44. package/dist/security/rls-enforcement.d.ts +122 -0
  45. package/dist/services/BranchService.d.ts +47 -0
  46. package/dist/services/FetchService.d.ts +214 -0
  47. package/dist/services/PersistService.d.ts +39 -0
  48. package/dist/services/RelationService.d.ts +109 -0
  49. package/dist/services/cdc/CdcListener.d.ts +54 -0
  50. package/dist/services/cdc/trigger-cdc.d.ts +64 -0
  51. package/dist/services/collection-helpers.d.ts +38 -0
  52. package/dist/services/dataService.d.ts +110 -0
  53. package/dist/services/index.d.ts +4 -0
  54. package/dist/services/realtimeService.d.ts +298 -0
  55. package/dist/src-Eh-CZosp.js +595 -0
  56. package/dist/src-Eh-CZosp.js.map +1 -0
  57. package/dist/types.d.ts +3 -0
  58. package/dist/utils/drizzle-conditions.d.ts +138 -0
  59. package/dist/utils/pg-array-null-patch.d.ts +16 -0
  60. package/dist/utils/pg-error-utils.d.ts +65 -0
  61. package/dist/utils/table-classification.d.ts +8 -0
  62. package/dist/websocket.d.ts +18 -0
  63. package/package.json +113 -0
  64. package/src/PostgresAdapter.ts +58 -0
  65. package/src/PostgresBackendDriver.ts +1387 -0
  66. package/src/PostgresBootstrapper.ts +581 -0
  67. package/src/auth/ensure-tables.ts +367 -0
  68. package/src/auth/services.ts +1321 -0
  69. package/src/backup/backup-cli.ts +383 -0
  70. package/src/backup/backup-cron.ts +189 -0
  71. package/src/backup/backup-service.ts +299 -0
  72. package/src/backup/index.ts +12 -0
  73. package/src/backup/pg-tools.ts +231 -0
  74. package/src/backup/retention.ts +75 -0
  75. package/src/cli-errors.ts +265 -0
  76. package/src/cli-helpers.ts +196 -0
  77. package/src/cli.ts +786 -0
  78. package/src/collections/PostgresCollectionRegistry.ts +103 -0
  79. package/src/connection.ts +166 -0
  80. package/src/data-transformer.ts +733 -0
  81. package/src/databasePoolManager.ts +87 -0
  82. package/src/history/HistoryService.ts +272 -0
  83. package/src/history/ensure-history-table.ts +46 -0
  84. package/src/index.ts +15 -0
  85. package/src/interfaces.ts +60 -0
  86. package/src/schema/auth-bootstrap-sql.ts +41 -0
  87. package/src/schema/auth-default-policies.ts +125 -0
  88. package/src/schema/auth-schema.ts +233 -0
  89. package/src/schema/doctor-cli.ts +113 -0
  90. package/src/schema/doctor.ts +733 -0
  91. package/src/schema/dynamic-tables.test.ts +302 -0
  92. package/src/schema/dynamic-tables.ts +293 -0
  93. package/src/schema/generate-drizzle-schema-logic.ts +850 -0
  94. package/src/schema/generate-drizzle-schema.ts +131 -0
  95. package/src/schema/generate-postgres-ddl-logic.ts +490 -0
  96. package/src/schema/generate-postgres-ddl.ts +92 -0
  97. package/src/schema/introspect-db-inference.ts +238 -0
  98. package/src/schema/introspect-db-logic.ts +910 -0
  99. package/src/schema/introspect-db.ts +266 -0
  100. package/src/schema/introspect-runtime.test.ts +212 -0
  101. package/src/schema/introspect-runtime.ts +293 -0
  102. package/src/schema/test-schema.ts +11 -0
  103. package/src/security/policy-drift.test.ts +122 -0
  104. package/src/security/policy-drift.ts +159 -0
  105. package/src/security/rls-enforcement.ts +295 -0
  106. package/src/services/BranchService.ts +251 -0
  107. package/src/services/FetchService.ts +1661 -0
  108. package/src/services/PersistService.ts +329 -0
  109. package/src/services/RelationService.ts +1306 -0
  110. package/src/services/cdc/CdcListener.ts +167 -0
  111. package/src/services/cdc/trigger-cdc.ts +169 -0
  112. package/src/services/collection-helpers.ts +151 -0
  113. package/src/services/dataService.ts +246 -0
  114. package/src/services/index.ts +13 -0
  115. package/src/services/realtimeService.ts +1502 -0
  116. package/src/types.ts +4 -0
  117. package/src/utils/drizzle-conditions.ts +1162 -0
  118. package/src/utils/pg-array-null-patch.ts +42 -0
  119. package/src/utils/pg-error-utils.ts +227 -0
  120. package/src/utils/table-classification.ts +16 -0
  121. package/src/websocket.ts +640 -0
@@ -0,0 +1,42 @@
1
+ import { getTableColumns } from "drizzle-orm";
2
+ import { PgArray, PgTable } from "drizzle-orm/pg-core";
3
+ import { logger } from "@rebasepro/server";
4
+
5
+ /**
6
+ * Patches all PgArray columns on the given tables to handle NULL values safely.
7
+ *
8
+ * Drizzle ORM's `PgArray.mapFromDriverValue` calls `value.map(...)` without
9
+ * guarding against `null`. When a PostgreSQL native array column (`text[]`,
10
+ * `integer[]`, etc.) contains NULL, the pg driver returns `null` in JavaScript,
11
+ * and `null.map(...)` throws `TypeError: value.map is not a function`.
12
+ *
13
+ * This function walks every column of every registered table and, for any
14
+ * `PgArray` column, wraps its `mapFromDriverValue` to return `null` when the
15
+ * database value is nullish.
16
+ *
17
+ * This is a workaround for a known Drizzle ORM issue. Should be removed once
18
+ * Drizzle handles nullable arrays natively.
19
+ */
20
+ export function patchPgArrayNullSafety(tables: Record<string, unknown>): void {
21
+ let patchedCount = 0;
22
+
23
+ for (const tableOrRelation of Object.values(tables)) {
24
+ if (!(tableOrRelation instanceof PgTable)) continue;
25
+
26
+ const columns = getTableColumns(tableOrRelation);
27
+ for (const column of Object.values(columns)) {
28
+ if (column instanceof PgArray) {
29
+ const original = column.mapFromDriverValue.bind(column);
30
+ column.mapFromDriverValue = function (value: unknown) {
31
+ if (value == null) return null;
32
+ return original(value as string | unknown[]);
33
+ };
34
+ patchedCount++;
35
+ }
36
+ }
37
+ }
38
+
39
+ if (patchedCount > 0) {
40
+ logger.debug(`[PgArray] Patched ${patchedCount} array column(s) for null-safety`);
41
+ }
42
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Shared PostgreSQL error extraction and user-friendly message formatting.
3
+ *
4
+ * Drizzle wraps native PG errors in a `.cause` chain. These utilities
5
+ * unwrap that chain to get the real PostgreSQL error (identified by a
6
+ * 5-character alphanumeric `code` such as `42P01`) and translate it into
7
+ * a message that is safe and helpful to show to end-users.
8
+ */
9
+
10
+ import { logger } from "@rebasepro/server";
11
+
12
+ /** Shape of PostgreSQL errors with diagnostic metadata. */
13
+ export interface PostgresError extends Error {
14
+ code?: string;
15
+ detail?: string;
16
+ hint?: string;
17
+ constraint?: string;
18
+ column?: string;
19
+ table?: string;
20
+ dataType?: string;
21
+ cause?: unknown;
22
+ }
23
+
24
+ /**
25
+ * Extract the underlying PostgreSQL error from a Drizzle wrapper.
26
+ * Drizzle wraps PG errors in a `cause` property — this function
27
+ * recursively walks the chain until it finds an object with a PG
28
+ * error code (5-char alphanumeric, e.g. `42P01`).
29
+ */
30
+ export function extractPgError(error: unknown): PostgresError | null {
31
+ if (!error || typeof error !== "object") return null;
32
+ if (!(error instanceof Error)) {
33
+ // Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)
34
+ if ("cause" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === "object") {
35
+ return extractPgError((error as Record<string, unknown>).cause);
36
+ }
37
+ return null;
38
+ }
39
+
40
+ // Check if the error itself has a PG error code
41
+ if ("code" in error && typeof (error as PostgresError).code === "string" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {
42
+ return error as PostgresError;
43
+ }
44
+
45
+ // Check the cause chain (Drizzle wraps PG errors)
46
+ if (error.cause && typeof error.cause === "object") {
47
+ return extractPgError(error.cause);
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Walk the error cause chain and return the deepest meaningful message.
55
+ */
56
+ export function extractCauseMessage(error: unknown): string | null {
57
+ if (!error || typeof error !== "object") return null;
58
+ if (!(error instanceof Error)) return null;
59
+
60
+ if (error.cause && typeof error.cause === "object") {
61
+ const deeper = extractCauseMessage(error.cause);
62
+ if (deeper) return deeper;
63
+ // The cause itself has a message
64
+ if (error.cause instanceof Error && error.cause.message) {
65
+ return error.cause.message;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Detect whether an error is specifically a role-switching permission failure
73
+ * (e.g. "permission denied to set role" or "must be member of role"),
74
+ * as opposed to a table-level permission denial.
75
+ *
76
+ * This is used by the backend driver to auto-disable role switching when the
77
+ * connection user lacks SET ROLE privileges, rather than surfacing a confusing
78
+ * error to the Studio SQL Editor user.
79
+ */
80
+ export function isRoleSwitchingPermissionError(error: unknown): boolean {
81
+ const pgError = extractPgError(error);
82
+ if (!pgError || pgError.code !== "42501") return false;
83
+ const msg = pgError.message.toLowerCase();
84
+ return msg.includes("set role") || msg.includes("member of role");
85
+ }
86
+
87
+ /**
88
+ * Translate a raw PostgreSQL error into a user-friendly message.
89
+ *
90
+ * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
91
+ * @param context - A human-readable context string (e.g. collection slug or path)
92
+ * @returns An object with a `message` safe for the client and the PG `code`.
93
+ */
94
+ export function pgErrorToFriendlyMessage(pgError: PostgresError, context: string): { message: string; code: string } {
95
+ const detail = pgError.detail as string | undefined;
96
+ const hint = pgError.hint as string | undefined;
97
+ const constraint = pgError.constraint as string | undefined;
98
+ const column = pgError.column as string | undefined;
99
+ const table = pgError.table as string | undefined;
100
+ const dataType = pgError.dataType as string | undefined;
101
+ const pgMessage = pgError.message || "Unknown database error";
102
+ const code = pgError.code || "UNKNOWN";
103
+
104
+ const suffix = hint ? ` Hint: ${hint}` : "";
105
+ const tableRef = table ?? context;
106
+
107
+ switch (pgError.code) {
108
+ case "23503": // foreign_key_violation
109
+ return {
110
+ message: detail
111
+ ? `Foreign key constraint violated: ${detail}${suffix}`
112
+ : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
113
+ code
114
+ };
115
+ case "23505": // unique_violation
116
+ return {
117
+ message: detail
118
+ ? `Duplicate value: ${detail}${suffix}`
119
+ : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
120
+ code
121
+ };
122
+ case "23502": // not_null_violation
123
+ return {
124
+ message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
125
+ code
126
+ };
127
+ case "23514": // check_violation
128
+ return {
129
+ message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
130
+ code
131
+ };
132
+ case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
133
+ return {
134
+ message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
135
+ code
136
+ };
137
+ case "22001": // string_data_right_truncation (value too long)
138
+ return {
139
+ message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
140
+ code
141
+ };
142
+ case "22003": // numeric_value_out_of_range
143
+ return {
144
+ message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
145
+ code
146
+ };
147
+ case "42703": // undefined_column
148
+ return {
149
+ message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
150
+ code
151
+ };
152
+ case "42P01": // undefined_table
153
+ return {
154
+ message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
155
+ code
156
+ };
157
+ case "42501": // insufficient_privilege
158
+ return {
159
+ message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
160
+ code
161
+ };
162
+ case "28000": // invalid_authorization_specification
163
+ return {
164
+ message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
165
+ code
166
+ };
167
+ default: {
168
+ // Unhandled PG code — still surface the actual database message
169
+ const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
170
+ if (detail) parts.push(`Detail: ${detail}`);
171
+ if (column) parts.push(`Column: ${column}`);
172
+ if (dataType) parts.push(`Data type: ${dataType}`);
173
+ if (constraint) parts.push(`Constraint: ${constraint}`);
174
+ if (hint) parts.push(`Hint: ${hint}`);
175
+ return { message: parts.join(". "), code };
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Sanitize any error into a message safe and helpful for the client.
182
+ *
183
+ * Extracts the PG error from the Drizzle cause chain when possible;
184
+ * falls back to a generic message that doesn't leak SQL.
185
+ *
186
+ * @param error - The raw caught error
187
+ * @param context - A human-readable context string (e.g. collection path)
188
+ * @returns An object with `message` (user-friendly) and optional `code` (PG code).
189
+ */
190
+ export function sanitizeErrorForClient(error: unknown, context: string): { message: string; code?: string } {
191
+ // ── Always log the full, unsanitized error server-side ──────────
192
+ const pgError = extractPgError(error);
193
+
194
+ if (pgError) {
195
+ logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
196
+ code: pgError.code,
197
+ message: pgError.message,
198
+ detail: pgError.detail,
199
+ hint: pgError.hint,
200
+ column: pgError.column,
201
+ table: pgError.table,
202
+ constraint: pgError.constraint,
203
+ dataType: pgError.dataType,
204
+ // Also log the outer Drizzle wrapper message for full context
205
+ drizzleMessage: error instanceof Error ? error.message : String(error)
206
+ });
207
+ return pgErrorToFriendlyMessage(pgError, context);
208
+ }
209
+
210
+ // No PG error found — log the raw error as-is
211
+ logger.error(`Database error in "${context}" (no PG error extracted)`, {
212
+ error: error instanceof Error ? error.message : String(error),
213
+ stack: error instanceof Error ? error.stack : undefined,
214
+ cause: error instanceof Error && error.cause
215
+ ? (error.cause instanceof Error ? error.cause.message : String(error.cause))
216
+ : undefined
217
+ });
218
+
219
+ // Try to get the deepest cause message
220
+ const causeMessage = extractCauseMessage(error);
221
+ if (causeMessage) {
222
+ return { message: `Database error in "${context}": ${causeMessage}` };
223
+ }
224
+
225
+ // Last resort — generic message, never leak raw SQL
226
+ return { message: `Could not load data for "${context}". Check server logs for details.` };
227
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Table Classification Utility
3
+ *
4
+ * Re-exports shared classification logic from @rebasepro/common.
5
+ * This module exists for backward compatibility — prefer importing directly
6
+ * from @rebasepro/common in new code.
7
+ */
8
+ export {
9
+ type TableCategory,
10
+ REBASE_INTERNAL_SCHEMAS,
11
+ REBASE_INTERNAL_PREFIXES,
12
+ classifyTable,
13
+ isRebaseInternalTable,
14
+ detectJunctionTables,
15
+ JUNCTION_TABLES_SQL,
16
+ } from "@rebasepro/common";