@rebasepro/server-postgresql 0.1.0 → 0.2.1

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 (75) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/index.es.js +1250 -1665
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +1196 -1611
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
  9. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
  10. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
  11. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
  12. package/dist/server-postgresql/src/auth/services.d.ts +37 -15
  13. package/dist/server-postgresql/src/index.d.ts +1 -0
  14. package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
  15. package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
  16. package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
  17. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
  18. package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
  19. package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
  20. package/dist/server-postgresql/src/websocket.d.ts +2 -1
  21. package/dist/types/src/controllers/auth.d.ts +9 -8
  22. package/dist/types/src/controllers/client.d.ts +3 -0
  23. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  24. package/dist/types/src/types/collections.d.ts +67 -2
  25. package/dist/types/src/types/database_adapter.d.ts +94 -0
  26. package/dist/types/src/types/entity_actions.d.ts +7 -1
  27. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  28. package/dist/types/src/types/entity_views.d.ts +36 -1
  29. package/dist/types/src/types/index.d.ts +2 -0
  30. package/dist/types/src/types/plugins.d.ts +1 -1
  31. package/dist/types/src/types/properties.d.ts +24 -5
  32. package/dist/types/src/types/property_config.d.ts +6 -2
  33. package/dist/types/src/types/relations.d.ts +1 -1
  34. package/dist/types/src/types/translations.d.ts +8 -0
  35. package/dist/types/src/users/user.d.ts +5 -0
  36. package/package.json +21 -15
  37. package/src/PostgresAdapter.ts +59 -0
  38. package/src/PostgresBackendDriver.ts +57 -8
  39. package/src/PostgresBootstrapper.ts +35 -15
  40. package/src/auth/ensure-tables.ts +82 -189
  41. package/src/auth/services.ts +421 -170
  42. package/src/cli.ts +44 -13
  43. package/src/data-transformer.ts +78 -8
  44. package/src/history/HistoryService.ts +25 -2
  45. package/src/index.ts +1 -0
  46. package/src/schema/auth-schema.ts +130 -98
  47. package/src/schema/default-collections.ts +68 -0
  48. package/src/schema/doctor-cli.ts +5 -1
  49. package/src/schema/doctor.ts +85 -8
  50. package/src/schema/generate-drizzle-schema-logic.ts +74 -27
  51. package/src/schema/generate-drizzle-schema.ts +13 -3
  52. package/src/schema/introspect-db-inference.ts +5 -5
  53. package/src/schema/introspect-db-logic.ts +9 -2
  54. package/src/schema/introspect-db.ts +14 -3
  55. package/src/services/EntityFetchService.ts +5 -5
  56. package/src/services/RelationService.ts +2 -2
  57. package/src/services/entity-helpers.ts +1 -1
  58. package/src/services/realtimeService.ts +145 -136
  59. package/src/utils/drizzle-conditions.ts +16 -2
  60. package/src/websocket.ts +113 -37
  61. package/test/auth-services.test.ts +163 -74
  62. package/test/data-transformer-hardening.test.ts +57 -0
  63. package/test/data-transformer.test.ts +43 -0
  64. package/test/generate-drizzle-schema.test.ts +7 -5
  65. package/test/introspect-db-utils.test.ts +4 -1
  66. package/test/postgresDataDriver.test.ts +17 -0
  67. package/test/realtimeService.test.ts +7 -7
  68. package/test/websocket.test.ts +139 -0
  69. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
  70. package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
  71. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
  72. package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
  73. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
  74. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
  75. package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
@@ -2,16 +2,11 @@
2
2
  * PostgresBootstrapper
3
3
  *
4
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
5
  */
11
6
 
12
- import { getTableName, isTable, Relations, sql } from "drizzle-orm";
7
+ import { getTableName, isTable, Relations, sql, Table } from "drizzle-orm";
13
8
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
14
- import { PgEnum, PgTable } from "drizzle-orm/pg-core";
9
+ import { PgEnum, PgTable, getTableConfig, AnyPgColumn } from "drizzle-orm/pg-core";
15
10
  import {
16
11
  BackendBootstrapper,
17
12
  InitializedDriver,
@@ -19,6 +14,7 @@ import {
19
14
  DatabaseAdmin,
20
15
  RealtimeProvider,
21
16
  type DataDriver,
17
+ type AuthAdapter,
22
18
  EntityCollection
23
19
  } from "@rebasepro/types";
24
20
  import { PostgresBackendDriver } from "./PostgresBackendDriver";
@@ -33,7 +29,8 @@ import {
33
29
  // @ts-ignore
34
30
  } from "@rebasepro/server-core";
35
31
  import { ensureAuthTablesExist } from "./auth/ensure-tables";
36
- import { RoleService, UserService, PostgresAuthRepository } from "./auth/services";
32
+ import { RoleService, UserService, PostgresAuthRepository, AuthSchemaTables } from "./auth/services";
33
+ import { createAuthSchema } from "./schema/auth-schema";
37
34
 
38
35
  // @ts-ignore
39
36
  import { createEmailService, type EmailConfig, type EmailService } from "@rebasepro/server-core";
@@ -99,7 +96,7 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
99
96
  });
