@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- 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 +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -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/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- 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/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- 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 +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +5 -5
|
@@ -9,21 +9,34 @@ const DEFAULT_ROLES = [
|
|
|
9
9
|
id: "admin",
|
|
10
10
|
name: "Admin",
|
|
11
11
|
is_admin: true,
|
|
12
|
-
default_permissions: { read: true,
|
|
13
|
-
|
|
12
|
+
default_permissions: { read: true,
|
|
13
|
+
create: true,
|
|
14
|
+
edit: true,
|
|
15
|
+
delete: true },
|
|
16
|
+
config: { createCollections: true,
|
|
17
|
+
editCollections: "all",
|
|
18
|
+
deleteCollections: "all" }
|
|
14
19
|
},
|
|
15
20
|
{
|
|
16
21
|
id: "editor",
|
|
17
22
|
name: "Editor",
|
|
18
23
|
is_admin: false,
|
|
19
|
-
default_permissions: { read: true,
|
|
20
|
-
|
|
24
|
+
default_permissions: { read: true,
|
|
25
|
+
create: true,
|
|
26
|
+
edit: true,
|
|
27
|
+
delete: true },
|
|
28
|
+
config: { createCollections: true,
|
|
29
|
+
editCollections: "own",
|
|
30
|
+
deleteCollections: "own" }
|
|
21
31
|
},
|
|
22
32
|
{
|
|
23
33
|
id: "viewer",
|
|
24
34
|
name: "Viewer",
|
|
25
35
|
is_admin: false,
|
|
26
|
-
default_permissions: { read: true,
|
|
36
|
+
default_permissions: { read: true,
|
|
37
|
+
create: false,
|
|
38
|
+
edit: false,
|
|
39
|
+
delete: false },
|
|
27
40
|
config: null
|
|
28
41
|
}
|
|
29
42
|
];
|
|
@@ -49,8 +62,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
49
62
|
password_hash TEXT,
|
|
50
63
|
display_name TEXT,
|
|
51
64
|
photo_url TEXT,
|
|
52
|
-
provider TEXT DEFAULT 'email',
|
|
53
|
-
google_id TEXT UNIQUE,
|
|
54
65
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
55
66
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
56
67
|
)
|
|
@@ -62,12 +73,27 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
62
73
|
ON rebase.users(email)
|
|
63
74
|
`);
|
|
64
75
|
|
|
65
|
-
// Create
|
|
76
|
+
// Create user_identities table
|
|
66
77
|
await db.execute(sql`
|
|
67
|
-
CREATE
|
|
68
|
-
|
|
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
|
+
)
|
|
69
88
|
`);
|
|
70
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
|
+
|
|
71
97
|
// Create roles table
|
|
72
98
|
await db.execute(sql`
|
|
73
99
|
CREATE TABLE IF NOT EXISTS rebase.roles (
|
|
@@ -81,12 +107,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
81
107
|
)
|
|
82
108
|
`);
|
|
83
109
|
|
|
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
110
|
// Create user_roles junction table
|
|
91
111
|
await db.execute(sql`
|
|
92
112
|
CREATE TABLE IF NOT EXISTS rebase.user_roles (
|
|
@@ -128,60 +148,6 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
128
148
|
ON rebase.refresh_tokens(user_id)
|
|
129
149
|
`);
|
|
130
150
|
|
|
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
151
|
// Create password reset tokens table
|
|
186
152
|
await db.execute(sql`
|
|
187
153
|
CREATE TABLE IF NOT EXISTS rebase.password_reset_tokens (
|
|
@@ -215,21 +181,8 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
215
181
|
)
|
|
216
182
|
`);
|
|
217
183
|
|
|
218
|
-
//
|
|
219
|
-
await db
|
|
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
|
-
`);
|
|
184
|
+
// Apply any schema alterations for existing databases
|
|
185
|
+
await applyInternalMigrations(db);
|
|
233
186
|
|
|
234
187
|
// Create the `auth` schema with Supabase-style helper functions for RLS.
|
|
235
188
|
// auth.uid() → returns the current user's ID (reads app.user_id)
|
|
@@ -239,7 +192,7 @@ export async function ensureAuthTablesExist(db: NodePgDatabase): Promise<void> {
|
|
|
239
192
|
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS auth`);
|
|
240
193
|
|
|
241
194
|
// Use an advisory transaction lock to serialize function recreation during HMR
|
|
242
|
-
// This prevents the "tuple concurrently updated" race condition when multiple Node
|
|
195
|
+
// This prevents the "tuple concurrently updated" race condition when multiple Node
|
|
243
196
|
// workers or rapid restarts attempt to CREATE OR REPLACE FUNCTION simultaneously.
|
|
244
197
|
await db.transaction(async (tx) => {
|
|
245
198
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);
|
|
@@ -307,3 +260,122 @@ async function seedDefaultRoles(db: NodePgDatabase): Promise<void> {
|
|
|
307
260
|
|
|
308
261
|
console.log("✅ Default roles created: admin, editor, viewer");
|
|
309
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
|
+
}
|
package/src/auth/services.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { eq, sql } from "drizzle-orm";
|
|
2
2
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
3
|
-
import { users, refreshTokens, passwordResetTokens, User, NewUser } from "../schema/auth-schema";
|
|
3
|
+
import { users, userIdentities, refreshTokens, passwordResetTokens, User, NewUser } from "../schema/auth-schema";
|
|
4
4
|
import {
|
|
5
5
|
UserRepository,
|
|
6
6
|
RoleRepository,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CreateRoleData,
|
|
13
13
|
RefreshTokenInfo,
|
|
14
14
|
PasswordResetTokenInfo,
|
|
15
|
+
UserIdentityData,
|
|
15
16
|
ListUsersOptions,
|
|
16
17
|
PaginatedUsersResult,
|
|
17
18
|
RoleData as Role
|
|
@@ -42,15 +43,64 @@ export class UserService implements UserRepository {
|
|
|
42
43
|
return user || null;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
async
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
async getUserByIdentity(provider: string, providerId: string): Promise<User | null> {
|
|
47
|
+
const result = await this.db.execute(sql`
|
|
48
|
+
SELECT u.*
|
|
49
|
+
FROM rebase.users u
|
|
50
|
+
INNER JOIN rebase.user_identities ui ON u.id = ui.user_id
|
|
51
|
+
WHERE ui.provider = ${provider} AND ui.provider_id = ${providerId}
|
|
52
|
+
LIMIT 1
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
if (result.rows.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
58
|
+
return {
|
|
59
|
+
id: row.id as string,
|
|
60
|
+
email: row.email as string,
|
|
61
|
+
passwordHash: (row.password_hash as string | null) ?? null,
|
|
62
|
+
displayName: (row.display_name as string | null) ?? null,
|
|
63
|
+
photoUrl: (row.photo_url as string | null) ?? null,
|
|
64
|
+
emailVerified: (row.email_verified as boolean | undefined) ?? false,
|
|
65
|
+
emailVerificationToken: (row.email_verification_token as string | null) ?? null,
|
|
66
|
+
emailVerificationSentAt: (row.email_verification_sent_at as Date | null) ?? null,
|
|
67
|
+
createdAt: row.created_at as Date,
|
|
68
|
+
updatedAt: row.updated_at as Date
|
|
69
|
+
} as User;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getUserIdentities(userId: string): Promise<UserIdentityData[]> {
|
|
73
|
+
const result = await this.db.execute(sql`
|
|
74
|
+
SELECT id, user_id, provider, provider_id, profile_data, created_at, updated_at
|
|
75
|
+
FROM rebase.user_identities
|
|
76
|
+
WHERE user_id = ${userId}
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
return result.rows.map((row: Record<string, unknown>) => ({
|
|
80
|
+
id: row.id as string,
|
|
81
|
+
userId: row.user_id as string,
|
|
82
|
+
provider: row.provider as string,
|
|
83
|
+
providerId: row.provider_id as string,
|
|
84
|
+
profileData: (row.profile_data as Record<string, unknown> | null) ?? null,
|
|
85
|
+
createdAt: row.created_at as Date,
|
|
86
|
+
updatedAt: row.updated_at as Date
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
91
|
+
await this.db.insert(userIdentities).values({
|
|
92
|
+
userId,
|
|
93
|
+
provider,
|
|
94
|
+
providerId,
|
|
95
|
+
profileData: profileData || null
|
|
96
|
+
}).onConflictDoNothing({ target: [userIdentities.provider, userIdentities.providerId] });
|
|
48
97
|
}
|
|
49
98
|
|
|
50
99
|
async updateUser(id: string, data: Partial<Omit<NewUser, "id">>): Promise<User | null> {
|
|
51
100
|
const [user] = await this.db
|
|
52
101
|
.update(users)
|
|
53
|
-
.set({ ...data,
|
|
102
|
+
.set({ ...data,
|
|
103
|
+
updatedAt: new Date() })
|
|
54
104
|
.where(eq(users.id, id))
|
|
55
105
|
.returning();
|
|
56
106
|
return user || null;
|
|
@@ -70,6 +120,7 @@ export class UserService implements UserRepository {
|
|
|
70
120
|
const search = options?.search?.trim() || "";
|
|
71
121
|
const orderBy = options?.orderBy || "createdAt";
|
|
72
122
|
const orderDir = options?.orderDir || "desc";
|
|
123
|
+
const roleId = options?.roleId;
|
|
73
124
|
|
|
74
125
|
// Map camelCase field names to snake_case column names
|
|
75
126
|
const columnMap: Record<string, string> = {
|
|
@@ -82,56 +133,54 @@ export class UserService implements UserRepository {
|
|
|
82
133
|
const orderColumn = columnMap[orderBy] || "created_at";
|
|
83
134
|
const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
|
|
84
135
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
136
|
+
const conditions = [];
|
|
137
|
+
if (roleId) {
|
|
138
|
+
conditions.push(sql`EXISTS (SELECT 1 FROM rebase.user_roles ur WHERE ur.user_id = users.id AND ur.role_id = ${roleId})`);
|
|
139
|
+
}
|
|
88
140
|
if (search) {
|
|
89
141
|
const pattern = `%${search}%`;
|
|
142
|
+
conditions.push(sql`(email ILIKE ${pattern} OR display_name ILIKE ${pattern})`);
|
|
143
|
+
}
|
|
90
144
|
|
|
91
|
-
|
|
92
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
93
|
-
WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
|
|
94
|
-
`);
|
|
95
|
-
total = (countResult.rows[0] as { total: number }).total;
|
|
145
|
+
const whereClause = conditions.length > 0 ? sql`WHERE ${sql.join(conditions, sql` AND `)}` : sql``;
|
|
96
146
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
LIMIT ${limit} OFFSET ${offset}
|
|
102
|
-
`);
|
|
103
|
-
rows = dataResult.rows as User[];
|
|
104
|
-
} else {
|
|
105
|
-
const countResult = await this.db.execute(sql`
|
|
106
|
-
SELECT count(*)::int as total FROM rebase.users
|
|
107
|
-
`);
|
|
108
|
-
total = (countResult.rows[0] as { total: number }).total;
|
|
147
|
+
// Sorting: users with roles first if no role filter, then by requested order
|
|
148
|
+
const orderByClause = roleId
|
|
149
|
+
? sql`ORDER BY ${sql.raw(orderColumn)} ${direction}`
|
|
150
|
+
: sql`ORDER BY (SELECT count(*) FROM rebase.user_roles ur WHERE ur.user_id = users.id) DESC, ${sql.raw(orderColumn)} ${direction}`;
|
|
109
151
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
152
|
+
const countResult = await this.db.execute(sql`
|
|
153
|
+
SELECT count(*)::int as total FROM rebase.users
|
|
154
|
+
${whereClause}
|
|
155
|
+
`);
|
|
156
|
+
const total = (countResult.rows[0] as { total: number }).total;
|
|
157
|
+
|
|
158
|
+
const dataResult = await this.db.execute(sql`
|
|
159
|
+
SELECT * FROM rebase.users
|
|
160
|
+
${whereClause}
|
|
161
|
+
${orderByClause}
|
|
162
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
163
|
+
`);
|
|
164
|
+
const rows = dataResult.rows as User[];
|
|
117
165
|
|
|
118
166
|
// Map snake_case rows to camelCase UserData
|
|
119
|
-
const mappedUsers: User[] = rows.map((row: Record<string,
|
|
120
|
-
id: row.id,
|
|
121
|
-
email: row.email,
|
|
122
|
-
passwordHash: row.password_hash ?? row.passwordHash ?? null,
|
|
123
|
-
displayName: row.display_name ?? row.displayName ?? null,
|
|
124
|
-
photoUrl: row.photo_url ?? row.photoUrl ?? null,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
createdAt: row.created_at ?? row.createdAt,
|
|
131
|
-
updatedAt: row.updated_at ?? row.updatedAt
|
|
167
|
+
const mappedUsers: User[] = rows.map((row: Record<string, unknown>) => ({
|
|
168
|
+
id: row.id as string,
|
|
169
|
+
email: row.email as string,
|
|
170
|
+
passwordHash: ((row.password_hash ?? row.passwordHash) as string | null) ?? null,
|
|
171
|
+
displayName: ((row.display_name ?? row.displayName) as string | null) ?? null,
|
|
172
|
+
photoUrl: ((row.photo_url ?? row.photoUrl) as string | null) ?? null,
|
|
173
|
+
emailVerified: ((row.email_verified ?? row.emailVerified) as boolean | undefined) ?? false,
|
|
174
|
+
emailVerificationToken: ((row.email_verification_token ?? row.emailVerificationToken) as string | null) ?? null,
|
|
175
|
+
emailVerificationSentAt: ((row.email_verification_sent_at ?? row.emailVerificationSentAt) as Date | null) ?? null,
|
|
176
|
+
createdAt: (row.created_at ?? row.createdAt) as Date,
|
|
177
|
+
updatedAt: (row.updated_at ?? row.updatedAt) as Date
|
|
132
178
|
})) as User[];
|
|
133
179
|
|
|
134
|
-
return { users: mappedUsers,
|
|
180
|
+
return { users: mappedUsers,
|
|
181
|
+
total,
|
|
182
|
+
limit,
|
|
183
|
+
offset };
|
|
135
184
|
}
|
|
136
185
|
|
|
137
186
|
/**
|
|
@@ -140,7 +189,8 @@ export class UserService implements UserRepository {
|
|
|
140
189
|
async updatePassword(id: string, passwordHash: string): Promise<void> {
|
|
141
190
|
await this.db
|
|
142
191
|
.update(users)
|
|
143
|
-
.set({ passwordHash,
|
|
192
|
+
.set({ passwordHash,
|
|
193
|
+
updatedAt: new Date() })
|
|
144
194
|
.where(eq(users.id, id));
|
|
145
195
|
}
|
|
146
196
|
|
|
@@ -248,7 +298,8 @@ export class UserService implements UserRepository {
|
|
|
248
298
|
if (!user) return null;
|
|
249
299
|
|
|
250
300
|
const roles = await this.getUserRoles(userId);
|
|
251
|
-
return { user,
|
|
301
|
+
return { user,
|
|
302
|
+
roles };
|
|
252
303
|
}
|
|
253
304
|
}
|
|
254
305
|
|
|
@@ -597,8 +648,16 @@ export class PostgresAuthRepository implements AuthRepository {
|
|
|
597
648
|
return this.userService.getUserByEmail(email) as Promise<UserData | null>;
|
|
598
649
|
}
|
|
599
650
|
|
|
600
|
-
async
|
|
601
|
-
return this.userService.
|
|
651
|
+
async getUserByIdentity(provider: string, providerId: string): Promise<UserData | null> {
|
|
652
|
+
return this.userService.getUserByIdentity(provider, providerId) as Promise<UserData | null>;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async getUserIdentities(userId: string): Promise<UserIdentityData[]> {
|
|
656
|
+
return this.userService.getUserIdentities(userId);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async linkUserIdentity(userId: string, provider: string, providerId: string, profileData?: Record<string, unknown>): Promise<void> {
|
|
660
|
+
return this.userService.linkUserIdentity(userId, provider, providerId, profileData);
|
|
602
661
|
}
|
|
603
662
|
|
|
604
663
|
async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
|