@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,231 @@
1
+ /**
2
+ * PostgresBootstrapper
3
+ *
4
+ * Implements the `BackendBootstrapper` interface for PostgreSQL.
5
+ * Encapsulates all Postgres-specific initialization logic that was previously
6
+ * hardcoded inside `initializeRebaseBackend()`.
7
+ *
8
+ * Third-party drivers (MongoDB, MySQL, etc.) can implement their own
9
+ * bootstrapper following this pattern and pass it to the coordinator.
10
+ */
11
+
12
+ import { getTableName, isTable, Relations, sql } from "drizzle-orm";
13
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
14
+ import { PgEnum, PgTable } from "drizzle-orm/pg-core";
15
+ import {
16
+ BackendBootstrapper,
17
+ InitializedDriver,
18
+ BootstrappedAuth,
19
+ DatabaseAdmin,
20
+ RealtimeProvider,
21
+ type DataDriver,
22
+ EntityCollection
23
+ } from "@rebasepro/types";
24
+ import { PostgresBackendDriver } from "./PostgresBackendDriver";
25
+ import { RealtimeService } from "./services/realtimeService";
26
+ import { DatabasePoolManager } from "./databasePoolManager";
27
+ import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry";
28
+ import {
29
+ createAuthRoutes,
30
+ createAdminRoutes,
31
+ requireAuth,
32
+ requireAdmin
33
+ // @ts-ignore
34
+ } from "@rebasepro/server-core";
35
+ import { ensureAuthTablesExist } from "./auth/ensure-tables";
36
+ import { RoleService, UserService, PostgresAuthRepository } from "./auth/services";
37
+
38
+ // @ts-ignore
39
+ import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
40
+ // @ts-ignore
41
+ import { createHistoryRoutes } from "@rebasepro/server-core";
42
+ import { HistoryService } from "./history/HistoryService";
43
+ import { ensureHistoryTableExists } from "./history/ensure-history-table";
44
+ // @ts-ignore
45
+ import type { AuthConfig, PostgresDriverConfig, HistoryConfig } from "@rebasepro/server-core";
46
+ import type { Hono } from "hono";
47
+ // @ts-ignore
48
+ import type { HonoEnv } from "@rebasepro/server-core";
49
+
50
+ /**
51
+ * Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
52
+ * and re-uses in subsequent lifecycle hooks.
53
+ */
54
+ export interface PostgresDriverInternals {
55
+ db: NodePgDatabase<any>;
56
+ registry: PostgresCollectionRegistry;
57
+ realtimeService: RealtimeService;
58
+ driver: PostgresBackendDriver;
59
+ poolManager?: DatabasePoolManager;
60
+ }
61
+
62
+ /**
63
+ * Default PostgreSQL bootstrapper.
64
+ *
65
+ * Use it to register Postgres with `initializeRebaseBackend()`:
66
+ * ```typescript
67
+ * initializeRebaseBackend({
68
+ * ...config,
69
+ * bootstrappers: [postgresBootstrapper()]
70
+ * });
71
+ * ```
72
+ */
73
+ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): BackendBootstrapper {
74
+ return {
75
+ type: "postgres",
76
+
77
+ async initializeDriver(config: unknown): Promise<InitializedDriver> {
78
+ // config is passed from coordinator, we merge it with our internal pgConfig if needed
79
+ // Currently config from init.ts is `{ collections, collectionRegistry }`
80
+ const { collections, collectionRegistry } = config as {
81
+ collections?: EntityCollection[];
82
+ collectionRegistry?: unknown;
83
+ };
84
+
85
+ // Create a fresh registry for this driver
86
+ const registry = new PostgresCollectionRegistry();
87
+ if (collections) {
88
+ registry.registerMultiple(collections);
89
+ console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map(c => c.slug).join(", ")}]`);
90
+ }
91
+
92
+ // Register tables
93
+ if (pgConfig.schema?.tables) {
94
+ Object.values(pgConfig.schema.tables).forEach((table) => {
95
+ if (isTable(table)) {
96
+ const tableName = getTableName(table);
97
+ registry.registerTable(table as PgTable, tableName);
98
+ }
99
+ });
100
+ }
101
+
102
+ if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums as Record<string, PgEnum<any>>);
103
+ if (pgConfig.schema?.relations) registry.registerRelations(pgConfig.schema.relations as Record<string, Relations>);
104
+
105
+ // Build schema-aware Drizzle connection
106
+ const mergedSchema: Record<string, unknown> = {
107
+ ...pgConfig.schema?.tables,
108
+ ...(pgConfig.schema?.relations || {})
109
+ };
110
+ const { drizzle: createDrizzle } = await import("drizzle-orm/node-postgres");
111
+ const rawClient = ("$client" in pgConfig.connection
112
+ ? (pgConfig.connection as Record<string, unknown>).$client
113
+ : pgConfig.connection) as import("pg").Pool;
114
+ const schemaAwareDb = createDrizzle(rawClient, { schema: mergedSchema });
115
+
116
+ // Verify connection
117
+ try {
118
+ await schemaAwareDb.execute(sql`SELECT 1`);
119
+ } catch (err) {
120
+ console.error("❌ Failed to connect to PostgreSQL:", err);
121
+ console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
122
+ }
123
+
124
+ // Create services
125
+ const realtimeService = new RealtimeService(schemaAwareDb, registry);
126
+ const poolManager = pgConfig.adminConnectionString
127
+ ? new DatabasePoolManager(pgConfig.adminConnectionString)
128
+ : undefined;
129
+ const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, undefined, poolManager);
130
+ realtimeService.setDataDriver(driver);
131
+
132
+ // Ensure branch metadata table exists when branching is available
133
+ if (driver.branchService) {
134
+ try {
135
+ await driver.branchService.ensureBranchMetadataTable();
136
+ } catch (err) {
137
+ console.warn("⚠️ Could not initialize branch metadata table:", err);
138
+ }
139
+ }
140
+
141
+ // Enable cross-instance realtime (opt-in)
142
+ if (pgConfig.connectionString) {
143
+ try {
144
+ await realtimeService.startListening(pgConfig.connectionString);
145
+ } catch (err) {
146
+ console.warn("⚠️ Cross-instance realtime could not be started:", err);
147
+ }
148
+ }
149
+
150
+ const internals: PostgresDriverInternals = {
151
+ db: schemaAwareDb,
152
+ registry,
153
+ realtimeService,
154
+ driver,
155
+ poolManager
156
+ };
157
+
158
+ return {
159
+ driver,
160
+ realtimeProvider: realtimeService,
161
+ collectionRegistry: registry,
162
+ internals
163
+ };
164
+ },
165
+
166
+ async initializeAuth(config: unknown, driverResult: InitializedDriver): Promise<BootstrappedAuth | undefined> {
167
+ const authConfig = config as AuthConfig | undefined;
168
+ if (!authConfig) return undefined;
169
+
170
+ const internals = driverResult.internals as PostgresDriverInternals;
171
+ const db = internals.db;
172
+
173
+ await ensureAuthTablesExist(db);
174
+
175
+ let emailService: EmailService | undefined;
176
+ if (authConfig.email) {
177
+ emailService = createEmailService(authConfig.email);
178
+ }
179
+
180
+ const userService = new UserService(db);
181
+ const roleService = new RoleService(db);
182
+ const authRepository = new PostgresAuthRepository(db);
183
+
184
+ return { userService,
185
+ roleService,
186
+ emailService,
187
+ authRepository };
188
+ },
189
+
190
+ async initializeHistory(config: unknown, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {
191
+ const historyConfig = config as HistoryConfig | boolean | undefined;
192
+ if (!historyConfig) return undefined;
193
+
194
+ const internals = driverResult.internals as PostgresDriverInternals;
195
+ const db = internals.db;
196
+
197
+ await ensureHistoryTableExists(db);
198
+
199
+ const retention = typeof historyConfig === "object" && historyConfig !== null ? (historyConfig as { retention?: number }).retention : undefined;
200
+ const historyService = new HistoryService(db, retention ? { ttlDays: retention } : undefined);
201
+
202
+ return { historyService };
203
+ },
204
+
205
+ async initializeRealtime(_config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined> {
206
+ const internals = driverResult.internals as PostgresDriverInternals;
207
+ return internals.realtimeService;
208
+ },
209
+
210
+ getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {
211
+ const internals = driverResult.internals as PostgresDriverInternals;
212
+ return internals.driver.admin;
213
+ },
214
+
215
+ mountRoutes(app: unknown, basePath: string, driverResult: InitializedDriver): void {
216
+ // The coordinator handles auth/storage/data routes.
217
+ // This hook is for driver-specific extensions only.
218
+ // Currently Postgres doesn't need additional routes beyond what the coordinator mounts.
219
+ },
220
+
221
+ async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown): Promise<void> {
222
+ const { createPostgresWebSocket } = await import("./websocket");
223
+ createPostgresWebSocket(
224
+ server as import("http").Server,
225
+ realtimeService as RealtimeService,
226
+ driver as PostgresBackendDriver,
227
+ config as AuthConfig
228
+ );
229
+ }
230
+ };
231
+ }
@@ -0,0 +1,381 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+
4
+ /**
5
+ * Default roles to seed on first run
6
+ */
7
+ const DEFAULT_ROLES = [
8
+ {
9
+ id: "admin",
10
+ name: "Admin",
11
+ is_admin: true,
12
+ default_permissions: { read: true,
13
+ create: true,
14
+ edit: true,
15
+ delete: true },
16
+ config: { createCollections: true,
17
+ editCollections: "all",
18
+ deleteCollections: "all" }
19
+ },
20
+ {
21
+ id: "editor",
22
+ name: "Editor",
23
+ is_admin: false,
24
+ default_permissions: { read: true,
25
+ create: true,
26
+ edit: true,
27
+ delete: true },
28
+ config: { createCollections: true,
29
+ editCollections: "own",
30
+ deleteCollections: "own" }
31
+ },
32
+ {
33
+ id: "viewer",
34
+ name: "Viewer",
35
+ is_admin: false,
36
+ default_permissions: { read: true,
37
+ create: false,
38
+ edit: false,
39
+ delete: false },
40
+ config: null
41
+ }
42
+ ];
43
+
44
+ /**
45
+ * Auto-create auth tables if they don't exist
46
+ * This runs on startup to ensure the database is ready for auth
47
+ */
48
+ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
49
+ console.log("🔍 Checking auth tables...");
50
+
51
+ try {
52
+ // ── Create the rebase schema ────────────────────────────────────
53
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
54
+
55
+ // ── Create tables (idempotent) ──────────────────────────────────
56
+
57
+ // Create users table
58
+ await db.execute(sql`
59
+ CREATE TABLE IF NOT EXISTS rebase.users (
60
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
61
+ email TEXT NOT NULL UNIQUE,
62
+ password_hash TEXT,
63
+ display_name TEXT,
64
+ photo_url TEXT,
65
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
66
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
67
+ )
68
+ `);
69
+
70
+ // Create index on email for faster lookups
71
+ await db.execute(sql`
72
+ CREATE INDEX IF NOT EXISTS idx_users_email
73
+ ON rebase.users(email)
74
+ `);
75
+
76
+ // Create user_identities table
77
+ await db.execute(sql`
78
+ CREATE TABLE IF NOT EXISTS rebase.user_identities (
79
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
80
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
81
+ provider TEXT NOT NULL,
82
+ provider_id TEXT NOT NULL,
83
+ profile_data JSONB,
84
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
85
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
86
+ UNIQUE(provider, provider_id)
87
+ )
88
+ `);
89
+
90
+ // Create indexes on user_identities
91
+ await db.execute(sql`
92
+ CREATE INDEX IF NOT EXISTS idx_user_identities_user
93
+ ON rebase.user_identities(user_id)
94
+ `);
95
+
96
+
97
+ // Create roles table
98
+ await db.execute(sql`
99
+ CREATE TABLE IF NOT EXISTS rebase.roles (
100
+ id TEXT PRIMARY KEY,
101
+ name TEXT NOT NULL,
102
+ is_admin BOOLEAN DEFAULT FALSE,
103
+ default_permissions JSONB,
104
+ collection_permissions JSONB,
105
+ config JSONB,
106
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
107
+ )
108
+ `);
109
+
110
+ // Create user_roles junction table
111
+ await db.execute(sql`
112
+ CREATE TABLE IF NOT EXISTS rebase.user_roles (
113
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
114
+ role_id TEXT NOT NULL REFERENCES rebase.roles(id) ON DELETE CASCADE,
115
+ PRIMARY KEY (user_id, role_id)
116
+ )
117
+ `);
118
+
119
+ // Create index on user_id for faster lookups
120
+ await db.execute(sql`
121
+ CREATE INDEX IF NOT EXISTS idx_user_roles_user
122
+ ON rebase.user_roles(user_id)
123
+ `);
124
+
125
+ // Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
126
+ await db.execute(sql`
127
+ CREATE TABLE IF NOT EXISTS rebase.refresh_tokens (
128
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
129
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
130
+ token_hash TEXT NOT NULL UNIQUE,
131
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
132
+ user_agent TEXT,
133
+ ip_address TEXT,
134
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
135
+ CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
136
+ )
137
+ `);
138
+
139
+ // Create index on token_hash for faster lookups
140
+ await db.execute(sql`
141
+ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
142
+ ON rebase.refresh_tokens(token_hash)
143
+ `);
144
+
145
+ // Create index on user_id for cleanup operations
146
+ await db.execute(sql`
147
+ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
148
+ ON rebase.refresh_tokens(user_id)
149
+ `);
150
+
151
+ // Create password reset tokens table
152
+ await db.execute(sql`
153
+ CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
154
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
155
+ user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
156
+ token_hash TEXT NOT NULL UNIQUE,
157
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
158
+ used_at TIMESTAMP WITH TIME ZONE,
159
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
160
+ )
161
+ `);
162
+
163
+ // Create index on token_hash for password reset lookups
164
+ await db.execute(sql`
165
+ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
166
+ ON rebase.password_reset_tokens(token_hash)
167
+ `);
168
+
169
+ // Create index on user_id for password reset cleanup
170
+ await db.execute(sql`
171
+ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
172
+ ON rebase.password_reset_tokens(user_id)
173
+ `);
174
+
175
+ // Create app config table
176
+ await db.execute(sql`
177
+ CREATE TABLE IF NOT EXISTS rebase.app_config (
178
+ key TEXT PRIMARY KEY,
179
+ value JSONB NOT NULL,
180
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
181
+ )
182
+ `);
183
+
184
+ // Apply any schema alterations for existing databases
185
+ await applyInternalMigrations(db);
186
+
187
+ // Create the `auth` schema with Supabase-style helper functions for RLS.
188
+ // auth.uid() → returns the current user's ID (reads app.user_id)
189
+ // auth.jwt() → returns the full JWT claims as JSONB (reads app.jwt)
190
+ // auth.roles() → returns comma-separated role IDs (reads app.user_roles)
191
+ // These read from session-local config vars set per-transaction by withAuth().
192
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
193
+
194
+ // Use an advisory transaction lock to serialize function recreation during HMR
195
+ // This prevents the "tuple concurrently updated" race condition when multiple Node
196
+ // workers or rapid restarts attempt to CREATE OR REPLACE FUNCTION simultaneously.
197
+ await db.transaction(async (tx) => {
198
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
199
+
200
+ await tx.execute(sql`
201
+ CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
202
+ SELECT NULLIF(current_setting('app.user_id', true), '');
203
+ $$ LANGUAGE sql STABLE
204
+ `);
205
+
206
+ await tx.execute(sql`
207
+ CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
208
+ SELECT COALESCE(
209
+ NULLIF(current_setting('app.jwt', true), ''),
210
+ '{}'
211
+ )::jsonb;
212
+ $$ LANGUAGE sql STABLE
213
+ `);
214
+
215
+ await tx.execute(sql`
216
+ CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
217
+ SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
218
+ $$ LANGUAGE sql STABLE
219
+ `);
220
+ });
221
+
222
+ // Seed default roles if none exist
223
+ await seedDefaultRoles(db);
224
+
225
+ console.log("✅ Auth tables ready");
226
+ } catch (error) {
227
+ console.error("❌ Failed to create auth tables:", error);
228
+ console.warn("⚠️ Continuing without creating auth tables.");
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Seed default roles if the roles table is empty
234
+ */
235
+ async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
236
+ // Check if any roles exist
237
+ const result = await db.execute(sql`SELECT COUNT(*) as count FROM rebase.roles`);
238
+ const count = parseInt((result.rows[0] as unknown as Record<string, string | number>)?.count as string || "0", 10);
239
+
240
+ if (count > 0) {
241
+ console.log(`📋 Found ${count} existing roles`);
242
+ return;
243
+ }
244
+
245
+ console.log("🌱 Seeding default roles...");
246
+
247
+ for (const role of DEFAULT_ROLES) {
248
+ await db.execute(sql`
249
+ INSERT INTO rebase.roles (id, name, is_admin, default_permissions, config)
250
+ VALUES (
251
+ ${role.id},
252
+ ${role.name},
253
+ ${role.is_admin},
254
+ ${JSON.stringify(role.default_permissions)}::jsonb,
255
+ ${role.config ? JSON.stringify(role.config) : null}::jsonb
256
+ )
257
+ ON CONFLICT (id) DO NOTHING
258
+ `);
259
+ }
260
+
261
+ console.log("✅ Default roles created: admin, editor, viewer");
262
+ }
263
+
264
+ /**
265
+ * Apply idempotent alterations for internal Rebase tables.
266
+ * This runs after CREATE TABLE IF NOT EXISTS to ensure existing
267
+ * databases get new columns without needing external Drizzle migrations.
268
+ */
269
+ async function applyInternalMigrations(db: NodePgDatabase): Promise<void> {
270
+ try {
271
+ // Users Table Migrations
272
+ await db.execute(sql`
273
+ ALTER TABLE rebase.users
274
+ ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
275
+ ADD COLUMN IF NOT EXISTS email_verification_token TEXT,
276
+ ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
277
+ `);
278
+
279
+ // Migrate Old OAuth Data to user_identities table
280
+
281
+ // 1. Check if legacy columns exist
282
+ const columnsCheck = await db.execute(sql`
283
+ SELECT column_name
284
+ FROM information_schema.columns
285
+ WHERE table_schema='rebase' AND table_name='users' AND column_name IN ('google_id', 'linkedin_id', 'provider')
286
+ `);
287
+ const existingColumns = columnsCheck.rows.map(r => r.column_name);
288
+
289
+ if (existingColumns.includes("google_id")) {
290
+ // Migrate google users
291
+ await db.execute(sql`
292
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
293
+ SELECT id, 'google', google_id
294
+ FROM rebase.users
295
+ WHERE google_id IS NOT NULL
296
+ ON CONFLICT (provider, provider_id) DO NOTHING
297
+ `);
298
+ }
299
+
300
+ if (existingColumns.includes("linkedin_id")) {
301
+ // Migrate linkedin users
302
+ await db.execute(sql`
303
+ INSERT INTO rebase.user_identities (user_id, provider, provider_id)
304
+ SELECT id, 'linkedin', linkedin_id
305
+ FROM rebase.users
306
+ WHERE linkedin_id IS NOT NULL
307
+ ON CONFLICT (provider, provider_id) DO NOTHING
308
+ `);
309
+ }
310
+
311
+ // Now drop legacy columns safely if they exist
312
+ if (existingColumns.length > 0) {
313
+ await db.execute(sql`
314
+ ALTER TABLE rebase.users
315
+ DROP COLUMN IF EXISTS provider,
316
+ DROP COLUMN IF EXISTS google_id,
317
+ DROP COLUMN IF EXISTS linkedin_id
318
+ `);
319
+
320
+ // Drop legacy indexes
321
+ await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_google_id`);
322
+ await db.execute(sql`DROP INDEX IF EXISTS rebase.idx_users_linkedin_id`);
323
+
324
+ console.log("✅ Migrated to user_identities and dropped legacy columns.");
325
+ }
326
+
327
+ // Roles Table Migrations
328
+ await db.execute(sql`
329
+ ALTER TABLE rebase.roles
330
+ ADD COLUMN IF NOT EXISTS collection_permissions JSONB
331
+ `);
332
+
333
+ // Refresh Tokens Table Migrations
334
+ await db.execute(sql`
335
+ ALTER TABLE rebase.refresh_tokens
336
+ ADD COLUMN IF NOT EXISTS user_agent TEXT,
337
+ ADD COLUMN IF NOT EXISTS ip_address TEXT
338
+ `);
339
+
340
+ const constraintCheck = await db.execute(sql`
341
+ SELECT 1 FROM information_schema.table_constraints
342
+ WHERE constraint_name = 'unique_device_session'
343
+ AND table_schema = 'rebase'
344
+ AND table_name = 'refresh_tokens'
345
+ `);
346
+
347
+ if (constraintCheck.rows.length === 0) {
348
+ try {
349
+ await db.execute(sql`
350
+ ALTER TABLE rebase.refresh_tokens
351
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
352
+ `);
353
+ console.log("✅ Added unique_device_session constraint");
354
+ } catch (e: unknown) {
355
+ const errorMessage = e instanceof Error ? e.message : String(e);
356
+ if (errorMessage.includes("could not create unique index")) {
357
+ console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
358
+ await db.execute(sql`
359
+ DELETE FROM rebase.refresh_tokens a
360
+ USING rebase.refresh_tokens b
361
+ WHERE a.user_id = b.user_id
362
+ AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
363
+ AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
364
+ AND a.created_at < b.created_at
365
+ `);
366
+ await db.execute(sql`
367
+ ALTER TABLE rebase.refresh_tokens
368
+ ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
369
+ `).catch((retryErr: unknown) => {
370
+ const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
371
+ console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
372
+ });
373
+ } else {
374
+ console.error("Constraint migration issue:", errorMessage);
375
+ }
376
+ }
377
+ }
378
+ } catch (error) {
379
+ console.error("❌ Failed to run internal migrations:", error);
380
+ }
381
+ }