@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e
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 +48 -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 +36 -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 +12 -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 +10635 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +10643 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -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 +188 -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 +7 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +36 -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 +767 -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/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -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 +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -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/index.d.ts +17 -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 +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -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 +9 -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 +22 -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 +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -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 +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -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/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +93 -0
- package/src/PostgresBackendDriver.ts +1024 -0
- package/src/PostgresBootstrapper.ts +232 -0
- package/src/auth/ensure-tables.ts +309 -0
- package/src/auth/services.ts +740 -0
- package/src/cli.ts +347 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +62 -0
- package/src/data-transformer.ts +569 -0
- package/src/databasePoolManager.ts +84 -0
- package/src/history/HistoryService.ts +257 -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 +146 -0
- package/src/schema/generate-drizzle-schema-logic.ts +618 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1447 -0
- package/src/services/EntityPersistService.ts +351 -0
- package/src/services/RelationService.ts +1012 -0
- package/src/services/entity-helpers.ts +121 -0
- package/src/services/entityService.ts +209 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1005 -0
- package/src/utils/drizzle-conditions.ts +999 -0
- package/src/websocket.ts +487 -0
- package/test/auth-services.test.ts +569 -0
- package/test/branchService.test.ts +357 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +352 -0
- package/test/entityService.relations.test.ts +912 -0
- package/test/entityService.subcollection-search.test.ts +516 -0
- package/test/entityService.test.ts +977 -0
- package/test/generate-drizzle-schema.test.ts +795 -0
- package/test/historyService.test.ts +126 -0
- package/test/postgresDataDriver.test.ts +556 -0
- package/test/realtimeService.test.ts +276 -0
- package/test/relations.test.ts +662 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +30 -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,232 @@
|
|
|
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
|
+
configureGoogleOAuth,
|
|
30
|
+
createAuthRoutes,
|
|
31
|
+
createAdminRoutes,
|
|
32
|
+
requireAuth,
|
|
33
|
+
requireAdmin
|
|
34
|
+
// @ts-ignore
|
|
35
|
+
} from "@rebasepro/server-core";
|
|
36
|
+
import { ensureAuthTablesExist } from "./auth/ensure-tables";
|
|
37
|
+
import { RoleService, UserService, PostgresAuthRepository } from "./auth/services";
|
|
38
|
+
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
import { createHistoryRoutes } from "@rebasepro/server-core";
|
|
43
|
+
import { HistoryService } from "./history/HistoryService";
|
|
44
|
+
import { ensureHistoryTableExists } from "./history/ensure-history-table";
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
import type { AuthConfig, PostgresDriverConfig, HistoryConfig } from "@rebasepro/server-core";
|
|
47
|
+
import type { Hono } from "hono";
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
import type { HonoEnv } from "@rebasepro/server-core";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Opaque internals bag that PostgresBootstrapper stores during `initializeDriver()`
|
|
53
|
+
* and re-uses in subsequent lifecycle hooks.
|
|
54
|
+
*/
|
|
55
|
+
export interface PostgresDriverInternals {
|
|
56
|
+
db: NodePgDatabase<any>;
|
|
57
|
+
registry: PostgresCollectionRegistry;
|
|
58
|
+
realtimeService: RealtimeService;
|
|
59
|
+
driver: PostgresBackendDriver;
|
|
60
|
+
poolManager?: DatabasePoolManager;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default PostgreSQL bootstrapper.
|
|
65
|
+
*
|
|
66
|
+
* Use it to register Postgres with `initializeRebaseBackend()`:
|
|
67
|
+
* ```typescript
|
|
68
|
+
* initializeRebaseBackend({
|
|
69
|
+
* ...config,
|
|
70
|
+
* bootstrappers: [postgresBootstrapper()]
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): BackendBootstrapper {
|
|
75
|
+
return {
|
|
76
|
+
type: "postgres",
|
|
77
|
+
|
|
78
|
+
async initializeDriver(config: unknown): Promise<InitializedDriver> {
|
|
79
|
+
// config is passed from coordinator, we merge it with our internal pgConfig if needed
|
|
80
|
+
// Currently config from init.ts is `{ collections, collectionRegistry }`
|
|
81
|
+
const { collections, collectionRegistry } = config as {
|
|
82
|
+
collections?: EntityCollection[];
|
|
83
|
+
collectionRegistry?: unknown;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Create a fresh registry for this driver
|
|
87
|
+
const registry = new PostgresCollectionRegistry();
|
|
88
|
+
if (collections) {
|
|
89
|
+
registry.registerMultiple(collections);
|
|
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
|
+
if (authConfig.google?.clientId) {
|
|
176
|
+
configureGoogleOAuth(authConfig.google.clientId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let emailService: EmailService | undefined;
|
|
180
|
+
if (authConfig.email) {
|
|
181
|
+
emailService = createEmailService(authConfig.email);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const userService = new UserService(db);
|
|
185
|
+
const roleService = new RoleService(db);
|
|
186
|
+
const authRepository = new PostgresAuthRepository(db);
|
|
187
|
+
|
|
188
|
+
return { userService, roleService, emailService, authRepository };
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async initializeHistory(config: unknown, driverResult: InitializedDriver): Promise<{ historyService: HistoryService } | undefined> {
|
|
192
|
+
const historyConfig = config as HistoryConfig | boolean | undefined;
|
|
193
|
+
if (!historyConfig) return undefined;
|
|
194
|
+
|
|
195
|
+
const internals = driverResult.internals as PostgresDriverInternals;
|
|
196
|
+
const db = internals.db;
|
|
197
|
+
|
|
198
|
+
await ensureHistoryTableExists(db);
|
|
199
|
+
|
|
200
|
+
const retention = typeof historyConfig === "object" && historyConfig !== null ? (historyConfig as { retention?: number }).retention : undefined;
|
|
201
|
+
const historyService = new HistoryService(db, retention ? { ttlDays: retention } : undefined);
|
|
202
|
+
|
|
203
|
+
return { historyService };
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
async initializeRealtime(_config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined> {
|
|
207
|
+
const internals = driverResult.internals as PostgresDriverInternals;
|
|
208
|
+
return internals.realtimeService;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
getAdmin(driverResult: InitializedDriver): DatabaseAdmin | undefined {
|
|
212
|
+
const internals = driverResult.internals as PostgresDriverInternals;
|
|
213
|
+
return internals.driver.admin;
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
mountRoutes(app: unknown, basePath: string, driverResult: InitializedDriver): void {
|
|
217
|
+
// The coordinator handles auth/storage/data routes.
|
|
218
|
+
// This hook is for driver-specific extensions only.
|
|
219
|
+
// Currently Postgres doesn't need additional routes beyond what the coordinator mounts.
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown): Promise<void> {
|
|
223
|
+
const { createPostgresWebSocket } = await import("./websocket");
|
|
224
|
+
createPostgresWebSocket(
|
|
225
|
+
server as import("http").Server,
|
|
226
|
+
realtimeService as RealtimeService,
|
|
227
|
+
driver as PostgresBackendDriver,
|
|
228
|
+
config as AuthConfig
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
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, create: true, edit: true, delete: true },
|
|
13
|
+
config: { createCollections: true, editCollections: "all", deleteCollections: "all" }
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "editor",
|
|
17
|
+
name: "Editor",
|
|
18
|
+
is_admin: false,
|
|
19
|
+
default_permissions: { read: true, create: true, edit: true, delete: true },
|
|
20
|
+
config: { createCollections: true, editCollections: "own", deleteCollections: "own" }
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "viewer",
|
|
24
|
+
name: "Viewer",
|
|
25
|
+
is_admin: false,
|
|
26
|
+
default_permissions: { read: true, create: false, edit: false, delete: false },
|
|
27
|
+
config: null
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Auto-create auth tables if they don't exist
|
|
33
|
+
* This runs on startup to ensure the database is ready for auth
|
|
34
|
+
*/
|
|
35
|
+
export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
36
|
+
console.log("🔍 Checking auth tables...");
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// ── Create the rebase schema ────────────────────────────────────
|
|
40
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
|
|
41
|
+
|
|
42
|
+
// ── Create tables (idempotent) ──────────────────────────────────
|
|
43
|
+
|
|
44
|
+
// Create users table
|
|
45
|
+
await db.execute(sql`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS rebase.users (
|
|
47
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
48
|
+
email TEXT NOT NULL UNIQUE,
|
|
49
|
+
password_hash TEXT,
|
|
50
|
+
display_name TEXT,
|
|
51
|
+
photo_url TEXT,
|
|
52
|
+
provider TEXT DEFAULT 'email',
|
|
53
|
+
google_id TEXT UNIQUE,
|
|
54
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
55
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
// Create index on email for faster lookups
|
|
60
|
+
await db.execute(sql`
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_users_email
|
|
62
|
+
ON rebase.users(email)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
// Create index on google_id for OAuth lookups
|
|
66
|
+
await db.execute(sql`
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_users_google_id
|
|
68
|
+
ON rebase.users(google_id)
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
// Create roles table
|
|
72
|
+
await db.execute(sql`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS rebase.roles (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
is_admin BOOLEAN DEFAULT FALSE,
|
|
77
|
+
default_permissions JSONB,
|
|
78
|
+
collection_permissions JSONB,
|
|
79
|
+
config JSONB,
|
|
80
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
81
|
+
)
|
|
82
|
+
`);
|
|
83
|
+
|
|
84
|
+
// Migration: Add collection_permissions column if it doesn't exist (for existing databases)
|
|
85
|
+
await db.execute(sql`
|
|
86
|
+
ALTER TABLE rebase.roles
|
|
87
|
+
ADD COLUMN IF NOT EXISTS collection_permissions JSONB
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
// Create user_roles junction table
|
|
91
|
+
await db.execute(sql`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS rebase.user_roles (
|
|
93
|
+
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
94
|
+
role_id TEXT NOT NULL REFERENCES rebase.roles(id) ON DELETE CASCADE,
|
|
95
|
+
PRIMARY KEY (user_id, role_id)
|
|
96
|
+
)
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
// Create index on user_id for faster lookups
|
|
100
|
+
await db.execute(sql`
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_user_roles_user
|
|
102
|
+
ON rebase.user_roles(user_id)
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
// Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
|
|
106
|
+
await db.execute(sql`
|
|
107
|
+
CREATE TABLE IF NOT EXISTS rebase.refresh_tokens (
|
|
108
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
109
|
+
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
110
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
111
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
112
|
+
user_agent TEXT,
|
|
113
|
+
ip_address TEXT,
|
|
114
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
115
|
+
CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
116
|
+
)
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
// Create index on token_hash for faster lookups
|
|
120
|
+
await db.execute(sql`
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
|
|
122
|
+
ON rebase.refresh_tokens(token_hash)
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
// Create index on user_id for cleanup operations
|
|
126
|
+
await db.execute(sql`
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
|
|
128
|
+
ON rebase.refresh_tokens(user_id)
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
// Migration: Add user_agent and ip_address to refresh tokens (for tables created before these columns existed)
|
|
132
|
+
await db.execute(sql`
|
|
133
|
+
ALTER TABLE rebase.refresh_tokens
|
|
134
|
+
ADD COLUMN IF NOT EXISTS user_agent TEXT
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
await db.execute(sql`
|
|
138
|
+
ALTER TABLE rebase.refresh_tokens
|
|
139
|
+
ADD COLUMN IF NOT EXISTS ip_address TEXT
|
|
140
|
+
`);
|
|
141
|
+
|
|
142
|
+
// Migration: Ensure unique_device_session constraint exists (for tables created before it was in CREATE TABLE)
|
|
143
|
+
// Check if constraint already exists before attempting to add it
|
|
144
|
+
const constraintCheck = await db.execute(sql`
|
|
145
|
+
SELECT 1 FROM information_schema.table_constraints
|
|
146
|
+
WHERE constraint_name = 'unique_device_session'
|
|
147
|
+
AND table_schema = 'rebase'
|
|
148
|
+
AND table_name = 'refresh_tokens'
|
|
149
|
+
`);
|
|
150
|
+
if (constraintCheck.rows.length === 0) {
|
|
151
|
+
try {
|
|
152
|
+
await db.execute(sql`
|
|
153
|
+
ALTER TABLE rebase.refresh_tokens
|
|
154
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
155
|
+
`);
|
|
156
|
+
console.log("✅ Added unique_device_session constraint");
|
|
157
|
+
} catch (e: unknown) {
|
|
158
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
159
|
+
// If there's duplicate data preventing the constraint, clean up first
|
|
160
|
+
if (errorMessage.includes('could not create unique index')) {
|
|
161
|
+
console.warn("⚠️ Duplicate sessions found, cleaning up before adding constraint...");
|
|
162
|
+
// Keep only the most recent token per user/device combo
|
|
163
|
+
await db.execute(sql`
|
|
164
|
+
DELETE FROM rebase.refresh_tokens a
|
|
165
|
+
USING rebase.refresh_tokens b
|
|
166
|
+
WHERE a.user_id = b.user_id
|
|
167
|
+
AND COALESCE(a.user_agent, '') = COALESCE(b.user_agent, '')
|
|
168
|
+
AND COALESCE(a.ip_address, '') = COALESCE(b.ip_address, '')
|
|
169
|
+
AND a.created_at < b.created_at
|
|
170
|
+
`);
|
|
171
|
+
// Retry constraint creation
|
|
172
|
+
await db.execute(sql`
|
|
173
|
+
ALTER TABLE rebase.refresh_tokens
|
|
174
|
+
ADD CONSTRAINT unique_device_session UNIQUE (user_id, user_agent, ip_address)
|
|
175
|
+
`).catch((retryErr: unknown) => {
|
|
176
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
177
|
+
console.error("Failed to add unique_device_session constraint after cleanup:", retryMessage);
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
console.error("Constraint migration issue:", errorMessage);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create password reset tokens table
|
|
186
|
+
await db.execute(sql`
|
|
187
|
+
CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
|
|
188
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
189
|
+
user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
|
|
190
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
191
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
192
|
+
used_at TIMESTAMP WITH TIME ZONE,
|
|
193
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
194
|
+
)
|
|
195
|
+
`);
|
|
196
|
+
|
|
197
|
+
// Create index on token_hash for password reset lookups
|
|
198
|
+
await db.execute(sql`
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
|
|
200
|
+
ON rebase.password_reset_tokens(token_hash)
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
// Create index on user_id for password reset cleanup
|
|
204
|
+
await db.execute(sql`
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
|
|
206
|
+
ON rebase.password_reset_tokens(user_id)
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
// Create app config table
|
|
210
|
+
await db.execute(sql`
|
|
211
|
+
CREATE TABLE IF NOT EXISTS rebase.app_config (
|
|
212
|
+
key TEXT PRIMARY KEY,
|
|
213
|
+
value JSONB NOT NULL,
|
|
214
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
215
|
+
)
|
|
216
|
+
`);
|
|
217
|
+
|
|
218
|
+
// Migration: Add email verification columns to users if they don't exist
|
|
219
|
+
await db.execute(sql`
|
|
220
|
+
ALTER TABLE rebase.users
|
|
221
|
+
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE
|
|
222
|
+
`);
|
|
223
|
+
|
|
224
|
+
await db.execute(sql`
|
|
225
|
+
ALTER TABLE rebase.users
|
|
226
|
+
ADD COLUMN IF NOT EXISTS email_verification_token TEXT
|
|
227
|
+
`);
|
|
228
|
+
|
|
229
|
+
await db.execute(sql`
|
|
230
|
+
ALTER TABLE rebase.users
|
|
231
|
+
ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMP WITH TIME ZONE
|
|
232
|
+
`);
|
|
233
|
+
|
|
234
|
+
// Create the `auth` schema with Supabase-style helper functions for RLS.
|
|
235
|
+
// auth.uid() → returns the current user's ID (reads app.user_id)
|
|
236
|
+
// auth.jwt() → returns the full JWT claims as JSONB (reads app.jwt)
|
|
237
|
+
// auth.roles() → returns comma-separated role IDs (reads app.user_roles)
|
|
238
|
+
// These read from session-local config vars set per-transaction by withAuth().
|
|
239
|
+
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
240
|
+
|
|
241
|
+
// Use an advisory transaction lock to serialize function recreation during HMR
|
|
242
|
+
// This prevents the "tuple concurrently updated" race condition when multiple Node
|
|
243
|
+
// workers or rapid restarts attempt to CREATE OR REPLACE FUNCTION simultaneously.
|
|
244
|
+
await db.transaction(async (tx) => {
|
|
245
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
246
|
+
|
|
247
|
+
await tx.execute(sql`
|
|
248
|
+
CREATE OR REPLACE FUNCTION auth.uid() RETURNS text AS $$
|
|
249
|
+
SELECT NULLIF(current_setting('app.user_id', true), '');
|
|
250
|
+
$$ LANGUAGE sql STABLE
|
|
251
|
+
`);
|
|
252
|
+
|
|
253
|
+
await tx.execute(sql`
|
|
254
|
+
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb AS $$
|
|
255
|
+
SELECT COALESCE(
|
|
256
|
+
NULLIF(current_setting('app.jwt', true), ''),
|
|
257
|
+
'{}'
|
|
258
|
+
)::jsonb;
|
|
259
|
+
$$ LANGUAGE sql STABLE
|
|
260
|
+
`);
|
|
261
|
+
|
|
262
|
+
await tx.execute(sql`
|
|
263
|
+
CREATE OR REPLACE FUNCTION auth.roles() RETURNS text AS $$
|
|
264
|
+
SELECT COALESCE(NULLIF(current_setting('app.user_roles', true), ''), '');
|
|
265
|
+
$$ LANGUAGE sql STABLE
|
|
266
|
+
`);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Seed default roles if none exist
|
|
270
|
+
await seedDefaultRoles(db);
|
|
271
|
+
|
|
272
|
+
console.log("✅ Auth tables ready");
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error("❌ Failed to create auth tables:", error);
|
|
275
|
+
console.warn("⚠️ Continuing without creating auth tables.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Seed default roles if the roles table is empty
|
|
281
|
+
*/
|
|
282
|
+
async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
|
|
283
|
+
// Check if any roles exist
|
|
284
|
+
const result = await db.execute(sql`SELECT COUNT(*) as count FROM rebase.roles`);
|
|
285
|
+
const count = parseInt((result.rows[0] as unknown as Record<string, string | number>)?.count as string || "0", 10);
|
|
286
|
+
|
|
287
|
+
if (count > 0) {
|
|
288
|
+
console.log(`📋 Found ${count} existing roles`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log("🌱 Seeding default roles...");
|
|
293
|
+
|
|
294
|
+
for (const role of DEFAULT_ROLES) {
|
|
295
|
+
await db.execute(sql`
|
|
296
|
+
INSERT INTO rebase.roles (id, name, is_admin, default_permissions, config)
|
|
297
|
+
VALUES (
|
|
298
|
+
${role.id},
|
|
299
|
+
${role.name},
|
|
300
|
+
${role.is_admin},
|
|
301
|
+
${JSON.stringify(role.default_permissions)}::jsonb,
|
|
302
|
+
${role.config ? JSON.stringify(role.config) : null}::jsonb
|
|
303
|
+
)
|
|
304
|
+
ON CONFLICT (id) DO NOTHING
|
|
305
|
+
`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log("✅ Default roles created: admin, editor, viewer");
|
|
309
|
+
}
|