@rebasepro/server-postgresql 0.1.2 → 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.
- package/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/index.es.js +1160 -612
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1158 -610
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresAdapter.d.ts +6 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -1
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +0 -5
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +2 -1
- package/dist/server-postgresql/src/auth/services.d.ts +37 -15
- package/dist/server-postgresql/src/index.d.ts +1 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +43 -856
- package/dist/server-postgresql/src/schema/default-collections.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +10 -1
- package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +1 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +1 -1
- package/dist/server-postgresql/src/services/realtimeService.d.ts +12 -0
- package/dist/server-postgresql/src/websocket.d.ts +2 -1
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +21 -15
- package/src/PostgresAdapter.ts +59 -0
- package/src/PostgresBackendDriver.ts +57 -8
- package/src/PostgresBootstrapper.ts +35 -15
- package/src/auth/ensure-tables.ts +82 -189
- package/src/auth/services.ts +421 -170
- package/src/cli.ts +44 -13
- package/src/data-transformer.ts +78 -8
- package/src/history/HistoryService.ts +25 -2
- package/src/index.ts +1 -0
- package/src/schema/auth-schema.ts +130 -98
- package/src/schema/default-collections.ts +68 -0
- package/src/schema/doctor-cli.ts +5 -1
- package/src/schema/doctor.ts +85 -8
- package/src/schema/generate-drizzle-schema-logic.ts +74 -27
- package/src/schema/generate-drizzle-schema.ts +13 -3
- package/src/schema/introspect-db-inference.ts +5 -5
- package/src/schema/introspect-db-logic.ts +9 -2
- package/src/schema/introspect-db.ts +14 -3
- package/src/services/EntityFetchService.ts +5 -5
- package/src/services/RelationService.ts +2 -2
- package/src/services/entity-helpers.ts +1 -1
- package/src/services/realtimeService.ts +145 -136
- package/src/utils/drizzle-conditions.ts +16 -2
- package/src/websocket.ts +113 -37
- package/test/auth-services.test.ts +163 -74
- package/test/data-transformer-hardening.test.ts +57 -0
- package/test/data-transformer.test.ts +43 -0
- package/test/generate-drizzle-schema.test.ts +7 -5
- package/test/introspect-db-utils.test.ts +4 -1
- package/test/postgresDataDriver.test.ts +17 -0
- package/test/realtimeService.test.ts +7 -7
- package/test/websocket.test.ts +139 -0
|
@@ -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<
|
|
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
|
|
181
|
-
const
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
53
|
-
|
|
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
|
-
//
|
|
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
|
|
58
|
-
|
|
59
|
-
CREATE
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
99
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (
|
|
79
100
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
80
|
-
user_id
|
|
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
|
|
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
|
|
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
|
|
113
|
-
user_id
|
|
114
|
-
role_id TEXT NOT NULL REFERENCES
|
|
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
|
|
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
|
|
148
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (
|
|
128
149
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
129
|
-
user_id
|
|
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
|
|
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
|
|
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
|
|
174
|
+
CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (
|
|
154
175
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
155
|
-
user_id
|
|
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
|
|
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
|
|
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
|
|
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
|
|
238
|
-
const count = parseInt((result.rows[0] as
|
|
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
|
|
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
|
-
}
|