@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.
Files changed (147) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +36 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +12 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +10635 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +10643 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +188 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +7 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
  43. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  45. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  46. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
  47. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  48. package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
  49. package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
  51. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  52. package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
  53. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  54. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  55. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  56. package/dist/types/src/controllers/auth.d.ts +117 -0
  57. package/dist/types/src/controllers/client.d.ts +58 -0
  58. package/dist/types/src/controllers/collection_registry.d.ts +44 -0
  59. package/dist/types/src/controllers/customization_controller.d.ts +54 -0
  60. package/dist/types/src/controllers/data.d.ts +141 -0
  61. package/dist/types/src/controllers/data_driver.d.ts +168 -0
  62. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  63. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  64. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  65. package/dist/types/src/controllers/index.d.ts +17 -0
  66. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  67. package/dist/types/src/controllers/navigation.d.ts +213 -0
  68. package/dist/types/src/controllers/registry.d.ts +51 -0
  69. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  70. package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
  71. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  72. package/dist/types/src/controllers/storage.d.ts +173 -0
  73. package/dist/types/src/index.d.ts +4 -0
  74. package/dist/types/src/rebase_context.d.ts +101 -0
  75. package/dist/types/src/types/backend.d.ts +533 -0
  76. package/dist/types/src/types/builders.d.ts +14 -0
  77. package/dist/types/src/types/chips.d.ts +5 -0
  78. package/dist/types/src/types/collections.d.ts +812 -0
  79. package/dist/types/src/types/data_source.d.ts +64 -0
  80. package/dist/types/src/types/entities.d.ts +145 -0
  81. package/dist/types/src/types/entity_actions.d.ts +98 -0
  82. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  83. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  84. package/dist/types/src/types/entity_overrides.d.ts +9 -0
  85. package/dist/types/src/types/entity_views.d.ts +61 -0
  86. package/dist/types/src/types/export_import.d.ts +21 -0
  87. package/dist/types/src/types/index.d.ts +22 -0
  88. package/dist/types/src/types/locales.d.ts +4 -0
  89. package/dist/types/src/types/modify_collections.d.ts +5 -0
  90. package/dist/types/src/types/plugins.d.ts +225 -0
  91. package/dist/types/src/types/properties.d.ts +1091 -0
  92. package/dist/types/src/types/property_config.d.ts +70 -0
  93. package/dist/types/src/types/relations.d.ts +336 -0
  94. package/dist/types/src/types/slots.d.ts +228 -0
  95. package/dist/types/src/types/translations.d.ts +826 -0
  96. package/dist/types/src/types/user_management_delegate.d.ts +120 -0
  97. package/dist/types/src/types/websockets.d.ts +78 -0
  98. package/dist/types/src/users/index.d.ts +2 -0
  99. package/dist/types/src/users/roles.d.ts +22 -0
  100. package/dist/types/src/users/user.d.ts +46 -0
  101. package/jest-all.log +3128 -0
  102. package/jest.log +49 -0
  103. package/package.json +93 -0
  104. package/src/PostgresBackendDriver.ts +1024 -0
  105. package/src/PostgresBootstrapper.ts +232 -0
  106. package/src/auth/ensure-tables.ts +309 -0
  107. package/src/auth/services.ts +740 -0
  108. package/src/cli.ts +347 -0
  109. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  110. package/src/connection.ts +62 -0
  111. package/src/data-transformer.ts +569 -0
  112. package/src/databasePoolManager.ts +84 -0
  113. package/src/history/HistoryService.ts +257 -0
  114. package/src/history/ensure-history-table.ts +45 -0
  115. package/src/index.ts +13 -0
  116. package/src/interfaces.ts +60 -0
  117. package/src/schema/auth-schema.ts +146 -0
  118. package/src/schema/generate-drizzle-schema-logic.ts +618 -0
  119. package/src/schema/generate-drizzle-schema.ts +151 -0
  120. package/src/services/BranchService.ts +237 -0
  121. package/src/services/EntityFetchService.ts +1447 -0
  122. package/src/services/EntityPersistService.ts +351 -0
  123. package/src/services/RelationService.ts +1012 -0
  124. package/src/services/entity-helpers.ts +121 -0
  125. package/src/services/entityService.ts +209 -0
  126. package/src/services/index.ts +13 -0
  127. package/src/services/realtimeService.ts +1005 -0
  128. package/src/utils/drizzle-conditions.ts +999 -0
  129. package/src/websocket.ts +487 -0
  130. package/test/auth-services.test.ts +569 -0
  131. package/test/branchService.test.ts +357 -0
  132. package/test/drizzle-conditions.test.ts +895 -0
  133. package/test/entityService.errors.test.ts +352 -0
  134. package/test/entityService.relations.test.ts +912 -0
  135. package/test/entityService.subcollection-search.test.ts +516 -0
  136. package/test/entityService.test.ts +977 -0
  137. package/test/generate-drizzle-schema.test.ts +795 -0
  138. package/test/historyService.test.ts +126 -0
  139. package/test/postgresDataDriver.test.ts +556 -0
  140. package/test/realtimeService.test.ts +276 -0
  141. package/test/relations.test.ts +662 -0
  142. package/test_drizzle_mock.js +3 -0
  143. package/test_find_changed.mjs +30 -0
  144. package/test_output.txt +3145 -0
  145. package/tsconfig.json +49 -0
  146. package/tsconfig.prod.json +20 -0
  147. package/vite.config.ts +82 -0