100
97
  }
101
98
 
102
- if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums as Record<string, PgEnum<any>>);
99
+ if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums as Record<string, PgEnum<[string, ...string[]]>>);
103
100
  if (pgConfig.schema?.relations) registry.registerRelations(pgConfig.schema.relations as Record<string, Relations>);
104
101
 
105
102
  // Build schema-aware Drizzle connection
@@ -169,17 +166,39 @@ export function createPostgresBootstrapper(pgConfig: PostgresDriverConfig): Back
169
166
 
170
167
  const internals = driverResult.internals as PostgresDriverInternals;
171
168
  const db = internals.db;
169
+ const registry = internals.registry;
172
170
 
173
- await ensureAuthTablesExist(db);
171
+ await ensureAuthTablesExist(db, registry);
174
172
 
175
173
  let emailService: EmailService | undefined;
176
174
  if (authConfig.email) {
177
175
  emailService = createEmailService(authConfig.email);
178
176
  }
179
177
 
180
- const userService = new UserService(db);
181
- const roleService = new RoleService(db);
182
- const authRepository = new PostgresAuthRepository(db);
178
+ const customUsersTable = registry?.getTable("users");
179
+ const customRolesTable = registry?.getTable("roles");
180
+
181
+ let usersSchemaName = "rebase";
182
+ let rolesSchemaName = "rebase";
183
+
184
+ if (customUsersTable) {
185
+ usersSchemaName = getTableConfig(customUsersTable).schema || "public";
186
+ }
187
+ if (customRolesTable) {
188
+ rolesSchemaName = getTableConfig(customRolesTable).schema || "public";
189
+ }
190
+
191
+ const authTables = createAuthSchema(rolesSchemaName, usersSchemaName) as unknown as AuthSchemaTables;
192
+ if (customUsersTable) {
193
+ authTables.users = customUsersTable as unknown as PgTable & Record<string, AnyPgColumn>;
194
+ }
195
+ if (customRolesTable) {
196
+ authTables.roles = customRolesTable as unknown as PgTable & Record<string, AnyPgColumn>;
197
+ }
198
+
199
+ const userService = new UserService(db, authTables);
200
+ const roleService = new RoleService(db, authTables);
201
+ const authRepository = new PostgresAuthRepository(db, authTables);
183
202
 
184
203
  return { userService,
185
204
  roleService,
@@ -218,13 +237,14 @@ authRepository };
218
237
  // Currently Postgres doesn't need additional routes beyond what the coordinator mounts.
219
238
  },
220
239
 
221
- async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown): Promise<void> {
240
+ async initializeWebsockets(server: unknown, realtimeService: RealtimeProvider, driver: DataDriver, config?: unknown, adapter?: unknown): Promise<void> {
222
241
  const { createPostgresWebSocket } = await import("./websocket");
223
242
  createPostgresWebSocket(
224
243
  server as import("http").Server,
225
244
  realtimeService as RealtimeService,
226
245
  driver as PostgresBackendDriver,
227
- config as AuthConfig
246
+ config as AuthConfig,
247
+ adapter as AuthAdapter | undefined
228
248
  );
229
249
  }
230
250
  };
