@friggframework/core 2.0.0-next.41 → 2.0.0-next.42

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/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +25 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +121 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +321 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  134. package/prisma-postgresql/schema.prisma +303 -0
  135. package/syncs/manager.js +468 -443
  136. package/syncs/repositories/sync-repository-factory.js +38 -0
  137. package/syncs/repositories/sync-repository-interface.js +109 -0
  138. package/syncs/repositories/sync-repository-mongo.js +239 -0
  139. package/syncs/repositories/sync-repository-postgres.js +319 -0
  140. package/syncs/sync.js +0 -1
  141. package/token/repositories/token-repository-factory.js +33 -0
  142. package/token/repositories/token-repository-interface.js +131 -0
  143. package/token/repositories/token-repository-mongo.js +212 -0
  144. package/token/repositories/token-repository-postgres.js +257 -0
  145. package/token/repositories/token-repository.js +219 -0
  146. package/types/integrations/index.d.ts +2 -6
  147. package/types/module-plugin/index.d.ts +5 -57
  148. package/types/syncs/index.d.ts +0 -2
  149. package/user/repositories/user-repository-factory.js +46 -0
  150. package/user/repositories/user-repository-interface.js +198 -0
  151. package/user/repositories/user-repository-mongo.js +250 -0
  152. package/user/repositories/user-repository-postgres.js +311 -0
  153. package/user/tests/doubles/test-user-repository.js +72 -0
  154. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  155. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  156. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  157. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  158. package/user/tests/use-cases/login-user.test.js +140 -0
  159. package/user/use-cases/create-individual-user.js +61 -0
  160. package/user/use-cases/create-organization-user.js +47 -0
  161. package/user/use-cases/create-token-for-user-id.js +30 -0
  162. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  163. package/user/use-cases/login-user.js +122 -0
  164. package/user/user.js +77 -0
  165. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  166. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  167. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  168. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  169. package/websocket/repositories/websocket-connection-repository.js +160 -0
  170. package/database/models/State.js +0 -9
  171. package/database/models/Token.js +0 -70
  172. package/database/mongo.js +0 -171
  173. package/encrypt/Cryptor.test.js +0 -32
  174. package/encrypt/encrypt.js +0 -104
  175. package/encrypt/encrypt.test.js +0 -1069
  176. package/handlers/routers/middleware/loadUser.js +0 -15
  177. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  178. package/integrations/create-frigg-backend.js +0 -31
  179. package/integrations/integration-factory.js +0 -251
  180. package/integrations/integration-mapping.js +0 -43
  181. package/integrations/integration-model.js +0 -46
  182. package/integrations/integration-user.js +0 -144
  183. package/integrations/test/integration-base.test.js +0 -144
  184. package/module-plugin/auther.js +0 -393
  185. package/module-plugin/credential.js +0 -22
  186. package/module-plugin/entity-manager.js +0 -70
  187. package/module-plugin/manager.js +0 -169
  188. package/module-plugin/module-factory.js +0 -61
  189. package/module-plugin/test/auther.test.js +0 -97
  190. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  191. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  192. /package/{module-plugin → modules}/requester/basic.js +0 -0
  193. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  194. /package/{module-plugin → modules}/requester/requester.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  196. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,250 @@
