@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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 (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,85 @@
1
+ import { Pool } from "pg";
2
+ import { drizzle } from "drizzle-orm/node-postgres";
3
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+
5
+ export class DatabasePoolManager {
6
+ private pools: Map<string, Pool> = new Map();
7
+ private drizzleInstances: Map<string, NodePgDatabase> = new Map();
8
+ public readonly defaultDatabaseName: string;
9
+ private readonly rootConnectionString: string;
10
+
11
+ constructor(adminConnectionString: string) {
12
+ this.rootConnectionString = adminConnectionString;
13
+ try {
14
+ const url = new URL(adminConnectionString);
15
+ this.defaultDatabaseName = url.pathname.slice(1);
16
+ } catch (e) {
17
+ throw new Error(`Invalid adminConnectionString provided: ${e}`);
18
+ }
19
+ }
20
+
21
+ public getDrizzle(databaseName: string): NodePgDatabase<any> {
22
+ const existing = this.drizzleInstances.get(databaseName);
23
+ if (existing) {
24
+ return existing;
25
+ }
26
+
27
+ const pool = this.getPool(databaseName);
28
+ const db = drizzle(pool);
29
+ this.drizzleInstances.set(databaseName, db);
30
+ return db;
31
+ }
32
+
33
+ public getPool(databaseName: string): Pool {
34
+ if (this.pools.has(databaseName)) {
35
+ return this.pools.get(databaseName)!;
36
+ }
37
+
38
+ const url = new URL(this.rootConnectionString);
39
+ url.pathname = `/${databaseName}`;
40
+
41
+ const pool = new Pool({
42
+ connectionString: url.toString(),
43
+ max: 10, // Default sensible limit, can be tuned later
44
+ idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup
45
+ allowExitOnIdle: true // Prevent idle clients from hanging the Node.js process
46
+ });
47
+
48
+ // Prevent idle client errors from crashing the Node.js process
49
+ pool.on("error", (err) => {
50
+ console.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, err);
51
+ });
52
+
53
+ this.pools.set(databaseName, pool);
54
+ return pool;
55
+ }
56
+
57
+ /**
58
+ * Disconnect and remove the pool for a specific database.
59
+ * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,
60
+ * which need exclusive access to the target database.
61
+ */
62
+ public async disconnectDatabase(databaseName: string): Promise<void> {
63
+ const pool = this.pools.get(databaseName);
64
+ if (pool) {
65
+ await pool.end();
66
+ this.pools.delete(databaseName);
67
+ this.drizzleInstances.delete(databaseName);
68
+ }
69
+ }
70
+
71
+ /** Check if a pool exists for a given database name. */
72
+ public hasPool(databaseName: string): boolean {
73
+ return this.pools.has(databaseName);
74
+ }
75
+
76
+ public async shutdown(): Promise<void> {
77
+ const promises = [];
78
+ for (const [dbName, pool] of this.pools.entries()) {
79
+ console.log(`[DatabasePoolManager] Shutting down pool for ${dbName}`);
80
+ promises.push(pool.end());
81
+ }
82
+ await Promise.all(promises);
83
+ this.pools.clear();
84
+ }
85
+ }
@@ -0,0 +1,248 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ export interface HistoryEntry {
5
+ id: string;
6
+ table_name: string;
7
+ entity_id: string;
8
+ action: "create" | "update" | "delete";
9
+ changed_fields: string[] | null;
10
+ values: Record<string, unknown> | null;
11
+ previous_values: Record<string, unknown> | null;
12
+ updated_by: string | null;
13
+ updated_at: string;
14
+ }
15
+
16
+ export interface RecordHistoryParams {
17
+ tableName: string;
18
+ entityId: string;
19
+ action: "create" | "update" | "delete";
20
+ values?: Record<string, unknown> | null;
21
+ previousValues?: Record<string, unknown> | null;
22
+ updatedBy?: string | null;
23
+ }
24
+
25
+ export interface FetchHistoryOptions {
26
+ limit?: number;
27
+ offset?: number;
28
+ }
29
+
30
+ export interface HistoryRetentionConfig {
31
+ /** Max entries per entity. Oldest pruned first. Default 200. */
32
+ maxEntries: number;
33
+ /** Entries older than this many days are pruned. Default 90. */
34
+ ttlDays: number;
35
+ }
36
+
37
+ const DEFAULT_RETENTION: HistoryRetentionConfig = {
38
+ maxEntries: 200,
39
+ ttlDays: 90
40
+ };
41
+
42
+ /**
43
+ * Service for recording and querying entity change history.
44
+ * Stores snapshots in the `rebase.entity_history` table.
45
+ */
46
+ export class HistoryService {
47
+ public retention: HistoryRetentionConfig;
48
+
49
+ constructor(
50
+ private db: NodePgDatabase,
51
+ retention?: Partial<HistoryRetentionConfig>
52
+ ) {
53
+ this.retention = { ...DEFAULT_RETENTION,
54
+ ...retention };
55
+ }
56
+
57
+ /**
58
+ * Record a history entry for an entity change.
59
+ * This is intentionally fire-and-forget safe — errors are logged but never
60
+ * bubble up to block the main save/delete operation.
61
+ *
62
+ * After inserting, kicks off a non-blocking pruning pass for this entity.
63
+ */
64
+ async recordHistory(params: RecordHistoryParams): Promise<void> {
65
+ const {
66
+ tableName,
67
+ entityId,
68
+ action,
69
+ values,
70
+ previousValues,
71
+ updatedBy
72
+ } = params;
73
+
74
+ const changedFields = previousValues && values
75
+ ? findChangedFields(previousValues, values)
76
+ : null;
77
+
78
+
79
+ // Skip recording if this is an update with zero actual changes
80
+
81
+ if (action === "update" && (!changedFields || changedFields.length === 0)) {
82
+ return;
83
+ }
84
+
85
+ try {
86
+ await this.db.execute(sql`
87
+ INSERT INTO rebase.entity_history
88
+ (table_name, entity_id, action, changed_fields, "values", previous_values, updated_by)
89
+ VALUES (
90
+ ${tableName},
91
+ ${String(entityId)},
92
+ ${action},
93
+ ${changedFields ? sql`ARRAY[${sql.join(changedFields.map(f => sql`${f}`), sql`, `)}]::text[]` : sql`NULL`},
94
+ ${values ? sql`${JSON.stringify(values)}::jsonb` : sql`NULL`},
95
+ ${previousValues ? sql`${JSON.stringify(previousValues)}::jsonb` : sql`NULL`},
96
+ ${updatedBy ?? null}
97
+ )
98
+ `);
99
+
100
+ // Non-blocking prune for this specific entity
101
+ this.pruneEntity(tableName, entityId).catch(err =>
102
+ console.error("History prune failed:", err)
103
+ );
104
+ } catch (error) {
105
+ console.error("Failed to record entity history:", error);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Fetch history entries for an entity, ordered by most recent first.
111
+ */
112
+ async fetchHistory(
113
+ tableName: string,
114
+ entityId: string,
115
+ options: FetchHistoryOptions = {}
116
+ ): Promise<{ data: HistoryEntry[]; total: number }> {
117
+ const limit = options.limit ?? 20;
118
+ const offset = options.offset ?? 0;
119
+
120
+ const [countResult, dataResult] = await Promise.all([
121
+ this.db.execute(sql`
122
+ SELECT COUNT(*) as count
123
+ FROM rebase.entity_history
124
+ WHERE table_name = ${tableName}
125
+ AND entity_id = ${String(entityId)}
126
+ `),
127
+ this.db.execute(sql`
128
+ SELECT id, table_name, entity_id, action, changed_fields,
129
+ "values", previous_values, updated_by, updated_at
130
+ FROM rebase.entity_history
131
+ WHERE table_name = ${tableName}
132
+ AND entity_id = ${String(entityId)}
133
+ ORDER BY updated_at DESC
134
+ LIMIT ${limit}
135
+ OFFSET ${offset}
136
+ `)
137
+ ]);
138
+
139
+ const total = parseInt(
140
+ (countResult.rows[0] as Record<string, string>)?.count ?? "0",
141
+ 10
142
+ );
143
+
144
+ return {
145
+ data: dataResult.rows as unknown as HistoryEntry[],
146
+ total
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Fetch a single history entry by ID.
152
+ */
153
+ async fetchHistoryEntry(historyId: string): Promise<HistoryEntry | null> {
154
+ const result = await this.db.execute(sql`
155
+ SELECT id, table_name, entity_id, action, changed_fields,
156
+ "values", previous_values, updated_by, updated_at
157
+ FROM rebase.entity_history
158
+ WHERE id = ${historyId}
159
+ `);
160
+
161
+ if (result.rows.length === 0) return null;
162
+ return result.rows[0] as unknown as HistoryEntry;
163
+ }
164
+
165
+ // ───────── Retention / Pruning ─────────
166
+
167
+ /**
168
+ * Prune history for a single entity: enforce maxEntries and TTL.
169
+ */
170
+ async pruneEntity(tableName: string, entityId: string): Promise<number> {
171
+ let deleted = 0;
172
+
173
+ // 1. TTL — delete entries older than ttlDays
174
+ const ttlResult = await this.db.execute(sql`
175
+ DELETE FROM rebase.entity_history
176
+ WHERE table_name = ${tableName}
177
+ AND entity_id = ${String(entityId)}
178
+ AND updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
179
+ `);
180
+ deleted += ttlResult.rowCount ?? 0;
181
+
182
+ // 2. Max entries — keep the newest maxEntries, delete the rest
183
+ const maxResult = await this.db.execute(sql`
184
+ DELETE FROM rebase.entity_history
185
+ WHERE id IN (
186
+ SELECT id FROM rebase.entity_history
187
+ WHERE table_name = ${tableName}
188
+ AND entity_id = ${String(entityId)}
189
+ ORDER BY updated_at DESC
190
+ OFFSET ${this.retention.maxEntries}
191
+ )
192
+ `);
193
+ deleted += maxResult.rowCount ?? 0;
194
+
195
+ return deleted;
196
+ }
197
+
198
+ /**
199
+ * Global prune: enforce TTL across ALL entities in a single sweep.
200
+ * Intended to be called periodically (e.g. once per hour or daily).
201
+ */
202
+ async pruneExpired(): Promise<number> {
203
+ const result = await this.db.execute(sql`
204
+ DELETE FROM rebase.entity_history
205
+ WHERE updated_at < NOW() - MAKE_INTERVAL(days => ${this.retention.ttlDays})
206
+ `);
207
+ return result.rowCount ?? 0;
208
+ }
209
+ }
210
+
211
+
212
+ /**
213
+ * Shallow comparison to find top-level keys that changed between two objects.
214
+ */
215
+ export function findChangedFields(
216
+ oldValues: Record<string, unknown>,
217
+ newValues: Record<string, unknown>
218
+ ): string[] | null {
219
+ const changed: string[] = [];
220
+ const allKeys = new Set([
221
+ ...Object.keys(oldValues),
222
+ ...Object.keys(newValues)
223
+ ]);
224
+
225
+ for (const key of allKeys) {
226
+ const oldVal = oldValues[key];
227
+ const newVal = newValues[key];
228
+
229
+ // Skip internal metadata
230
+ if (key.startsWith("__")) continue;
231
+
232
+ if (oldVal !== newVal) {
233
+ // For objects/arrays, use JSON comparison
234
+ if (
235
+ typeof oldVal === "object" && oldVal !== null &&
236
+ typeof newVal === "object" && newVal !== null
237
+ ) {
238
+ if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
239
+ changed.push(key);
240
+ }
241
+ } else {
242
+ changed.push(key);
243
+ }
244
+ }
245
+ }
246
+
247
+ return changed.length > 0 ? changed : null;
248
+ }
@@ -0,0 +1,45 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ /**
5
+ * Auto-create the entity history table if it doesn't exist.
6
+ * This runs on startup when history is enabled, following the same
7
+ * pattern as `ensureAuthTablesExist`.
8
+ */
9
+ export async function ensureHistoryTableExists(db: NodePgDatabase): Promise<void> {
10
+ console.log("🔍 Checking entity history table...");
11
+
12
+ try {
13
+ // Create the rebase schema (idempotent — may already exist from auth init)
14
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
15
+
16
+ await db.execute(sql`
17
+ CREATE TABLE IF NOT EXISTS rebase.entity_history (
18
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
19
+ table_name TEXT NOT NULL,
20
+ entity_id TEXT NOT NULL,
21
+ action TEXT NOT NULL,
22
+ changed_fields TEXT[],
23
+ "values" JSONB,
24
+ previous_values JSONB,
25
+ updated_by TEXT,
26
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
27
+ )
28
+ `);
29
+
30
+ await db.execute(sql`
31
+ CREATE INDEX IF NOT EXISTS idx_history_entity
32
+ ON rebase.entity_history(table_name, entity_id)
33
+ `);
34
+
35
+ await db.execute(sql`
36
+ CREATE INDEX IF NOT EXISTS idx_history_time
37
+ ON rebase.entity_history(table_name, entity_id, updated_at DESC)
38
+ `);
39
+
40
+ console.log("✅ Entity history table ready");
41
+ } catch (error) {
42
+ console.error("❌ Failed to create entity history table:", error);
43
+ console.warn("⚠️ Continuing without creating history table.");
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./connection";
2
+ export * from "./interfaces";
3
+ export * from "./PostgresBackendDriver";
4
+ export * from "./databasePoolManager";
5
+ export * from "./schema/auth-schema";
6
+ export * from "./schema/generate-drizzle-schema-logic";
7
+ export * from "./schema/generate-drizzle-schema";
8
+ export * from "./utils/drizzle-conditions";
9
+ export * from "./services/realtimeService";
10
+ export * from "./websocket";
11
+ export * from "./collections/PostgresCollectionRegistry";
12
+ export * from "./services/BranchService";
13
+ export * from "./PostgresBootstrapper";
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Database Abstraction Interfaces
3
+ *
4
+ * These interfaces define the contracts that any database backend must implement
5
+ * to be used with Rebase. This allows for pluggable database backends like
6
+ * PostgreSQL, MongoDB, MySQL, etc.
7
+ */
8
+
9
+ import {
10
+ Entity,
11
+ EntityCollection,
12
+ FilterValues,
13
+ WhereFilterOp,
14
+ DatabaseConnection,
15
+ QueryFilter,
16
+ FetchCollectionOptions,
17
+ SearchOptions,
18
+ CountOptions,
19
+ ConditionBuilder,
20
+ ConditionBuilderStatic,
21
+ EntityRepository,
22
+ CollectionSubscriptionConfig,
23
+ EntitySubscriptionConfig,
24
+ RealtimeProvider,
25
+ CollectionRegistryInterface,
26
+ DataTransformer,
27
+ BackendConfig,
28
+ BackendInstance,
29
+ BackendFactory
30
+ } from "@rebasepro/types";
31
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
32
+ import { PgTransaction } from "drizzle-orm/pg-core";
33
+
34
+ /**
35
+ * Type representing either a direct database connection or a transaction.
36
+ * Used to allow services to operate within a transaction context.
37
+ * Note: `any` is intentional here — it represents a Drizzle client with
38
+ * a dynamic schema, enabling `db.query[tableName]` access without casts.
39
+ */
40
+
41
+ export type DrizzleClient = NodePgDatabase<any> | PgTransaction<any, any, any>;
42
+
43
+ export type {
44
+ DatabaseConnection,
45
+ QueryFilter,
46
+ FetchCollectionOptions,
47
+ SearchOptions,
48
+ CountOptions,
49
+ ConditionBuilder,
50
+ ConditionBuilderStatic,
51
+ EntityRepository,
52
+ CollectionSubscriptionConfig,
53
+ EntitySubscriptionConfig,
54
+ RealtimeProvider,
55
+ CollectionRegistryInterface,
56
+ DataTransformer,
57
+ BackendConfig,
58
+ BackendInstance,
59
+ BackendFactory
60
+ };
@@ -0,0 +1,169 @@
1
+ import { pgSchema, varchar, uuid, timestamp, boolean, jsonb, primaryKey, unique } from "drizzle-orm/pg-core";
2
+ import { relations } from "drizzle-orm";
3
+
4
+ /**
5
+ * Dedicated PostgreSQL schema for all Rebase internal tables.
6
+ * Keeps the user's `public` schema clean.
7
+ */
8
+ export const rebaseSchema = pgSchema("rebase");
9
+
10
+ /**
11
+ * Users table - stores both email/password and OAuth users
12
+ */
13
+ export const users = rebaseSchema.table("users", {
14
+ id: uuid("id").defaultRandom().primaryKey(),
15
+ email: varchar("email", { length: 255 }).notNull().unique(),
16
+ passwordHash: varchar("password_hash", { length: 255 }), // NULL for OAuth-only users
17
+ displayName: varchar("display_name", { length: 255 }),
18
+ photoUrl: varchar("photo_url", { length: 500 }),
19
+ emailVerified: boolean("email_verified").default(false).notNull(),
20
+ emailVerificationToken: varchar("email_verification_token", { length: 255 }),
21
+ emailVerificationSentAt: timestamp("email_verification_sent_at"),
22
+ createdAt: timestamp("created_at").defaultNow().notNull(),
23
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
24
+ });
25
+
26
+ /**
27
+ * Roles table - defines permission sets
28
+ */
29
+ export const roles = rebaseSchema.table("roles", {
30
+ id: varchar("id", { length: 50 }).primaryKey(), // 'admin', 'editor', 'viewer'
31
+ name: varchar("name", { length: 100 }).notNull(),
32
+ isAdmin: boolean("is_admin").default(false).notNull(),
33
+ defaultPermissions: jsonb("default_permissions").$type<{
34
+ read?: boolean;
35
+ create?: boolean;
36
+ edit?: boolean;
37
+ delete?: boolean;
38
+ }>(),
39
+ collectionPermissions: jsonb("collection_permissions").$type<
40
+ Record<string, {
41
+ read?: boolean;
42
+ create?: boolean;
43
+ edit?: boolean;
44
+ delete?: boolean;
45
+ }>
46
+ >(),
47
+ config: jsonb("config").$type<{
48
+ createCollections?: boolean;
49
+ editCollections?: "own" | "all" | boolean;
50
+ deleteCollections?: "own" | "all" | boolean;
51
+ }>()
52
+ });
53
+
54
+ /**
55
+ * User-Role junction table
56
+ */
57
+ export const userRoles = rebaseSchema.table("user_roles", {
58
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
59
+ roleId: varchar("role_id", { length: 50 }).notNull().references(() => roles.id, { onDelete: "cascade" })
60
+ }, (table) => ({
61
+ pk: primaryKey({ columns: [table.userId, table.roleId] })
62
+ }));
63
+
64
+ /**
65
+ * Refresh tokens for long-lived sessions
66
+ */
67
+ export const refreshTokens = rebaseSchema.table("refresh_tokens", {
68
+ id: uuid("id").defaultRandom().primaryKey(),
69
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
70
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
71
+ expiresAt: timestamp("expires_at").notNull(),
72
+ userAgent: varchar("user_agent", { length: 500 }),
73
+ ipAddress: varchar("ip_address", { length: 45 }),
74
+ createdAt: timestamp("created_at").defaultNow().notNull()
75
+ }, (table) => ({
76
+ uniqueDeviceSession: unique("unique_device_session").on(table.userId, table.userAgent, table.ipAddress)
77
+ }));
78
+
79
+ /**
80
+ * Password reset tokens for forgot password flow
81
+ */
82
+ export const passwordResetTokens = rebaseSchema.table("password_reset_tokens", {
83
+ id: uuid("id").defaultRandom().primaryKey(),
84
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
85
+ tokenHash: varchar("token_hash", { length: 255 }).notNull().unique(),
86
+ expiresAt: timestamp("expires_at").notNull(),
87
+ usedAt: timestamp("used_at"),
88
+ createdAt: timestamp("created_at").defaultNow().notNull()
89
+ });
90
+
91
+ /**
92
+ * App config - key/value store for custom settings
93
+ */
94
+ export const appConfig = rebaseSchema.table("app_config", {
95
+ key: varchar("key", { length: 100 }).primaryKey(),
96
+ value: jsonb("value").notNull(),
97
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
98
+ });
99
+
100
+ /**
101
+ * User identities - maps external OAuth profiles back to local users
102
+ */
103
+ export const userIdentities = rebaseSchema.table("user_identities", {
104
+ id: uuid("id").defaultRandom().primaryKey(),
105
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
106
+ provider: varchar("provider", { length: 50 }).notNull(), // e.g. 'google', 'linkedin'
107
+ providerId: varchar("provider_id", { length: 255 }).notNull(),
108
+ profileData: jsonb("profile_data"),
109
+ createdAt: timestamp("created_at").defaultNow().notNull(),
110
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
111
+ }, (table) => ({
112
+ uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
113
+ }));
114
+
115
+ // Relations
116
+ export const usersRelations = relations(users, ({ many }) => ({
117
+ userRoles: many(userRoles),
118
+ refreshTokens: many(refreshTokens),
119
+ passwordResetTokens: many(passwordResetTokens),
120
+ userIdentities: many(userIdentities)
121
+ }));
122
+
123
+ export const rolesRelations = relations(roles, ({ many }) => ({
124
+ userRoles: many(userRoles)
125
+ }));
126
+
127
+ export const userRolesRelations = relations(userRoles, ({ one }) => ({
128
+ user: one(users, {
129
+ fields: [userRoles.userId],
130
+ references: [users.id]
131
+ }),
132
+ role: one(roles, {
133
+ fields: [userRoles.roleId],
134
+ references: [roles.id]
135
+ })
136
+ }));
137
+
138
+ export const refreshTokensRelations = relations(refreshTokens, ({ one }) => ({
139
+ user: one(users, {
140
+ fields: [refreshTokens.userId],
141
+ references: [users.id]
142
+ })
143
+ }));
144
+
145
+ export const passwordResetTokensRelations = relations(passwordResetTokens, ({ one }) => ({
146
+ user: one(users, {
147
+ fields: [passwordResetTokens.userId],
148
+ references: [users.id]
149
+ })
150
+ }));
151
+
152
+ export const userIdentitiesRelations = relations(userIdentities, ({ one }) => ({
153
+ user: one(users, {
154
+ fields: [userIdentities.userId],
155
+ references: [users.id]
156
+ })
157
+ }));
158
+
159
+ // Type exports
160
+ export type User = typeof users.$inferSelect;
161
+ export type NewUser = typeof users.$inferInsert;
162
+ export type Role = typeof roles.$inferSelect;
163
+ export type NewRole = typeof roles.$inferInsert;
164
+ export type UserRole = typeof userRoles.$inferSelect;
165
+ export type RefreshToken = typeof refreshTokens.$inferSelect;
166
+ export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
167
+ export type AppConfig = typeof appConfig.$inferSelect;
168
+ export type UserIdentity = typeof userIdentities.$inferSelect;
169
+ export type NewUserIdentity = typeof userIdentities.$inferInsert;
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for `rebase doctor`.
4
+ * Invoked via tsx by the server-postgresql CLI plugin.
5
+ */
6
+ import path from "path";
7
+ import chalk from "chalk";
8
+ import { runDoctor } from "./doctor";
9
+
10
+ async function main() {
11
+ const collectionsArg = process.argv.find((a) => a.startsWith("--collections="));
12
+ const schemaArg = process.argv.find((a) => a.startsWith("--schema="));
13
+
14
+ const collectionsPath = collectionsArg?.split("=")[1] ?? path.join("..", "shared", "collections");
15
+ const schemaPath = schemaArg?.split("=")[1] ?? path.join("src", "schema.generated.ts");
16
+
17
+ // Load .env
18
+ try {
19
+ const dotenv = await import("dotenv");
20
+ const envPath = process.env.DOTENV_CONFIG_PATH;
21
+ if (envPath) {
22
+ dotenv.config({ path: envPath });
23
+ } else {
24
+ dotenv.config();
25
+ }
26
+ } catch {
27
+ // dotenv may not be installed
28
+ }
29
+
30
+ const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
31
+
32
+ const report = await runDoctor({
33
+ collectionsPath: path.resolve(process.cwd(), collectionsPath),
34
+ schemaPath: path.resolve(process.cwd(), schemaPath),
35
+ databaseUrl: databaseUrl ?? undefined
36
+ });
37
+
38
+ // Exit with non-zero code if there are errors
39
+ if (report.summary.errors > 0) {
40
+ process.exit(1);
41
+ }
42
+ }
43
+
44
+ main().catch((err) => {
45
+ console.error(chalk.red(" ✗ Doctor failed:"), err instanceof Error ? err.message : String(err));
46
+ process.exit(1);
47
+ });