@@ -1,5 +1,8 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import { getTableConfig, AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
4
+ import { getColumnMeta } from "../services/entity-helpers";
5
+ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
3
6
 
4
7
  /**
5
8
  * Default roles to seed on first run
@@ -9,34 +12,21 @@ const DEFAULT_ROLES = [
9
12
  id: "admin",
10
13
  name: "Admin",
11
14
  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" }
15
+ default_permissions: { read: true, create: true, edit: true, delete: true },
16
+ config: { createCollections: true, editCollections: "all", deleteCollections: "all" }
19
17
  },
20
18
  {
21
19
  id: "editor",
22
20
  name: "Editor",
23
21
  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" }
22
+ default_permissions: { read: true, create: true, edit: true, delete: true },
23
+ config: { createCollections: true, editCollections: "own", deleteCollections: "own" }
31
24
  },
32
25
  {
33
26
  id: "viewer",
34
27
  name: "Viewer",
35
28
  is_admin: false,
36
- default_permissions: { read: true,
37
- create: false,
38
- edit: false,
39
- delete: false },
29
+ default_permissions: { read: true, create: false, edit: false, delete: false },
40
30
  config: null
41
31
  }
42
32
  ];
@@ -45,39 +35,70 @@ delete: false },
45
35
  * Auto-create auth tables if they don't exist
46
36
  * This runs on startup to ensure the database is ready for auth
47
37
  */
48
- export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
38
+ export async function ensureAuthTablesExist(db: NodePgDatabase, registry?: PostgresCollectionRegistry): Promise<void> {
49
39
  console.log("🔍 Checking auth tables...");
50
40
 
51
41
  try {
52
- // ── Create the rebase schema ────────────────────────────────────
53
- await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
42
+ // Resolve dynamic user table name and ID type
43
+ let usersTableName = '"users"';
44
+ let userIdType = "TEXT";
45
+ let usersSchema = "public";
46
+ if (registry) {
47
+ const usersTable = registry.getTable("users") as (PgTable & Record<string, AnyPgColumn>) | undefined;
48
+ if (usersTable) {
49
+ const { getTableName } = await import("drizzle-orm");
50
+ usersSchema = getTableConfig(usersTable).schema || "public";
51
+ usersTableName = usersSchema === "public" ? `"${getTableName(usersTable)}"` : `"${usersSchema}"."${getTableName(usersTable)}"`;
52
+
53
+ // Inspect users.id column to match referenced column type
54
+ if (usersTable.id) {
55
+ const col = usersTable.id;
56
+ const meta = getColumnMeta(col);
57
+ const columnType = meta.columnType;
58
+ if (columnType === "PgUUID") {
59
+ userIdType = "UUID";
60
+ } else if (columnType === "PgSerial" || columnType === "PgInteger") {
61
+ userIdType = "INTEGER";
62
+ } else if (columnType === "PgBigInt" || columnType === "PgBigSerial") {
63
+ userIdType = "BIGINT";
64
+ }
65
+ }
66
+ }
67
+ }
54
68
 
55
- // ── Create tables (idempotent) ──────────────────────────────────
69
+ // Resolve dynamic roles schema name
70
+ let rolesSchema = "rebase";
71
+ if (registry) {
72
+ const rolesTable = registry.getTable("roles");
73
+ if (rolesTable) {
74
+ rolesSchema = getTableConfig(rolesTable).schema || "public";
75
+ }
76
+ }
56
77
 
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
- `);
78
+ // ── Create schemas (idempotent) ──────────────────────────────────
79
+ if (usersSchema !== "public") {
80
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);
81
+ }
82
+ if (rolesSchema !== "public" && rolesSchema !== usersSchema) {
83
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(rolesSchema)}`);
84
+ }
85
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);
69
86
 
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
- `);
87
+ // Dynamic table names
88
+ const userIdentitiesTable = `"${rolesSchema}"."user_identities"`;
89
+ const rolesTableName = `"${rolesSchema}"."roles"`;
90
+ const userRolesTableName = `"${rolesSchema}"."user_roles"`;
91
+ const refreshTokensTableName = `"${rolesSchema}"."refresh_tokens"`;
92
+ const passwordResetTokensTableName = `"${rolesSchema}"."password_reset_tokens"`;
93
+ const appConfigTableName = `"${rolesSchema}"."app_config"`;
94
+
95
+ // ── Create tables (idempotent) ──────────────────────────────────
75
96
 
76
97
  // Create user_identities table
77
98
  await db.execute(sql`