1
+ //todo: this repository is tightly coupled to the token repository.
2
+ const { prisma } = require('../../database/prisma');
3
+ const {
4
+ createTokenRepository,
5
+ } = require('../../token/repositories/token-repository-factory');
6
+ const { UserRepositoryInterface } = require('./user-repository-interface');
7
+
8
+ /**
9
+ * MongoDB User Repository Adapter
10
+ * Handles user operations with discriminator pattern support
11
+ *
12
+ * MongoDB-specific characteristics:
13
+ * - Uses String IDs (ObjectId)
14
+ * - No ID conversion needed (IDs are already strings)
15
+ * - IndividualUser/OrganizationUser discriminators → User model with type field
16
+ */
17
+ class UserRepositoryMongo extends UserRepositoryInterface {
18
+ constructor() {
19
+ super();
20
+ this.prisma = prisma;
21
+ this.tokenRepository = createTokenRepository();
22
+ }
23
+
24
+ /**
25
+ * Get session token from base64 buffer token
26
+ * Delegates to TokenRepository
27
+ *
28
+ * @param {string} token - Base64 buffer token
29
+ * @returns {Promise<Object>} Session token object with string IDs
30
+ */
31
+ async getSessionToken(token) {
32
+ const jsonToken =
33
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
34
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
35
+ jsonToken
36
+ );
37
+ return sessionToken;
38
+ }
39
+
40
+ /**
41
+ * Find organization user by ID
42
+ * Replaces: OrganizationUser.findById(userId)
43
+ *
44
+ * @param {string} userId - User ID
45
+ * @returns {Promise<Object|null>} User object with string IDs or null
46
+ */
47
+ async findOrganizationUserById(userId) {
48
+ return await this.prisma.user.findFirst({
49
+ where: {
50
+ id: userId,
51
+ type: 'ORGANIZATION',
52
+ },
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Find individual user by ID
58
+ * Replaces: IndividualUser.findById(userId)
59
+ *
60
+ * @param {string} userId - User ID
61
+ * @returns {Promise<Object|null>} User object with string IDs or null
62
+ */
63
+ async findIndividualUserById(userId) {
64
+ return await this.prisma.user.findFirst({
65
+ where: {
66
+ id: userId,
67
+ type: 'INDIVIDUAL',
68
+ },
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Create token with expiration
74
+ * Delegates to TokenRepository
75
+ *
76
+ * @param {string} userId - User ID
77
+ * @param {string} rawToken - Raw unhashed token
78
+ * @param {number} minutes - Minutes until expiration (default 120)
79
+ * @returns {Promise<string>} Base64 buffer token
80
+ */
81
+ async createToken(userId, rawToken, minutes = 120) {
82
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
83
+ userId,
84
+ rawToken,
85
+ minutes
86
+ );
87
+ return this.tokenRepository.createBase64BufferToken(
88
+ createdToken,
89
+ rawToken
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Create individual user
95
+ * Replaces: IndividualUser.create(params)
96
+ *
97
+ * @param {Object} params - User creation parameters
98
+ * @returns {Promise<Object>} Created user object with string IDs
99
+ */
100
+ async createIndividualUser(params) {
101
+ return await this.prisma.user.create({
102
+ data: {
103
+ type: 'INDIVIDUAL',
104
+ email: params.email,
105
+ username: params.username,
106
+ hashword: params.hashword,
107
+ appUserId: params.appUserId,
108
+ organizationId: params.organization || params.organizationId,
109
+ },
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Create organization user
115
+ * Replaces: OrganizationUser.create(params)
116
+ *
117
+ * @param {Object} params - Organization creation parameters
118
+ * @returns {Promise<Object>} Created organization object with string IDs
119
+ */
120
+ async createOrganizationUser(params) {
121
+ return await this.prisma.user.create({
122
+ data: {
123
+ type: 'ORGANIZATION',
124
+ appOrgId: params.appOrgId,
125
+ name: params.name,
126
+ },
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Find individual user by username
132
+ * Replaces: IndividualUser.findOne({ username })
133
+ *
134
+ * @param {string} username - Username to search for
135
+ * @returns {Promise<Object|null>} User object with string IDs or null
136
+ */
137
+ async findIndividualUserByUsername(username) {
138
+ return await this.prisma.user.findFirst({
139
+ where: {
140
+ type: 'INDIVIDUAL',
141
+ username,
142
+ },
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Find individual user by app user ID
148
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
149
+ *
150
+ * @param {string} appUserId - App user ID to search for
151
+ * @returns {Promise<Object|null>} User object with string IDs or null
152
+ */
153
+ async findIndividualUserByAppUserId(appUserId) {
154
+ return await this.prisma.user.findFirst({
155
+ where: {
156
+ type: 'INDIVIDUAL',
157
+ appUserId,
158
+ },
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Find organization user by app org ID
164
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
165
+ *
166
+ * @param {string} appOrgId - App organization ID to search for
167
+ * @returns {Promise<Object|null>} User object with string IDs or null
168
+ */
169
+ async findOrganizationUserByAppOrgId(appOrgId) {
170
+ return await this.prisma.user.findFirst({
171
+ where: {
172
+ type: 'ORGANIZATION',
173
+ appOrgId,
174
+ },
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Find user by ID (any type)
180
+ * @param {string} userId - User ID
181
+ * @returns {Promise<Object|null>} User object with string IDs or null
182
+ */
183
+ async findUserById(userId) {
184
+ return await this.prisma.user.findUnique({
185
+ where: { id: userId },
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Find individual user by email
191
+ * @param {string} email - Email to search for
192
+ * @returns {Promise<Object|null>} User object with string IDs or null
193
+ */
194
+ async findIndividualUserByEmail(email) {
195
+ return await this.prisma.user.findFirst({
196
+ where: {
197
+ type: 'INDIVIDUAL',
198
+ email,
199
+ },
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Update individual user
205
+ * @param {string} userId - User ID
206
+ * @param {Object} updates - Fields to update
207
+ * @returns {Promise<Object>} Updated user object with string IDs
208
+ */
209
+ async updateIndividualUser(userId, updates) {
210
+ return await this.prisma.user.update({
211
+ where: { id: userId },
212
+ data: updates,
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Update organization user
218
+ * @param {string} userId - User ID
219
+ * @param {Object} updates - Fields to update
220
+ * @returns {Promise<Object>} Updated user object with string IDs
221
+ */
222
+ async updateOrganizationUser(userId, updates) {
223
+ return await this.prisma.user.update({
224
+ where: { id: userId },
225
+ data: updates,
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Delete user by ID
231
+ * @param {string} userId - User ID to delete
232
+ * @returns {Promise<boolean>} True if deleted successfully
233
+ */
234
+ async deleteUser(userId) {
235
+ try {
236
+ await this.prisma.user.delete({
237
+ where: { id: userId },
238
+ });
239
+ return true;
240
+ } catch (error) {
241
+ if (error.code === 'P2025') {
242
+ // Record not found
243
+ return false;
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+ }
249
+
250
+ module.exports = { UserRepositoryMongo };
@@ -0,0 +1,311 @@
1
+ //todo: this repository is tightly coupled to the token repository.
2
+ const { prisma } = require('../../database/prisma');
3
+ const {
4
+ createTokenRepository,
5
+ } = require('../../token/repositories/token-repository-factory');
6
+ const { UserRepositoryInterface } = require('./user-repository-interface');
7
+
8
+ /**
9
+ * PostgreSQL User Repository Adapter
10
+ * Handles user operations with discriminator pattern support
11
+ *
12
+ * PostgreSQL-specific characteristics:
13
+ * - Uses Int IDs with autoincrement
14
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
15
+ * - All returned IDs are converted to strings for application layer consistency
16
+ */
17
+ class UserRepositoryPostgres extends UserRepositoryInterface {
18
+ constructor() {
19
+ super();
20
+ this.prisma = prisma;
21
+ this.tokenRepository = createTokenRepository();
22
+ }
23
+
24
+ /**
25
+ * Convert string ID to integer for PostgreSQL queries
26
+ * @private
27
+ * @param {string|number|null|undefined} id - ID to convert
28
+ * @returns {number|null|undefined} Integer ID or null/undefined
29
+ * @throws {Error} If ID cannot be converted to integer
30
+ */
31
+ _convertId(id) {
32
+ if (id === null || id === undefined) return id;
33
+ const parsed = parseInt(id, 10);
34
+ if (isNaN(parsed)) {
35
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ /**
41
+ * Convert user object IDs to strings
42
+ * @private
43
+ * @param {Object|null} user - User object from database
44
+ * @returns {Object|null} User with string IDs
45
+ */
46
+ _convertUserIds(user) {
47
+ if (!user) return user;
48
+ return {
49
+ ...user,
50
+ id: user.id?.toString(),
51
+ organizationId: user.organizationId?.toString(),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Get session token from base64 buffer token
57
+ * Delegates to TokenRepository
58
+ *
59
+ * @param {string} token - Base64 buffer token
60
+ * @returns {Promise<Object>} Session token object with string IDs
61
+ */
62
+ async getSessionToken(token) {
63
+ const jsonToken =
64
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
65
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
66
+ jsonToken
67
+ );
68
+ return sessionToken;
69
+ }
70
+
71
+ /**
72
+ * Find organization user by ID
73
+ * Replaces: OrganizationUser.findById(userId)
74
+ *
75
+ * @param {string} userId - User ID (string from application layer)
76
+ * @returns {Promise<Object|null>} User object with string IDs or null
77
+ */
78
+ async findOrganizationUserById(userId) {
79
+ const intId = this._convertId(userId);
80
+ const user = await this.prisma.user.findFirst({
81
+ where: {
82
+ id: intId,
83
+ type: 'ORGANIZATION',
84
+ },
85
+ });
86
+ return this._convertUserIds(user);
87
+ }
88
+
89
+ /**
90
+ * Find individual user by ID
91
+ * Replaces: IndividualUser.findById(userId)
92
+ *
93
+ * @param {string} userId - User ID (string from application layer)
94
+ * @returns {Promise<Object|null>} User object with string IDs or null
95
+ */
96
+ async findIndividualUserById(userId) {
97
+ const intId = this._convertId(userId);
98
+ const user = await this.prisma.user.findFirst({
99
+ where: {
100
+ id: intId,
101
+ type: 'INDIVIDUAL',
102
+ },
103
+ });
104
+ return this._convertUserIds(user);
105
+ }
106
+
107
+ /**
108
+ * Create token with expiration
109
+ * Delegates to TokenRepository
110
+ *
111
+ * @param {string} userId - User ID (string from application layer)
112
+ * @param {string} rawToken - Raw unhashed token
113
+ * @param {number} minutes - Minutes until expiration (default 120)
114
+ * @returns {Promise<string>} Base64 buffer token
115
+ */
116
+ async createToken(userId, rawToken, minutes = 120) {
117
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
118
+ userId,
119
+ rawToken,
120
+ minutes
121
+ );
122
+ return this.tokenRepository.createBase64BufferToken(
123
+ createdToken,
124
+ rawToken
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Create individual user
130
+ * Replaces: IndividualUser.create(params)
131
+ *
132
+ * @param {Object} params - User creation parameters (with string IDs from application layer)
133
+ * @returns {Promise<Object>} Created user object with string IDs
134
+ */
135
+ async createIndividualUser(params) {
136
+ const user = await this.prisma.user.create({
137
+ data: {
138
+ type: 'INDIVIDUAL',
139
+ email: params.email,
140
+ username: params.username,
141
+ hashword: params.hashword,
142
+ appUserId: params.appUserId,
143
+ organizationId: this._convertId(
144
+ params.organization || params.organizationId
145
+ ),
146
+ },
147
+ });
148
+ return this._convertUserIds(user);
149
+ }
150
+
151
+ /**
152
+ * Create organization user
153
+ * Replaces: OrganizationUser.create(params)
154
+ *
155
+ * @param {Object} params - Organization creation parameters
156
+ * @returns {Promise<Object>} Created organization object with string IDs
157
+ */
158
+ async createOrganizationUser(params) {
159
+ const user = await this.prisma.user.create({
160
+ data: {
161
+ type: 'ORGANIZATION',
162
+ appOrgId: params.appOrgId,
163
+ name: params.name,
164
+ },
165
+ });
166
+ return this._convertUserIds(user);
167
+ }
168
+
169
+ /**
170
+ * Find individual user by username
171
+ * Replaces: IndividualUser.findOne({ username })
172
+ *
173
+ * @param {string} username - Username to search for
174
+ * @returns {Promise<Object|null>} User object with string IDs or null
175
+ */
176
+ async findIndividualUserByUsername(username) {
177
+ const user = await this.prisma.user.findFirst({
178
+ where: {
179
+ type: 'INDIVIDUAL',
180
+ username,
181
+ },
182
+ });
183
+ return this._convertUserIds(user);
184
+ }
185
+
186
+ /**
187
+ * Find individual user by app user ID
188
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
189
+ *
190
+ * @param {string} appUserId - App user ID to search for
191
+ * @returns {Promise<Object|null>} User object with string IDs or null
192
+ */
193
+ async findIndividualUserByAppUserId(appUserId) {
194
+ const user = await this.prisma.user.findFirst({
195
+ where: {
196
+ type: 'INDIVIDUAL',
197
+ appUserId,
198
+ },
199
+ });
200
+ return this._convertUserIds(user);
201
+ }
202
+
203
+ /**
204
+ * Find organization user by app org ID
205
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
206
+ *
207
+ * @param {string} appOrgId - App organization ID to search for
208
+ * @returns {Promise<Object|null>} User object with string IDs or null
209
+ */
210
+ async findOrganizationUserByAppOrgId(appOrgId) {
211
+ const user = await this.prisma.user.findFirst({
212
+ where: {
213
+ type: 'ORGANIZATION',
214
+ appOrgId,
215
+ },
216
+ });
217
+ return this._convertUserIds(user);
218
+ }
219
+
220
+ /**
221
+ * Find user by ID (any type)
222
+ * @param {string} userId - User ID (string from application layer)
223
+ * @returns {Promise<Object|null>} User object with string IDs or null
224
+ */
225
+ async findUserById(userId) {
226
+ const intId = this._convertId(userId);
227
+ const user = await this.prisma.user.findUnique({
228
+ where: { id: intId },
229
+ });
230
+ return this._convertUserIds(user);
231
+ }
232
+
233
+ /**
234
+ * Find individual user by email
235
+ * @param {string} email - Email to search for
236
+ * @returns {Promise<Object|null>} User object with string IDs or null
237
+ */
238
+ async findIndividualUserByEmail(email) {
239
+ const user = await this.prisma.user.findFirst({
240
+ where: {
241
+ type: 'INDIVIDUAL',
242
+ email,
243
+ },
244
+ });
245
+ return this._convertUserIds(user);
246
+ }
247
+
248
+ /**
249
+ * Update individual user
250
+ * @param {string} userId - User ID (string from application layer)
251
+ * @param {Object} updates - Fields to update (with string IDs from application layer)
252
+ * @returns {Promise<Object>} Updated user object with string IDs
253
+ */
254
+ async updateIndividualUser(userId, updates) {
255
+ const intId = this._convertId(userId);
256
+
257
+ // Convert organizationId if present in updates
258
+ const data = { ...updates };
259
+ if (data.organizationId !== undefined) {
260
+ data.organizationId = this._convertId(data.organizationId);
261
+ }
262
+ if (data.organization !== undefined) {
263
+ data.organizationId = this._convertId(data.organization);
264
+ delete data.organization;
265
+ }
266
+
267
+ const user = await this.prisma.user.update({
268
+ where: { id: intId },
269
+ data,
270
+ });
271
+ return this._convertUserIds(user);
272
+ }
273
+
274
+ /**
275
+ * Update organization user
276
+ * @param {string} userId - User ID (string from application layer)
277
+ * @param {Object} updates - Fields to update
278
+ * @returns {Promise<Object>} Updated user object with string IDs
279
+ */
280
+ async updateOrganizationUser(userId, updates) {
281
+ const intId = this._convertId(userId);
282
+ const user = await this.prisma.user.update({
283
+ where: { id: intId },
284
+ data: updates,
285
+ });
286
+ return this._convertUserIds(user);
287
+ }
288
+
289
+ /**
290
+ * Delete user by ID
291
+ * @param {string} userId - User ID to delete (string from application layer)
292
+ * @returns {Promise<boolean>} True if deleted successfully
293
+ */
294
+ async deleteUser(userId) {
295
+ try {
296
+ const intId = this._convertId(userId);
297
+ await this.prisma.user.delete({
298
+ where: { id: intId },
299
+ });
300
+ return true;
301
+ } catch (error) {
302
+ if (error.code === 'P2025') {
303
+ // Record not found
304
+ return false;
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ }
310
+
311
+ module.exports = { UserRepositoryPostgres };
@@ -0,0 +1,72 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { User } = require('../../user');
3
+
4
+ class TestUserRepository {
5
+ constructor({ userConfig }) {
6
+ this.individualUsers = new Map();
7
+ this.organizationUsers = new Map();
8
+ this.tokens = new Map();
9
+ this.userConfig = userConfig;
10
+ }
11
+
12
+ async getSessionToken(token) {
13
+ return this.tokens.get(token);
14
+ }
15
+
16
+ async findOrganizationUserById(userId) {
17
+ return this.organizationUsers.get(userId);
18
+ }
19
+
20
+ async findIndividualUserById(userId) {
21
+ return this.individualUsers.get(userId);
22
+ }
23
+
24
+ async createToken(userId, rawToken, minutes = 120) {
25
+ const token = `token-for-${userId}-for-${minutes}-mins`;
26
+ this.tokens.set(token, { user: userId, rawToken });
27
+ return token;
28
+ }
29
+
30
+ async createIndividualUser(params) {
31
+ const individualUserData = { id: `individual-${Date.now()}`, ...params };
32
+ this.individualUsers.set(individualUserData.id, individualUserData);
33
+ return individualUserData;
34
+ }
35
+
36
+ async createOrganizationUser(params) {
37
+ const orgUserData = { ...params, id: `org-${Date.now()}` };
38
+ this.organizationUsers.set(orgUserData.id, orgUserData);
39
+ return orgUserData;
40
+ }
41
+
42
+ async findIndividualUserByUsername(username) {
43
+ for (const userDoc of this.individualUsers.values()) {
44
+ if (userDoc.username === username) {
45
+ return userDoc;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ async findIndividualUserByAppUserId(appUserId) {
52
+ if (!appUserId) return null;
53
+ for (const userDoc of this.individualUsers.values()) {
54
+ if (userDoc.appUserId === appUserId) {
55
+ return userDoc;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ async findOrganizationUserByAppOrgId(appOrgId) {
62
+ if (!appOrgId) return null;
63
+ for (const userDoc of this.organizationUsers.values()) {
64
+ if (userDoc.appOrgId === appOrgId) {
65
+ return userDoc;
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+
72
+ module.exports = { TestUserRepository };
@@ -0,0 +1,24 @@
1
+ const {
2
+ CreateIndividualUser,
3
+ } = require('../../use-cases/create-individual-user');
4
+ const { TestUserRepository } = require('../doubles/test-user-repository');
5
+
6
+ describe('CreateIndividualUser Use Case', () => {
7
+ it('should create and return an individual user via the repository', async () => {
8
+ const userConfig = { usePassword: true };
9
+ const userRepository = new TestUserRepository({ userConfig });
10
+ const createIndividualUser = new CreateIndividualUser({
11
+ userRepository,
12
+ userConfig,
13
+ });
14
+
15
+ const params = {
16
+ username: 'test-user',
17
+ password: 'password123',
18
+ };
19
+ const user = await createIndividualUser.execute(params);
20
+
21
+ expect(user).toBeDefined();
22
+ expect(user.getIndividualUser().username).toBe(params.username);
23
+ });
24
+ });
@@ -0,0 +1,28 @@
1
+ const {
2
+ CreateOrganizationUser,
3
+ } = require('../../use-cases/create-organization-user');
4
+ const { TestUserRepository } = require('../doubles/test-user-repository');
5
+
6
+ describe('CreateOrganizationUser Use Case', () => {
7
+ it('should create and return an organization user via the repository', async () => {
8
+ const userConfig = {
9
+ primary: 'organization',
10
+ organizationUserRequired: true,
11
+ individualUserRequired: false,
12
+ };
13
+ const userRepository = new TestUserRepository({ userConfig });
14
+ const createOrganizationUser = new CreateOrganizationUser({
15
+ userRepository,
16
+ userConfig,
17
+ });
18
+
19
+ const params = {
20
+ name: 'Test Org',
21
+ appOrgId: 'org-123',
22
+ };
23
+ const user = await createOrganizationUser.execute(params);
24
+
25
+ expect(user).toBeDefined();
26
+ expect(user.getOrganizationUser().name).toBe(params.name);
27
+ });
28
+ });
@@ -0,0 +1,19 @@
1
+ const {
2
+ CreateTokenForUserId,
3
+ } = require('../../use-cases/create-token-for-user-id');
4
+ const { TestUserRepository } = require('../doubles/test-user-repository');
5
+
6
+ describe('CreateTokenForUserId Use Case', () => {
7
+ it('should create and return a token via the repository', async () => {
8
+ const userConfig = {}; // Not used by this use case, but required by the test repo
9
+ const userRepository = new TestUserRepository({ userConfig });
10
+ const createTokenForUserId = new CreateTokenForUserId({ userRepository });
11
+
12
+ const userId = 'user-123';
13
+ const token = await createTokenForUserId.execute(userId);
14
+
15
+ expect(token).toBeDefined();
16
+ // The mock token is deterministic, so we can check it
17
+ expect(token).toContain(`token-for-${userId}`);
18
+ });
19
+ });