@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

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