78
- CREATE TABLE IF NOT EXISTS rebase.user_identities (
99
+ CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
79
100
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
80
- user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
101
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
81
102
  provider TEXT NOT NULL,
82
103
  provider_id TEXT NOT NULL,
83
104
  profile_data JSONB,
@@ -90,13 +111,13 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
90
111
  // Create indexes on user_identities
91
112
  await db.execute(sql`
92
113
  CREATE INDEX IF NOT EXISTS idx_user_identities_user
93
- ON rebase.user_identities(user_id)
114
+ ON ${sql.raw(userIdentitiesTable)}(user_id)
94
115
  `);
95
116
 
96
117
 
97
118
  // Create roles table
98
119
  await db.execute(sql`
99
- CREATE TABLE IF NOT EXISTS rebase.roles (
120
+ CREATE TABLE IF NOT EXISTS ${sql.raw(rolesTableName)} (
100
121
  id TEXT PRIMARY KEY,
101
122
  name TEXT NOT NULL,
102
123
  is_admin BOOLEAN DEFAULT FALSE,
@@ -109,9 +130,9 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
109
130
 
110
131
  // Create user_roles junction table
111
132
  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,
133
+ CREATE TABLE IF NOT EXISTS ${sql.raw(userRolesTableName)} (
134
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
135
+ role_id TEXT NOT NULL REFERENCES ${sql.raw(rolesTableName)}(id) ON DELETE CASCADE,
115
136
  PRIMARY KEY (user_id, role_id)
116
137
  )
117
138
  `);
@@ -119,14 +140,14 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
119
140
  // Create index on user_id for faster lookups
120
141
  await db.execute(sql`
121
142
  CREATE INDEX IF NOT EXISTS idx_user_roles_user
122
- ON rebase.user_roles(user_id)
143
+ ON ${sql.raw(userRolesTableName)}(user_id)
123
144
  `);
124
145
 
125
146
  // Create refresh tokens table (includes user_agent, ip_address, and unique constraint)
126
147
  await db.execute(sql`
127
- CREATE TABLE IF NOT EXISTS rebase.refresh_tokens (
148
+ CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
128
149
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
129
- user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
150
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
130
151
  token_hash TEXT NOT NULL UNIQUE,
131
152
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
132
153
  user_agent TEXT,
@@ -139,20 +160,20 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
139
160
  // Create index on token_hash for faster lookups
140
161
  await db.execute(sql`
141
162
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash
142
- ON rebase.refresh_tokens(token_hash)
163
+ ON ${sql.raw(refreshTokensTableName)}(token_hash)
143
164
  `);
144
165
 
145
166
  // Create index on user_id for cleanup operations
146
167
  await db.execute(sql`
147
168
  CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user
148
- ON rebase.refresh_tokens(user_id)
169
+ ON ${sql.raw(refreshTokensTableName)}(user_id)
149
170
  `);
150
171
 
151
172
  // Create password reset tokens table
152
173
  await db.execute(sql`
153
- CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
174
+ CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
154
175
  id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
155
- user_id TEXT NOT NULL REFERENCES rebase.users(id) ON DELETE CASCADE,
176
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
156
177
  token_hash TEXT NOT NULL UNIQUE,
157
178
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
158
179
  used_at TIMESTAMP WITH TIME ZONE,
@@ -163,37 +184,28 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
163
184
  // Create index on token_hash for password reset lookups
164
185
  await db.execute(sql`
165
186
  CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash
166
- ON rebase.password_reset_tokens(token_hash)
187
+ ON ${sql.raw(passwordResetTokensTableName)}(token_hash)
167
188
  `);
168
189
 
169
190
  // Create index on user_id for password reset cleanup
170
191
  await db.execute(sql`
171
192
  CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
172
- ON rebase.password_reset_tokens(user_id)
193
+ ON ${sql.raw(passwordResetTokensTableName)}(user_id)
173
194
  `);
174
195
 
175
196
  // Create app config table
176
197
  await db.execute(sql`
177
- CREATE TABLE IF NOT EXISTS rebase.app_config (
198
+ CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (
178
199
  key TEXT PRIMARY KEY,
179
200
  value JSONB NOT NULL,
180
201
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
181
202
  )
182
203
  `);
183
204
 
184
- // Apply any schema alterations for existing databases
185
- await applyInternalMigrations(db);
186
-
187
205
  // 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
206
  await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
193
207
 
194
208
  // 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
209
  await db.transaction(async (tx) => {
198
210
  await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
199
211
 
@@ -220,7 +232,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
220
232
  });
221
233
 
222
234
  // Seed default roles if none exist
223
- await seedDefaultRoles(db);
235
+ await seedDefaultRoles(db, rolesTableName);
224
236
 
225
237
  console.log("✅ Auth tables ready");
226
238
  } catch (error) {
@@ -232,10 +244,10 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
232
244
  /**
233
245
  * Seed default roles if the roles table is empty
234
246
  */
235
- async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
247
+ async function seedDefaultRoles(db: NodePgDatabase, rolesTableName: string): Promise<void> {
236
248
  // 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);
249
+ const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
250
+ const count = parseInt((result.rows[0] as Record<string, string | number>)?.count as string || "0", 10);
239
251
 
240
252
  if (count > 0) {
241
253
  console.log(`📋 Found ${count} existing roles`);
@@ -246,7 +258,7 @@ async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
246
258
 
247
259
  for (const role of DEFAULT_ROLES) {
248
260
  await db.execute(sql`
249
- INSERT INTO rebase.roles (id, name, is_admin, default_permissions, config)
261
+ INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
250
262
  VALUES (
251
263
  ${role.id},
252
264
  ${role.name},
@@ -260,122 +272,3 @@ async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
260
272
 
261
273
  console.log("✅ Default roles created: admin, editor, viewer");
262
274
  }
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
- }