@@ -0,0 +1,740 @@
1
+ import { eq, sql } from "drizzle-orm";
2
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+ import { users, refreshTokens, passwordResetTokens, User, NewUser } from "../schema/auth-schema";
4
+ import {
5
+ UserRepository,
6
+ RoleRepository,
7
+ TokenRepository,
8
+ AuthRepository,
9
+ UserData,
10
+ CreateUserData,
11
+ RoleData,
12
+ CreateRoleData,
13
+ RefreshTokenInfo,
14
+ PasswordResetTokenInfo,
15
+ ListUsersOptions,
16
+ PaginatedUsersResult,
17
+ RoleData as Role
18
+ // @ts-ignore
19
+ } from "@rebasepro/server-core";
20
+
21
+ export type { Role };
22
+
23
+ /**
24
+ * PostgreSQL implementation of UserRepository.
25
+ * Handles all user-related database operations using Drizzle ORM.
26
+ */
27
+ export class UserService implements UserRepository {
28
+ constructor(private db: NodePgDatabase) { }
29
+
30
+ async createUser(data: NewUser): Promise<User> {
31
+ const [user] = await this.db.insert(users).values(data).returning();
32
+ return user;
33
+ }
34
+
35
+ async getUserById(id: string): Promise<User | null> {
36
+ const [user] = await this.db.select().from(users).where(eq(users.id, id));
37
+ return user || null;
38
+ }
39
+
40
+ async getUserByEmail(email: string): Promise<User | null> {
41
+ const [user] = await this.db.select().from(users).where(eq(users.email, email.toLowerCase()));
42
+ return user || null;
43
+ }
44
+
45
+ async getUserByGoogleId(googleId: string): Promise<User | null> {
46
+ const [user] = await this.db.select().from(users).where(eq(users.googleId, googleId));
47
+ return user || null;
48
+ }
49
+
50
+ async updateUser(id: string, data: Partial<Omit<NewUser, "id">>): Promise<User | null> {
51
+ const [user] = await this.db
52
+ .update(users)
53
+ .set({ ...data, updatedAt: new Date() })
54
+ .where(eq(users.id, id))
55
+ .returning();
56
+ return user || null;
57
+ }
58
+
59
+ async deleteUser(id: string): Promise<void> {
60
+ await this.db.delete(users).where(eq(users.id, id));
61
+ }
62
+
63
+ async listUsers(): Promise<User[]> {
64
+ return this.db.select().from(users);
65
+ }
66
+
67
+ async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
68
+ const limit = options?.limit ?? 25;
69
+ const offset = options?.offset ?? 0;
70
+ const search = options?.search?.trim() || "";
71
+ const orderBy = options?.orderBy || "createdAt";
72
+ const orderDir = options?.orderDir || "desc";
73
+
74
+ // Map camelCase field names to snake_case column names
75
+ const columnMap: Record<string, string> = {
76
+ email: "email",
77
+ displayName: "display_name",
78
+ createdAt: "created_at",
79
+ updatedAt: "updated_at",
80
+ provider: "provider"
81
+ };
82
+ const orderColumn = columnMap[orderBy] || "created_at";
83
+ const direction = orderDir === "asc" ? sql`ASC` : sql`DESC`;
84
+
85
+ let rows: User[];
86
+ let total: number;
87
+
88
+ if (search) {
89
+ const pattern = `%${search}%`;
90
+
91
+ const countResult = await this.db.execute(sql`
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;
96
+
97
+ const dataResult = await this.db.execute(sql`
98
+ SELECT * FROM rebase.users
99
+ WHERE email ILIKE ${pattern} OR display_name ILIKE ${pattern}
100
+ ORDER BY ${sql.raw(orderColumn)} ${direction}
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;
109
+
110
+ const dataResult = await this.db.execute(sql`
111
+ SELECT * FROM rebase.users
112
+ ORDER BY ${sql.raw(orderColumn)} ${direction}
113
+ LIMIT ${limit} OFFSET ${offset}
114
+ `);
115
+ rows = dataResult.rows as User[];
116
+ }
117
+
118
+ // Map snake_case rows to camelCase UserData
119
+ const mappedUsers: User[] = rows.map((row: Record<string, any>) => ({
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
+ provider: row.provider,
126
+ googleId: row.google_id ?? row.googleId ?? null,
127
+ emailVerified: row.email_verified ?? row.emailVerified ?? false,
128
+ emailVerificationToken: row.email_verification_token ?? row.emailVerificationToken ?? null,
129
+ emailVerificationSentAt: row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null,
130
+ createdAt: row.created_at ?? row.createdAt,
131
+ updatedAt: row.updated_at ?? row.updatedAt
132
+ })) as User[];
133
+
134
+ return { users: mappedUsers, total, limit, offset };
135
+ }
136
+
137
+ /**
138
+ * Update user's password hash
139
+ */
140
+ async updatePassword(id: string, passwordHash: string): Promise<void> {
141
+ await this.db
142
+ .update(users)
143
+ .set({ passwordHash, updatedAt: new Date() })
144
+ .where(eq(users.id, id));
145
+ }
146
+
147
+ /**
148
+ * Set email verification status
149
+ */
150
+ async setEmailVerified(id: string, verified: boolean): Promise<void> {
151
+ await this.db
152
+ .update(users)
153
+ .set({
154
+ emailVerified: verified,
155
+ emailVerificationToken: null,
156
+ updatedAt: new Date()
157
+ })
158
+ .where(eq(users.id, id));
159
+ }
160
+
161
+ /**
162
+ * Set email verification token
163
+ */
164
+ async setVerificationToken(id: string, token: string | null): Promise<void> {
165
+ await this.db
166
+ .update(users)
167
+ .set({
168
+ emailVerificationToken: token,
169
+ emailVerificationSentAt: token ? new Date() : null,
170
+ updatedAt: new Date()
171
+ })
172
+ .where(eq(users.id, id));
173
+ }
174
+
175
+ /**
176
+ * Find user by email verification token
177
+ */
178
+ async getUserByVerificationToken(token: string): Promise<User | null> {
179
+ const [user] = await this.db
180
+ .select()
181
+ .from(users)
182
+ .where(eq(users.emailVerificationToken, token));
183
+ return user || null;
184
+ }
185
+
186
+ /**
187
+ * Get roles for a user from database
188
+ */
189
+ async getUserRoles(userId: string): Promise<Role[]> {
190
+ const result = await this.db.execute(sql`
191
+ SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
192
+ FROM rebase.roles r
193
+ INNER JOIN rebase.user_roles ur ON r.id = ur.role_id
194
+ WHERE ur.user_id = ${userId}
195
+ `);
196
+
197
+ return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null }>).map(row => ({
198
+ id: row.id,
199
+ name: row.name,
200
+ isAdmin: row.is_admin,
201
+ defaultPermissions: row.default_permissions,
202
+ collectionPermissions: row.collection_permissions,
203
+ config: row.config
204
+ }));
205
+ }
206
+
207
+ /**
208
+ * Get role IDs for a user
209
+ */
210
+ async getUserRoleIds(userId: string): Promise<string[]> {
211
+ const roles = await this.getUserRoles(userId);
212
+ return roles.map(r => r.id);
213
+ }
214
+
215
+ /**
216
+ * Set roles for a user
217
+ */
218
+ async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
219
+ // Delete existing roles
220
+ await this.db.execute(sql`DELETE FROM rebase.user_roles WHERE user_id = ${userId}`);
221
+
222
+ // Insert new roles
223
+ for (const roleId of roleIds) {
224
+ await this.db.execute(sql`
225
+ INSERT INTO rebase.user_roles (user_id, role_id)
226
+ VALUES (${userId}, ${roleId})
227
+ ON CONFLICT DO NOTHING
228
+ `);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Assign a specific role to new user
234
+ */
235
+ async assignDefaultRole(userId: string, roleId: string): Promise<void> {
236
+ await this.db.execute(sql`
237
+ INSERT INTO rebase.user_roles (user_id, role_id)
238
+ VALUES (${userId}, ${roleId})
239
+ ON CONFLICT DO NOTHING
240
+ `);
241
+ }
242
+
243
+ /**
244
+ * Get user with their roles
245
+ */
246
+ async getUserWithRoles(userId: string): Promise<{ user: User; roles: Role[] } | null> {
247
+ const user = await this.getUserById(userId);
248
+ if (!user) return null;
249
+
250
+ const roles = await this.getUserRoles(userId);
251
+ return { user, roles };
252
+ }
253
+ }
254
+
255
+ /**
256
+ * PostgreSQL implementation of RoleRepository.
257
+ * Handles all role-related database operations using Drizzle ORM.
258
+ */
259
+ export class RoleService implements RoleRepository {
260
+ constructor(private db: NodePgDatabase) { }
261
+
262
+ async getRoleById(id: string): Promise<Role | null> {
263
+ const result = await this.db.execute(sql`
264
+ SELECT id, name, is_admin, default_permissions, collection_permissions, config
265
+ FROM rebase.roles
266
+ WHERE id = ${id}
267
+ `);
268
+
269
+ if (result.rows.length === 0) return null;
270
+
271
+ const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null };
272
+ return {
273
+ id: row.id,
274
+ name: row.name,
275
+ isAdmin: row.is_admin,
276
+ defaultPermissions: row.default_permissions,
277
+ collectionPermissions: row.collection_permissions,
278
+ config: row.config
279
+ };
280
+ }
281
+
282
+ async listRoles(): Promise<Role[]> {
283
+ const result = await this.db.execute(sql`
284
+ SELECT id, name, is_admin, default_permissions, collection_permissions, config
285
+ FROM rebase.roles
286
+ ORDER BY name
287
+ `);
288
+
289
+ return (result.rows as Array<{ id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null }>).map(row => ({
290
+ id: row.id,
291
+ name: row.name,
292
+ isAdmin: row.is_admin,
293
+ defaultPermissions: row.default_permissions,
294
+ collectionPermissions: row.collection_permissions,
295
+ config: row.config
296
+ }));
297
+ }
298
+
299
+ async createRole(data: Omit<Role, "isAdmin" | "collectionPermissions"> & { isAdmin?: boolean; collectionPermissions?: Role["collectionPermissions"] }): Promise<Role> {
300
+ const result = await this.db.execute(sql`
301
+ INSERT INTO rebase.roles (id, name, is_admin, default_permissions, collection_permissions, config)
302
+ VALUES (
303
+ ${data.id},
304
+ ${data.name},
305
+ ${data.isAdmin ?? false},
306
+ ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
307
+ ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
308
+ ${data.config ? JSON.stringify(data.config) : null}::jsonb
309
+ )
310
+ RETURNING id, name, is_admin, default_permissions, collection_permissions, config
311
+ `);
312
+
313
+ const row = result.rows[0] as { id: string; name: string; is_admin: boolean; default_permissions: Record<string, boolean> | null; collection_permissions: Record<string, Record<string, boolean>> | null; config: Record<string, unknown> | null };
314
+ return {
315
+ id: row.id,
316
+ name: row.name,
317
+ isAdmin: row.is_admin,
318
+ defaultPermissions: row.default_permissions,
319
+ collectionPermissions: row.collection_permissions,
320
+ config: row.config
321
+ };
322
+ }
323
+
324
+ async updateRole(id: string, data: Partial<Omit<Role, "id">>): Promise<Role | null> {
325
+ // For now, use simpler approach
326
+ const existing = await this.getRoleById(id);
327
+ if (!existing) return null;
328
+
329
+ await this.db.execute(sql`
330
+ UPDATE rebase.roles
331
+ SET
332
+ name = ${data.name ?? existing.name},
333
+ is_admin = ${data.isAdmin ?? existing.isAdmin},
334
+ default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
335
+ collection_permissions = ${data.collectionPermissions !== undefined ? (data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null) : (existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null)}::jsonb,
336
+ config = ${data.config ? JSON.stringify(data.config) : (existing.config ? JSON.stringify(existing.config) : null)}::jsonb
337
+ WHERE id = ${id}
338
+ `);
339
+
340
+ return this.getRoleById(id);
341
+ }
342
+
343
+ async deleteRole(id: string): Promise<void> {
344
+ await this.db.execute(sql`DELETE FROM rebase.roles WHERE id = ${id}`);
345
+ }
346
+ }
347
+
348
+ export class RefreshTokenService {
349
+ constructor(private db: NodePgDatabase) { }
350
+
351
+ async createToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
352
+ // Fallback to empty string because UNIQUE constraints treat NULLs as strictly distinct in standard Postgres.
353
+ // We want (userId, NULL, NULL) to collide and overwrite, so we map undefined/null to empty strings.
354
+ const safeUserAgent = userAgent || "";
355
+ const safeIpAddress = ipAddress || "";
356
+
357
+ // Delete any existing session for this user/device combo, then insert.
358
+ // This approach doesn't require the unique_device_session constraint to exist.
359
+ await this.db.execute(sql`
360
+ DELETE FROM rebase.refresh_tokens
361
+ WHERE user_id = ${userId}
362
+ AND user_agent = ${safeUserAgent}
363
+ AND ip_address = ${safeIpAddress}
364
+ `);
365
+
366
+ await this.db.insert(refreshTokens)
367
+ .values({
368
+ userId,
369
+ tokenHash,
370
+ expiresAt,
371
+ userAgent: safeUserAgent,
372
+ ipAddress: safeIpAddress
373
+ });
374
+ }
375
+
376
+ async findByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
377
+ const [token] = await this.db
378
+ .select({
379
+ id: refreshTokens.id,
380
+ userId: refreshTokens.userId,
381
+ tokenHash: refreshTokens.tokenHash,
382
+ expiresAt: refreshTokens.expiresAt,
383
+ createdAt: refreshTokens.createdAt,
384
+ userAgent: refreshTokens.userAgent,
385
+ ipAddress: refreshTokens.ipAddress
386
+ })
387
+ .from(refreshTokens)
388
+ .where(eq(refreshTokens.tokenHash, tokenHash));
389
+
390
+ return token || null;
391
+ }
392
+
393
+ async deleteByHash(tokenHash: string): Promise<void> {
394
+ await this.db.delete(refreshTokens).where(eq(refreshTokens.tokenHash, tokenHash));
395
+ }
396
+
397
+ async deleteAllForUser(userId: string): Promise<void> {
398
+ await this.db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));
399
+ }
400
+
401
+ async listForUser(userId: string): Promise<RefreshTokenInfo[]> {
402
+ const tokens = await this.db
403
+ .select({
404
+ id: refreshTokens.id,
405
+ userId: refreshTokens.userId,
406
+ tokenHash: refreshTokens.tokenHash,
407
+ expiresAt: refreshTokens.expiresAt,
408
+ createdAt: refreshTokens.createdAt,
409
+ userAgent: refreshTokens.userAgent,
410
+ ipAddress: refreshTokens.ipAddress
411
+ })
412
+ .from(refreshTokens)
413
+ .where(eq(refreshTokens.userId, userId))
414
+ .orderBy(refreshTokens.createdAt);
415
+
416
+ return tokens;
417
+ }
418
+
419
+ async deleteById(id: string, userId: string): Promise<void> {
420
+ await this.db.delete(refreshTokens)
421
+ .where(sql`${refreshTokens.id} = ${id} AND ${refreshTokens.userId} = ${userId}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Password reset token service
427
+ */
428
+ export class PasswordResetTokenService {
429
+ constructor(private db: NodePgDatabase) { }
430
+
431
+ /**
432
+ * Create a password reset token
433
+ */
434
+ async createToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void> {
435
+ // Delete any existing unused tokens for this user
436
+ await this.db.execute(sql`
437
+ DELETE FROM rebase.password_reset_tokens
438
+ WHERE user_id = ${userId} AND used_at IS NULL
439
+ `);
440
+
441
+ await this.db.insert(passwordResetTokens).values({
442
+ userId,
443
+ tokenHash,
444
+ expiresAt
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Find a valid (not expired, not used) token by hash
450
+ */
451
+ async findValidByHash(tokenHash: string): Promise<{ userId: string; expiresAt: Date } | null> {
452
+ const [token] = await this.db
453
+ .select({
454
+ userId: passwordResetTokens.userId,
455
+ expiresAt: passwordResetTokens.expiresAt
456
+ })
457
+ .from(passwordResetTokens)
458
+ .where(eq(passwordResetTokens.tokenHash, tokenHash));
459
+
460
+ if (!token) return null;
461
+
462
+ // Check if expired or used
463
+ const result = await this.db.execute(sql`
464
+ SELECT user_id, expires_at
465
+ FROM rebase.password_reset_tokens
466
+ WHERE token_hash = ${tokenHash}
467
+ AND used_at IS NULL
468
+ AND expires_at > NOW()
469
+ `);
470
+
471
+ if (result.rows.length === 0) return null;
472
+
473
+ const row = result.rows[0] as { user_id: string; expires_at: string | number | Date };
474
+ return {
475
+ userId: row.user_id,
476
+ expiresAt: new Date(row.expires_at)
477
+ };
478
+ }
479
+
480
+ /**
481
+ * Mark token as used
482
+ */
483
+ async markAsUsed(tokenHash: string): Promise<void> {
484
+ await this.db
485
+ .update(passwordResetTokens)
486
+ .set({ usedAt: new Date() })
487
+ .where(eq(passwordResetTokens.tokenHash, tokenHash));
488
+ }
489
+
490
+ /**
491
+ * Delete all tokens for a user
492
+ */
493
+ async deleteAllForUser(userId: string): Promise<void> {
494
+ await this.db.delete(passwordResetTokens).where(eq(passwordResetTokens.userId, userId));
495
+ }
496
+
497
+ /**
498
+ * Clean up expired tokens
499
+ */
500
+ async deleteExpired(): Promise<void> {
501
+ await this.db.execute(sql`
502
+ DELETE FROM rebase.password_reset_tokens
503
+ WHERE expires_at < NOW()
504
+ `);
505
+ }
506
+ }
507
+
508
+ /**
509
+ * PostgreSQL implementation of TokenRepository.
510
+ * Combines refresh token and password reset token operations.
511
+ */
512
+ export class PostgresTokenRepository implements TokenRepository {
513
+ private refreshTokenService: RefreshTokenService;
514
+ private passwordResetTokenService: PasswordResetTokenService;
515
+
516
+ constructor(private db: NodePgDatabase) {
517
+ this.refreshTokenService = new RefreshTokenService(db);
518
+ this.passwordResetTokenService = new PasswordResetTokenService(db);
519
+ }
520
+
521
+ // Refresh token operations
522
+
523
+ async createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
524
+ await this.refreshTokenService.createToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
525
+ }
526
+
527
+ async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
528
+ return this.refreshTokenService.findByHash(tokenHash);
529
+ }
530
+
531
+ async deleteRefreshToken(tokenHash: string): Promise<void> {
532
+ await this.refreshTokenService.deleteByHash(tokenHash);
533
+ }
534
+
535
+ async deleteAllRefreshTokensForUser(userId: string): Promise<void> {
536
+ await this.refreshTokenService.deleteAllForUser(userId);
537
+ }
538
+
539
+ async listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]> {
540
+ return this.refreshTokenService.listForUser(userId);
541
+ }
542
+
543
+ async deleteRefreshTokenById(id: string, userId: string): Promise<void> {
544
+ await this.refreshTokenService.deleteById(id, userId);
545
+ }
546
+
547
+ // Password reset token operations
548
+
549
+ async createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void> {
550
+ await this.passwordResetTokenService.createToken(userId, tokenHash, expiresAt);
551
+ }
552
+
553
+ async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
554
+ return this.passwordResetTokenService.findValidByHash(tokenHash);
555
+ }
556
+
557
+ async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {
558
+ await this.passwordResetTokenService.markAsUsed(tokenHash);
559
+ }
560
+
561
+ async deleteAllPasswordResetTokensForUser(userId: string): Promise<void> {
562
+ await this.passwordResetTokenService.deleteAllForUser(userId);
563
+ }
564
+
565
+ async deleteExpiredTokens(): Promise<void> {
566
+ await this.passwordResetTokenService.deleteExpired();
567
+ }
568
+ }
569
+
570
+ /**
571
+ * PostgreSQL implementation of AuthRepository.
572
+ * Combines user, role, and token repository operations.
573
+ * This provides a convenient single-class interface for all auth operations.
574
+ */
575
+ export class PostgresAuthRepository implements AuthRepository {
576
+ private userService: UserService;
577
+ private roleService: RoleService;
578
+ private tokenRepository: PostgresTokenRepository;
579
+
580
+ constructor(private db: NodePgDatabase) {
581
+ this.userService = new UserService(db);
582
+ this.roleService = new RoleService(db);
583
+ this.tokenRepository = new PostgresTokenRepository(db);
584
+ }
585
+
586
+ // User operations (delegate to UserService)
587
+
588
+ async createUser(data: CreateUserData): Promise<UserData> {
589
+ return this.userService.createUser(data as NewUser) as Promise<UserData>;
590
+ }
591
+
592
+ async getUserById(id: string): Promise<UserData | null> {
593
+ return this.userService.getUserById(id) as Promise<UserData | null>;
594
+ }
595
+
596
+ async getUserByEmail(email: string): Promise<UserData | null> {
597
+ return this.userService.getUserByEmail(email) as Promise<UserData | null>;
598
+ }
599
+
600
+ async getUserByGoogleId(googleId: string): Promise<UserData | null> {
601
+ return this.userService.getUserByGoogleId(googleId) as Promise<UserData | null>;
602
+ }
603
+
604
+ async updateUser(id: string, data: Partial<Omit<CreateUserData, "id">>): Promise<UserData | null> {
605
+ return this.userService.updateUser(id, data) as Promise<UserData | null>;
606
+ }
607
+
608
+ async deleteUser(id: string): Promise<void> {
609
+ await this.userService.deleteUser(id);
610
+ }
611
+
612
+ async listUsers(): Promise<UserData[]> {
613
+ return this.userService.listUsers() as Promise<UserData[]>;
614
+ }
615
+
616
+ async listUsersPaginated(options?: ListUsersOptions): Promise<PaginatedUsersResult> {
617
+ return this.userService.listUsersPaginated(options);
618
+ }
619
+
620
+ async updatePassword(id: string, passwordHash: string): Promise<void> {
621
+ await this.userService.updatePassword(id, passwordHash);
622
+ }
623
+
624
+ async setEmailVerified(id: string, verified: boolean): Promise<void> {
625
+ await this.userService.setEmailVerified(id, verified);
626
+ }
627
+
628
+ async setVerificationToken(id: string, token: string | null): Promise<void> {
629
+ await this.userService.setVerificationToken(id, token);
630
+ }
631
+
632
+ async getUserByVerificationToken(token: string): Promise<UserData | null> {
633
+ return this.userService.getUserByVerificationToken(token) as Promise<UserData | null>;
634
+ }
635
+
636
+ async getUserRoles(userId: string): Promise<RoleData[]> {
637
+ return this.userService.getUserRoles(userId);
638
+ }
639
+
640
+ async getUserRoleIds(userId: string): Promise<string[]> {
641
+ return this.userService.getUserRoleIds(userId);
642
+ }
643
+
644
+ async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
645
+ await this.userService.setUserRoles(userId, roleIds);
646
+ }
647
+
648
+ async assignDefaultRole(userId: string, roleId: string): Promise<void> {
649
+ await this.userService.assignDefaultRole(userId, roleId);
650
+ }
651
+
652
+ async getUserWithRoles(userId: string): Promise<{ user: UserData; roles: RoleData[] } | null> {
653
+ const result = await this.userService.getUserWithRoles(userId);
654
+ return result as { user: UserData; roles: RoleData[] } | null;
655
+ }
656
+
657
+ // Role operations (delegate to RoleService)
658
+
659
+ async getRoleById(id: string): Promise<RoleData | null> {
660
+ return this.roleService.getRoleById(id);
661
+ }
662
+
663
+ async listRoles(): Promise<RoleData[]> {
664
+ return this.roleService.listRoles();
665
+ }
666
+
667
+ async createRole(data: CreateRoleData): Promise<RoleData> {
668
+ return this.roleService.createRole({
669
+ ...data,
670
+ defaultPermissions: data.defaultPermissions ?? null,
671
+ collectionPermissions: data.collectionPermissions ?? null,
672
+ config: data.config ?? null
673
+ });
674
+ }
675
+
676
+ async updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null> {
677
+ return this.roleService.updateRole(id, data);
678
+ }
679
+
680
+ async deleteRole(id: string): Promise<void> {
681
+ await this.roleService.deleteRole(id);
682
+ }
683
+
684
+ // Token operations (delegate to PostgresTokenRepository)
685
+
686
+ async createRefreshToken(userId: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void> {
687
+ await this.tokenRepository.createRefreshToken(userId, tokenHash, expiresAt, userAgent, ipAddress);
688
+ }
689
+
690
+ async findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null> {
691
+ return this.tokenRepository.findRefreshTokenByHash(tokenHash);
692
+ }
693
+
694
+ async deleteRefreshToken(tokenHash: string): Promise<void> {
695
+ await this.tokenRepository.deleteRefreshToken(tokenHash);
696
+ }
697
+
698
+ async deleteAllRefreshTokensForUser(userId: string): Promise<void> {
699
+ await this.tokenRepository.deleteAllRefreshTokensForUser(userId);
700
+ }
701
+
702
+ async listRefreshTokensForUser(userId: string): Promise<RefreshTokenInfo[]> {
703
+ return this.tokenRepository.listRefreshTokensForUser(userId);
704
+ }
705
+
706
+ async deleteRefreshTokenById(id: string, userId: string): Promise<void> {
707
+ await this.tokenRepository.deleteRefreshTokenById(id, userId);
708
+ }
709
+
710
+ async createPasswordResetToken(userId: string, tokenHash: string, expiresAt: Date): Promise<void> {
711
+ await this.tokenRepository.createPasswordResetToken(userId, tokenHash, expiresAt);
712
+ }
713
+
714
+ async findValidPasswordResetToken(tokenHash: string): Promise<PasswordResetTokenInfo | null> {
715
+ return this.tokenRepository.findValidPasswordResetToken(tokenHash);
716
+ }
717
+
718
+ async markPasswordResetTokenUsed(tokenHash: string): Promise<void> {
719
+ await this.tokenRepository.markPasswordResetTokenUsed(tokenHash);
720
+ }
721
+
722
+ async deleteAllPasswordResetTokensForUser(userId: string): Promise<void> {
723
+ await this.tokenRepository.deleteAllPasswordResetTokensForUser(userId);
724
+ }
725
+
726
+ async deleteExpiredTokens(): Promise<void> {
727
+ await this.tokenRepository.deleteExpiredTokens();
728
+ }
729
+ }
730
+
731
+ // =============================================================================
732
+ // PostgreSQL Type Aliases (for consistent naming with other implementations)
733
+ // =============================================================================
734
+
735
+ /** PostgreSQL user repository implementation */
736
+ export type PostgresUserRepository = UserService;
737
+
738
+ /** PostgreSQL role repository implementation */
739
+ export type PostgresRoleRepository = RoleService;
740
+