@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,329 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { AnyPgColumn } from "drizzle-orm/pg-core";
3
+ // import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+ import { CollectionConfig, Properties, Relation } from "@rebasepro/types";
5
+ import { getTableName, resolveCollectionRelations, findRelation } from "@rebasepro/common";
6
+ import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
7
+ import {
8
+ getCollectionByPath,
9
+ getTableForCollection,
10
+ getPrimaryKeys,
11
+ parseIdValues,
12
+ buildCompositeId
13
+ } from "./collection-helpers";
14
+ import { sanitizeAndConvertDates, serializeDataToServer } from "../data-transformer";
15
+ import { RelationService } from "./RelationService";
16
+ import { FetchService } from "./FetchService";
17
+ import { DrizzleClient } from "../interfaces";
18
+ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
19
+ import { logger } from "@rebasepro/server";
20
+ import { extractPgError, extractCauseMessage, pgErrorToFriendlyMessage } from "../utils/pg-error-utils";
21
+
22
+ /**
23
+ * Service for handling all row write operations.
24
+ * Handles saving, deleting, and updating rows.
25
+ */
26
+ export class PersistService {
27
+ private relationService: RelationService;
28
+ private fetchService: FetchService;
29
+
30
+ constructor(private db: DrizzleClient, private registry: PostgresCollectionRegistry) {
31
+ this.relationService = new RelationService(db, registry);
32
+ this.fetchService = new FetchService(db, registry);
33
+ }
34
+
35
+
36
+ /**
37
+ * Delete an row by ID
38
+ */
39
+ async delete(collectionPath: string, id: string | number, _databaseId?: string): Promise<void> {
40
+ const collection = getCollectionByPath(collectionPath, this.registry);
41
+ const table = getTableForCollection(collection, this.registry);
42
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
43
+
44
+ const parsedIdObj = parseIdValues(id, idInfoArray);
45
+
46
+ const conditions = [];
47
+ for (const info of idInfoArray) {
48
+ const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
49
+ if (!field) {
50
+ throw new Error(`ID field '${info.fieldName}' not found in table for collection '${collectionPath}'`);
51
+ }
52
+ conditions.push(eq(field, parsedIdObj[info.fieldName]));
53
+ }
54
+
55
+ await this.db
56
+ .delete(table)
57
+ .where(and(...conditions));
58
+ }
59
+
60
+ /**
61
+ * Delete all rows from a collection
62
+ */
63
+ async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
64
+ const collection = getCollectionByPath(collectionPath, this.registry);
65
+ const table = getTableForCollection(collection, this.registry);
66
+ await this.db.delete(table);
67
+ }
68
+
69
+ /**
70
+ * Save an row (create or update)
71
+ */
72
+ async save<M extends Record<string, unknown>>(
73
+ collectionPath: string,
74
+ values: Partial<M>,
75
+ id?: string | number,
76
+ databaseId?: string
77
+ ): Promise<Record<string, unknown>> {
78
+ // If saving under a nested relation path, resolve the parent and inject FK
79
+ let effectiveCollectionPath = collectionPath;
80
+ const effectiveValues: Partial<M> = { ...values };
81
+ let junctionTableInfo: { parentCollection: CollectionConfig; parentId: string | number; relation: Relation; relationKey: string; } | undefined;
82
+
83
+ if (collectionPath.includes("/")) {
84
+ const segments = collectionPath.split("/").filter(Boolean);
85
+ if (segments.length >= 3 && segments.length % 2 === 1) {
86
+ const rootSegment = segments[0];
87
+ let currentCollection = getCollectionByPath(rootSegment, this.registry);
88
+ let currentId: string | number = segments[1];
89
+
90
+ for (let i = 2; i < segments.length; i += 2) {
91
+ const relationKey = segments[i];
92
+ const resolvedRelations = resolveCollectionRelations(currentCollection);
93
+ const relation = findRelation(resolvedRelations, relationKey);
94
+
95
+ if (!relation) {
96
+ const available = Object.keys(resolvedRelations).join(", ") || "(none)";
97
+ throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'. Available relations: [${available}]`);
98
+ }
99
+
100
+ if (i === segments.length - 1) {
101
+ const targetCollection = relation.target();
102
+ effectiveCollectionPath = targetCollection.slug;
103
+
104
+ // Handle many-to-many with junction table
105
+ if (relation.cardinality === "many" && relation.through) {
106
+ const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
107
+ const parentIdInfo = parentIdInfoArray[0];
108
+ const parsedParentIdObj = parseIdValues(currentId, parentIdInfoArray);
109
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
110
+
111
+ junctionTableInfo = {
112
+ parentCollection: currentCollection,
113
+ parentId: parsedParentId,
114
+ relation: relation,
115
+ relationKey: relationKey
116
+ };
117
+ break;
118
+ }
119
+
120
+ // Find the FK column that should store the parent ID
121
+ let targetColumnName: string;
122
+
123
+ if (relation.localKey) {
124
+ targetColumnName = relation.localKey;
125
+ } else if (relation.foreignKeyOnTarget) {
126
+ targetColumnName = relation.foreignKeyOnTarget;
127
+ } else if (relation.joinPath && relation.joinPath.length === 1) {
128
+ const targetTableName = getTableName(targetCollection);
129
+ const relevantJoinStep = relation.joinPath.find(joinStep => joinStep.table === targetTableName);
130
+
131
+ if (relevantJoinStep) {
132
+ const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to);
133
+ targetColumnName = targetColumnNames[0];
134
+ } else {
135
+ logger.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
136
+ const targetColumnNames = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to);
137
+ targetColumnName = targetColumnNames[0];
138
+ }
139
+ } else if (relation.joinPath && relation.joinPath.length > 1) {
140
+ // For multi-hop relations (like many-to-many through a junction table),
141
+ // there is no direct foreign key on the target table pointing to the parent.
142
+ // The relationship is managed via the junction table.
143
+ // We shouldn't inject the parent ID directly into the target row payload.
144
+ break;
145
+ } else {
146
+ throw new Error(`Relation '${relationKey}' lacks configuration for path-based saving.`);
147
+ }
148
+
149
+ const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
150
+ const parentIdInfo = parentIdInfoArray[0];
151
+ const parsedParentIdObj = parseIdValues(currentId, parentIdInfoArray);
152
+ const parsedParentId = parsedParentIdObj[parentIdInfo.fieldName];
153
+
154
+ const existingValue = (effectiveValues as Record<string, unknown>)[targetColumnName];
155
+ if (existingValue !== undefined && existingValue !== null && existingValue !== parsedParentId) {
156
+ logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
157
+ }
158
+ (effectiveValues as Record<string, unknown>)[targetColumnName] = parsedParentId;
159
+ break;
160
+ } else {
161
+ const nextEntityId = segments[i + 1];
162
+ currentCollection = relation.target();
163
+ currentId = nextEntityId;
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ const collection = getCollectionByPath(effectiveCollectionPath, this.registry);
170
+ const table = getTableForCollection(collection, this.registry);
171
+ const idInfoArray = getPrimaryKeys(collection, this.registry);
172
+ const primaryKeyFields = idInfoArray.map(info => info.fieldName);
173
+
174
+ // Build an object mapping required for dynamic returning
175
+ const returningKeys: Record<string, AnyPgColumn> = {};
176
+ idInfoArray.forEach(info => {
177
+ const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
178
+ if (!field) throw new Error(`Primary key field '${info.fieldName}' not found in table for collection '${effectiveCollectionPath}'`);
179
+ returningKeys[info.fieldName] = field;
180
+ });
181
+
182
+ // Separate relations that require special handling
183
+ const relationValues: Record<string, unknown> = {};
184
+ const otherValues: Partial<M> = { ...effectiveValues };
185
+ const resolvedRelations = resolveCollectionRelations(collection);
186
+
187
+ for (const key in resolvedRelations) {
188
+ const relation = resolvedRelations[key];
189
+ if (relation && relation.cardinality === "many") {
190
+ if (Object.prototype.hasOwnProperty.call(otherValues, key)) {
191
+ relationValues[key] = otherValues[key as keyof M];
192
+ delete otherValues[key as keyof M];
193
+ }
194
+ }
195
+ }
196
+
197
+ let savedId: string | number;
198
+ try {
199
+ // Transform relations to IDs, then sanitize
200
+ const serializedResult = serializeDataToServer(otherValues as M, collection.properties as Properties, collection, this.registry);
201
+
202
+ // Extract relation updates from the typed result
203
+ const inverseRelationUpdates = serializedResult.inverseRelationUpdates;
204
+ const joinPathRelationUpdates = serializedResult.joinPathRelationUpdates;
205
+
206
+ const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
207
+
208
+ savedId = await this.db.transaction(async (tx) => {
209
+ let currentId: string | number;
210
+
211
+ if (id) {
212
+ // Update existing row
213
+ currentId = id; // `id` is already the formatted composite or singular string
214
+ const idValues = parseIdValues(id, idInfoArray);
215
+
216
+ // Apply joinPath one-to-one relation updates BEFORE the main UPDATE.
217
+ // This ensures parentSourceCol reads the pre-update FK value, preventing
218
+ // stale joinPath values from corrupting related rows when an
219
+ // intermediate FK (e.g., author_id) changes in the same save.
220
+ // Example: changing author A→B with stale profile P1 (A's):
221
+ // reads old author_id=A → clears P1.author_id → re-sets P1.author_id=A (no-op).
222
+ if (joinPathRelationUpdates.length > 0) {
223
+ await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
224
+ }
225
+
226
+ // Only issue an UPDATE if there are scalar columns to set.
227
+ // When the payload contains only relation data, entityData is
228
+ // empty after relation stripping and Drizzle throws "No values to set".
229
+ const scalarKeys = Object.keys(entityData as Record<string, unknown>);
230
+ if (scalarKeys.length > 0) {
231
+ const updateQuery = tx.update(table).set(entityData as Record<string, unknown>);
232
+ const conditions = [];
233
+ for (const info of idInfoArray) {
234
+ const field = table[info.fieldName as keyof typeof table] as AnyPgColumn;
235
+ conditions.push(eq(field, idValues[info.fieldName]));
236
+ }
237
+
238
+ await updateQuery.where(and(...conditions));
239
+ }
240
+ } else {
241
+ const dataForInsert = { ...(entityData as Record<string, unknown>) };
242
+
243
+ // Strip empty primary keys so the database defaults (e.g. uuid_gen(), auto-increment) can trigger
244
+ for (const info of idInfoArray) {
245
+ if (dataForInsert[info.fieldName] === "" || dataForInsert[info.fieldName] === null || dataForInsert[info.fieldName] === undefined) {
246
+ delete dataForInsert[info.fieldName];
247
+ }
248
+ }
249
+
250
+ const result = await tx
251
+ .insert(table)
252
+ .values(dataForInsert)
253
+ .returning(returningKeys);
254
+
255
+ const resultRow = result[0];
256
+ currentId = buildCompositeId(resultRow, idInfoArray);
257
+
258
+ // For inserts, apply joinPath after since the parent row didn't exist before
259
+ if (joinPathRelationUpdates.length > 0) {
260
+ await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
261
+ }
262
+ }
263
+
264
+ // Handle inverse relation updates
265
+ if (inverseRelationUpdates.length > 0) {
266
+ await this.relationService.updateInverseRelations(tx, collection, currentId, inverseRelationUpdates);
267
+ }
268
+
269
+ // Update many-to-many relations
270
+ if (Object.keys(relationValues).length > 0) {
271
+ await this.relationService.updateRelationsUsingJoins(tx, collection, currentId, relationValues);
272
+ }
273
+
274
+ // Handle junction table creation for many-to-many path-based saves
275
+ if (junctionTableInfo && !id) {
276
+ await this.relationService.handleJunctionTableCreation(tx, currentId, junctionTableInfo);
277
+ }
278
+
279
+ return currentId;
280
+ });
281
+ } catch (error: unknown) {
282
+ throw this.toUserFriendlyError(error, collection.slug);
283
+ }
284
+
285
+ // Fetch the updated/created row to return with proper relation objects
286
+ const finalEntity = await this.fetchService.fetchOne<M>(collection.slug, savedId, databaseId);
287
+ if (!finalEntity) throw new Error("Could not fetch row after save.");
288
+ return finalEntity;
289
+ }
290
+
291
+ /**
292
+ * Get the RelationService instance for external use
293
+ */
294
+ getRelationService(): RelationService {
295
+ return this.relationService;
296
+ }
297
+
298
+ /**
299
+ * Get the FetchService instance for external use
300
+ */
301
+ getFetchService(): FetchService {
302
+ return this.fetchService;
303
+ }
304
+
305
+ /**
306
+ * Translate raw PostgreSQL / Drizzle errors into user-friendly messages.
307
+ */
308
+ private toUserFriendlyError(error: unknown, collectionSlug: string): Error {
309
+ const pgError = extractPgError(error);
310
+
311
+ if (pgError) {
312
+ const { message } = pgErrorToFriendlyMessage(pgError, collectionSlug);
313
+ return new Error(message);
314
+ }
315
+
316
+ // No PG error found — try to extract a useful message from the
317
+ // Drizzle wrapper instead of leaking the raw SQL query + params.
318
+ const causeMessage = extractCauseMessage(error);
319
+ if (causeMessage) {
320
+ return new Error(`Database error in "${collectionSlug}": ${causeMessage}`);
321
+ }
322
+
323
+ // Last resort: generic message, never leak raw SQL
324
+ if (error instanceof Error && error.message.startsWith("Failed query:")) {
325
+ return new Error(`Failed to save row in "${collectionSlug}". Check server logs for details.`);
326
+ }
327
+ return new Error(`Database error in "${collectionSlug}": ${String(error)}`);
328
+ }
329
+ }