@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.
- package/LICENSE +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +58 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +22 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +11298 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +11306 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +192 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +40 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
- package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +119 -0
- package/dist/types/src/controllers/client.d.ts +170 -0
- package/dist/types/src/controllers/collection_registry.d.ts +45 -0
- package/dist/types/src/controllers/customization_controller.d.ts +60 -0
- package/dist/types/src/controllers/data.d.ts +168 -0
- package/dist/types/src/controllers/data_driver.d.ts +160 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +18 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +54 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +171 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +105 -0
- package/dist/types/src/types/backend.d.ts +536 -0
- package/dist/types/src/types/builders.d.ts +15 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +856 -0
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +10 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +23 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +279 -0
- package/dist/types/src/types/properties.d.ts +1176 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +252 -0
- package/dist/types/src/types/translations.d.ts +870 -0
- package/dist/types/src/types/user_management_delegate.d.ts +121 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +92 -0
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +1008 -0
- package/src/PostgresBootstrapper.ts +231 -0
- package/src/auth/ensure-tables.ts +381 -0
- package/src/auth/services.ts +799 -0
- package/src/cli.ts +648 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +84 -0
- package/src/data-transformer.ts +608 -0
- package/src/databasePoolManager.ts +85 -0
- package/src/history/HistoryService.ts +248 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +169 -0
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +765 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/schema/introspect-db-logic.ts +542 -0
- package/src/schema/introspect-db.ts +211 -0
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1576 -0
- package/src/services/EntityPersistService.ts +349 -0
- package/src/services/RelationService.ts +1274 -0
- package/src/services/entity-helpers.ts +147 -0
- package/src/services/entityService.ts +211 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1034 -0
- package/src/utils/drizzle-conditions.ts +1000 -0
- package/src/websocket.ts +518 -0
- package/test/auth-services.test.ts +661 -0
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +367 -0
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +367 -0
- package/test/entityService.relations.test.ts +1008 -0
- package/test/entityService.subcollection-search.test.ts +566 -0
- package/test/entityService.test.ts +1035 -0
- package/test/generate-drizzle-schema.test.ts +988 -0
- package/test/historyService.test.ts +141 -0
- package/test/introspect-db-generation.test.ts +436 -0
- package/test/introspect-db-utils.test.ts +389 -0
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +648 -0
- package/test/realtimeService.test.ts +307 -0
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +1115 -0
- package/test/unmapped-tables-safety.test.ts +345 -0
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +32 -0
- package/test_hash.js +14 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- 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
|
+
}
|