@friggframework/core 2.0.0-next.5 → 2.0.0-next.51

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 (267) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +959 -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 +179 -0
  7. package/application/commands/user-commands.js +213 -0
  8. package/application/index.js +69 -0
  9. package/core/CLAUDE.md +690 -0
  10. package/core/Worker.js +8 -21
  11. package/core/create-handler.js +2 -7
  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 +307 -0
  15. package/credential/repositories/credential-repository-postgres.js +313 -0
  16. package/credential/repositories/credential-repository.js +302 -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/MONGODB_TRANSACTION_FIX.md +198 -0
  20. package/database/adapters/lambda-invoker.js +97 -0
  21. package/database/config.js +154 -0
  22. package/database/encryption/README.md +684 -0
  23. package/database/encryption/encryption-schema-registry.js +141 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/logger.js +79 -0
  26. package/database/encryption/prisma-encryption-extension.js +222 -0
  27. package/database/index.js +25 -12
  28. package/database/models/WebsocketConnection.js +16 -10
  29. package/database/models/readme.md +1 -0
  30. package/database/prisma.js +222 -0
  31. package/database/repositories/health-check-repository-factory.js +43 -0
  32. package/database/repositories/health-check-repository-interface.js +87 -0
  33. package/database/repositories/health-check-repository-mongodb.js +91 -0
  34. package/database/repositories/health-check-repository-postgres.js +82 -0
  35. package/database/repositories/health-check-repository.js +108 -0
  36. package/database/repositories/migration-status-repository-s3.js +137 -0
  37. package/database/use-cases/check-database-health-use-case.js +29 -0
  38. package/database/use-cases/check-database-state-use-case.js +81 -0
  39. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  40. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  41. package/database/use-cases/get-migration-status-use-case.js +93 -0
  42. package/database/use-cases/run-database-migration-use-case.js +137 -0
  43. package/database/use-cases/test-encryption-use-case.js +253 -0
  44. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  45. package/database/utils/mongodb-collection-utils.js +91 -0
  46. package/database/utils/mongodb-schema-init.js +106 -0
  47. package/database/utils/prisma-runner.js +400 -0
  48. package/database/utils/prisma-schema-parser.js +182 -0
  49. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  50. package/encrypt/Cryptor.js +34 -168
  51. package/encrypt/index.js +1 -2
  52. package/encrypt/test-encrypt.js +0 -2
  53. package/generated/prisma-mongodb/client.d.ts +1 -0
  54. package/generated/prisma-mongodb/client.js +4 -0
  55. package/generated/prisma-mongodb/default.d.ts +1 -0
  56. package/generated/prisma-mongodb/default.js +4 -0
  57. package/generated/prisma-mongodb/edge.d.ts +1 -0
  58. package/generated/prisma-mongodb/edge.js +334 -0
  59. package/generated/prisma-mongodb/index-browser.js +316 -0
  60. package/generated/prisma-mongodb/index.d.ts +22898 -0
  61. package/generated/prisma-mongodb/index.js +359 -0
  62. package/generated/prisma-mongodb/package.json +183 -0
  63. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  64. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  65. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  66. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  67. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  68. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  69. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  70. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  71. package/generated/prisma-mongodb/runtime/library.d.ts +3982 -0
  72. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  73. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  74. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  75. package/generated/prisma-mongodb/schema.prisma +362 -0
  76. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  77. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  78. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  79. package/generated/prisma-mongodb/wasm.js +341 -0
  80. package/generated/prisma-postgresql/client.d.ts +1 -0
  81. package/generated/prisma-postgresql/client.js +4 -0
  82. package/generated/prisma-postgresql/default.d.ts +1 -0
  83. package/generated/prisma-postgresql/default.js +4 -0
  84. package/generated/prisma-postgresql/edge.d.ts +1 -0
  85. package/generated/prisma-postgresql/edge.js +356 -0
  86. package/generated/prisma-postgresql/index-browser.js +338 -0
  87. package/generated/prisma-postgresql/index.d.ts +25072 -0
  88. package/generated/prisma-postgresql/index.js +381 -0
  89. package/generated/prisma-postgresql/package.json +183 -0
  90. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  91. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  92. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  93. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  94. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  95. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  96. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  97. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  98. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  99. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  100. package/generated/prisma-postgresql/runtime/library.d.ts +3982 -0
  101. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  102. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  103. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  104. package/generated/prisma-postgresql/schema.prisma +345 -0
  105. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  106. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  107. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  108. package/generated/prisma-postgresql/wasm.js +363 -0
  109. package/handlers/WEBHOOKS.md +653 -0
  110. package/handlers/app-definition-loader.js +38 -0
  111. package/handlers/app-handler-helpers.js +56 -0
  112. package/handlers/backend-utils.js +180 -0
  113. package/handlers/database-migration-handler.js +227 -0
  114. package/handlers/integration-event-dispatcher.js +54 -0
  115. package/handlers/routers/HEALTHCHECK.md +342 -0
  116. package/handlers/routers/auth.js +15 -0
  117. package/handlers/routers/db-migration.handler.js +29 -0
  118. package/handlers/routers/db-migration.js +256 -0
  119. package/handlers/routers/health.js +519 -0
  120. package/handlers/routers/integration-defined-routers.js +45 -0
  121. package/handlers/routers/integration-webhook-routers.js +67 -0
  122. package/handlers/routers/user.js +63 -0
  123. package/handlers/routers/websocket.js +57 -0
  124. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  125. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  126. package/handlers/workers/db-migration.js +352 -0
  127. package/handlers/workers/integration-defined-workers.js +27 -0
  128. package/index.js +77 -22
  129. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  130. package/integrations/index.js +12 -10
  131. package/integrations/integration-base.js +296 -54
  132. package/integrations/integration-router.js +381 -182
  133. package/integrations/options.js +1 -1
  134. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  135. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  136. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  137. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  138. package/integrations/repositories/integration-mapping-repository.js +156 -0
  139. package/integrations/repositories/integration-repository-factory.js +44 -0
  140. package/integrations/repositories/integration-repository-interface.js +127 -0
  141. package/integrations/repositories/integration-repository-mongo.js +303 -0
  142. package/integrations/repositories/integration-repository-postgres.js +352 -0
  143. package/integrations/repositories/process-repository-factory.js +46 -0
  144. package/integrations/repositories/process-repository-interface.js +90 -0
  145. package/integrations/repositories/process-repository-mongo.js +190 -0
  146. package/integrations/repositories/process-repository-postgres.js +217 -0
  147. package/integrations/tests/doubles/dummy-integration-class.js +83 -0
  148. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  149. package/integrations/use-cases/create-integration.js +83 -0
  150. package/integrations/use-cases/create-process.js +128 -0
  151. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  152. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  153. package/integrations/use-cases/get-integration-for-user.js +78 -0
  154. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  155. package/integrations/use-cases/get-integration-instance.js +83 -0
  156. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  157. package/integrations/use-cases/get-possible-integrations.js +27 -0
  158. package/integrations/use-cases/get-process.js +87 -0
  159. package/integrations/use-cases/index.js +19 -0
  160. package/integrations/use-cases/load-integration-context.js +71 -0
  161. package/integrations/use-cases/update-integration-messages.js +44 -0
  162. package/integrations/use-cases/update-integration-status.js +32 -0
  163. package/integrations/use-cases/update-integration.js +93 -0
  164. package/integrations/use-cases/update-process-metrics.js +201 -0
  165. package/integrations/use-cases/update-process-state.js +119 -0
  166. package/integrations/utils/map-integration-dto.js +37 -0
  167. package/jest-global-setup-noop.js +3 -0
  168. package/jest-global-teardown-noop.js +3 -0
  169. package/logs/logger.js +0 -4
  170. package/{module-plugin → modules}/entity.js +1 -1
  171. package/{module-plugin → modules}/index.js +0 -8
  172. package/modules/module-factory.js +56 -0
  173. package/modules/module.js +221 -0
  174. package/modules/repositories/module-repository-factory.js +33 -0
  175. package/modules/repositories/module-repository-interface.js +129 -0
  176. package/modules/repositories/module-repository-mongo.js +377 -0
  177. package/modules/repositories/module-repository-postgres.js +426 -0
  178. package/modules/repositories/module-repository.js +316 -0
  179. package/{module-plugin → modules}/requester/requester.js +1 -0
  180. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  181. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  182. package/modules/tests/doubles/test-module-factory.js +16 -0
  183. package/modules/tests/doubles/test-module-repository.js +39 -0
  184. package/modules/use-cases/get-entities-for-user.js +32 -0
  185. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  186. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  187. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  188. package/modules/use-cases/get-module.js +55 -0
  189. package/modules/use-cases/process-authorization-callback.js +122 -0
  190. package/modules/use-cases/refresh-entity-options.js +59 -0
  191. package/modules/use-cases/test-module-auth.js +55 -0
  192. package/modules/utils/map-module-dto.js +18 -0
  193. package/package.json +82 -50
  194. package/prisma-mongodb/schema.prisma +362 -0
  195. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  196. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  197. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  198. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  199. package/prisma-postgresql/schema.prisma +345 -0
  200. package/queues/queuer-util.js +28 -15
  201. package/syncs/manager.js +468 -443
  202. package/syncs/repositories/sync-repository-factory.js +38 -0
  203. package/syncs/repositories/sync-repository-interface.js +109 -0
  204. package/syncs/repositories/sync-repository-mongo.js +239 -0
  205. package/syncs/repositories/sync-repository-postgres.js +319 -0
  206. package/syncs/sync.js +0 -1
  207. package/token/repositories/token-repository-factory.js +33 -0
  208. package/token/repositories/token-repository-interface.js +131 -0
  209. package/token/repositories/token-repository-mongo.js +212 -0
  210. package/token/repositories/token-repository-postgres.js +257 -0
  211. package/token/repositories/token-repository.js +219 -0
  212. package/types/core/index.d.ts +2 -2
  213. package/types/integrations/index.d.ts +2 -6
  214. package/types/module-plugin/index.d.ts +5 -59
  215. package/types/syncs/index.d.ts +0 -2
  216. package/user/repositories/user-repository-factory.js +46 -0
  217. package/user/repositories/user-repository-interface.js +198 -0
  218. package/user/repositories/user-repository-mongo.js +291 -0
  219. package/user/repositories/user-repository-postgres.js +350 -0
  220. package/user/tests/doubles/test-user-repository.js +72 -0
  221. package/user/use-cases/authenticate-user.js +127 -0
  222. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  223. package/user/use-cases/create-individual-user.js +61 -0
  224. package/user/use-cases/create-organization-user.js +47 -0
  225. package/user/use-cases/create-token-for-user-id.js +30 -0
  226. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  227. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  228. package/user/use-cases/get-user-from-x-frigg-headers.js +106 -0
  229. package/user/use-cases/login-user.js +122 -0
  230. package/user/user.js +93 -0
  231. package/utils/backend-path.js +38 -0
  232. package/utils/index.js +6 -0
  233. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  234. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  235. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  236. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  237. package/websocket/repositories/websocket-connection-repository.js +161 -0
  238. package/database/models/State.js +0 -9
  239. package/database/models/Token.js +0 -70
  240. package/database/mongo.js +0 -45
  241. package/encrypt/Cryptor.test.js +0 -32
  242. package/encrypt/encrypt.js +0 -132
  243. package/encrypt/encrypt.test.js +0 -1069
  244. package/errors/base-error.test.js +0 -32
  245. package/errors/fetch-error.test.js +0 -79
  246. package/errors/halt-error.test.js +0 -11
  247. package/errors/validation-errors.test.js +0 -120
  248. package/integrations/create-frigg-backend.js +0 -31
  249. package/integrations/integration-factory.js +0 -251
  250. package/integrations/integration-mapping.js +0 -43
  251. package/integrations/integration-model.js +0 -46
  252. package/integrations/integration-user.js +0 -144
  253. package/integrations/test/integration-base.test.js +0 -144
  254. package/lambda/TimeoutCatcher.test.js +0 -68
  255. package/logs/logger.test.js +0 -76
  256. package/module-plugin/auther.js +0 -393
  257. package/module-plugin/credential.js +0 -22
  258. package/module-plugin/entity-manager.js +0 -70
  259. package/module-plugin/manager.js +0 -169
  260. package/module-plugin/module-factory.js +0 -61
  261. package/module-plugin/requester/requester.test.js +0 -28
  262. package/module-plugin/test/auther.test.js +0 -97
  263. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  264. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  265. /package/{module-plugin → modules}/requester/basic.js +0 -0
  266. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  267. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,291 @@
1
+ const bcrypt = require('bcryptjs');
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
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
99
+ * @returns {Promise<Object>} Created user object with string IDs
100
+ */
101
+ async createIndividualUser(params) {
102
+ const data = {
103
+ type: 'INDIVIDUAL',
104
+ email: params.email,
105
+ username: params.username,
106
+ appUserId: params.appUserId,
107
+ organizationId: params.organization || params.organizationId,
108
+ };
109
+
110
+ if (
111
+ params.hashword !== undefined &&
112
+ params.hashword !== null &&
113
+ params.hashword !== ''
114
+ ) {
115
+ if (typeof params.hashword !== 'string') {
116
+ throw new Error('Password must be a string');
117
+ }
118
+
119
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
120
+ if (params.hashword.startsWith('$2')) {
121
+ throw new Error(
122
+ 'Password appears to be already hashed. Pass plain text password only.'
123
+ );
124
+ }
125
+
126
+ data.hashword = await bcrypt.hash(params.hashword, 10);
127
+ }
128
+
129
+ return await this.prisma.user.create({ data });
130
+ }
131
+
132
+ /**
133
+ * Create organization user
134
+ * Replaces: OrganizationUser.create(params)
135
+ *
136
+ * @param {Object} params - Organization creation parameters
137
+ * @returns {Promise<Object>} Created organization object with string IDs
138
+ */
139
+ async createOrganizationUser(params) {
140
+ return await this.prisma.user.create({
141
+ data: {
142
+ type: 'ORGANIZATION',
143
+ appOrgId: params.appOrgId,
144
+ name: params.name,
145
+ },
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Find individual user by username
151
+ * Replaces: IndividualUser.findOne({ username })
152
+ *
153
+ * @param {string} username - Username to search for
154
+ * @returns {Promise<Object|null>} User object with string IDs or null
155
+ */
156
+ async findIndividualUserByUsername(username) {
157
+ return await this.prisma.user.findFirst({
158
+ where: {
159
+ type: 'INDIVIDUAL',
160
+ username,
161
+ },
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Find individual user by app user ID
167
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
168
+ *
169
+ * @param {string} appUserId - App user ID to search for
170
+ * @returns {Promise<Object|null>} User object with string IDs or null
171
+ */
172
+ async findIndividualUserByAppUserId(appUserId) {
173
+ return await this.prisma.user.findFirst({
174
+ where: {
175
+ type: 'INDIVIDUAL',
176
+ appUserId,
177
+ },
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Find organization user by app org ID
183
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
184
+ *
185
+ * @param {string} appOrgId - App organization ID to search for
186
+ * @returns {Promise<Object|null>} User object with string IDs or null
187
+ */
188
+ async findOrganizationUserByAppOrgId(appOrgId) {
189
+ return await this.prisma.user.findFirst({
190
+ where: {
191
+ type: 'ORGANIZATION',
192
+ appOrgId,
193
+ },
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Find user by ID (any type)
199
+ * @param {string} userId - User ID
200
+ * @returns {Promise<Object|null>} User object with string IDs or null
201
+ */
202
+ async findUserById(userId) {
203
+ return await this.prisma.user.findUnique({
204
+ where: { id: userId },
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Find individual user by email
210
+ * @param {string} email - Email to search for
211
+ * @returns {Promise<Object|null>} User object with string IDs or null
212
+ */
213
+ async findIndividualUserByEmail(email) {
214
+ return await this.prisma.user.findFirst({
215
+ where: {
216
+ type: 'INDIVIDUAL',
217
+ email,
218
+ },
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Update individual user
224
+ * @param {string} userId - User ID
225
+ * @param {Object} updates - Fields to update
226
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
227
+ * @returns {Promise<Object>} Updated user object with string IDs
228
+ */
229
+ async updateIndividualUser(userId, updates) {
230
+ const data = { ...updates };
231
+
232
+ if (
233
+ data.hashword !== undefined &&
234
+ data.hashword !== null &&
235
+ data.hashword !== ''
236
+ ) {
237
+ if (typeof data.hashword !== 'string') {
238
+ throw new Error('Password must be a string');
239
+ }
240
+
241
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
242
+ if (data.hashword.startsWith('$2')) {
243
+ throw new Error(
244
+ 'Password appears to be already hashed. Pass plain text password only.'
245
+ );
246
+ }
247
+
248
+ data.hashword = await bcrypt.hash(data.hashword, 10);
249
+ }
250
+
251
+ return await this.prisma.user.update({
252
+ where: { id: userId },
253
+ data,
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Update organization user
259
+ * @param {string} userId - User ID
260
+ * @param {Object} updates - Fields to update
261
+ * @returns {Promise<Object>} Updated user object with string IDs
262
+ */
263
+ async updateOrganizationUser(userId, updates) {
264
+ return await this.prisma.user.update({
265
+ where: { id: userId },
266
+ data: updates,
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Delete user by ID
272
+ * @param {string} userId - User ID to delete
273
+ * @returns {Promise<boolean>} True if deleted successfully
274
+ */
275
+ async deleteUser(userId) {
276
+ try {
277
+ await this.prisma.user.delete({
278
+ where: { id: userId },
279
+ });
280
+ return true;
281
+ } catch (error) {
282
+ if (error.code === 'P2025') {
283
+ // Record not found
284
+ return false;
285
+ }
286
+ throw error;
287
+ }
288
+ }
289
+ }
290
+
291
+ module.exports = { UserRepositoryMongo };
@@ -0,0 +1,350 @@
1
+ const bcrypt = require('bcryptjs');
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
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
134
+ * @returns {Promise<Object>} Created user object with string IDs
135
+ */
136
+ async createIndividualUser(params) {
137
+ const data = {
138
+ type: 'INDIVIDUAL',
139
+ email: params.email,
140
+ username: params.username,
141
+ appUserId: params.appUserId,
142
+ organizationId: this._convertId(
143
+ params.organization || params.organizationId
144
+ ),
145
+ };
146
+
147
+ if (
148
+ params.hashword !== undefined &&
149
+ params.hashword !== null &&
150
+ params.hashword !== ''
151
+ ) {
152
+ if (typeof params.hashword !== 'string') {
153
+ throw new Error('Password must be a string');
154
+ }
155
+
156
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
157
+ if (params.hashword.startsWith('$2')) {
158
+ throw new Error(
159
+ 'Password appears to be already hashed. Pass plain text password only.'
160
+ );
161
+ }
162
+
163
+ data.hashword = await bcrypt.hash(params.hashword, 10);
164
+ }
165
+
166
+ const user = await this.prisma.user.create({ data });
167
+ return this._convertUserIds(user);
168
+ }
169
+
170
+ /**
171
+ * Create organization user
172
+ * Replaces: OrganizationUser.create(params)
173
+ *
174
+ * @param {Object} params - Organization creation parameters
175
+ * @returns {Promise<Object>} Created organization object with string IDs
176
+ */
177
+ async createOrganizationUser(params) {
178
+ const user = await this.prisma.user.create({
179
+ data: {
180
+ type: 'ORGANIZATION',
181
+ appOrgId: params.appOrgId,
182
+ name: params.name,
183
+ },
184
+ });
185
+ return this._convertUserIds(user);
186
+ }
187
+
188
+ /**
189
+ * Find individual user by username
190
+ * Replaces: IndividualUser.findOne({ username })
191
+ *
192
+ * @param {string} username - Username to search for
193
+ * @returns {Promise<Object|null>} User object with string IDs or null
194
+ */
195
+ async findIndividualUserByUsername(username) {
196
+ const user = await this.prisma.user.findFirst({
197
+ where: {
198
+ type: 'INDIVIDUAL',
199
+ username,
200
+ },
201
+ });
202
+ return this._convertUserIds(user);
203
+ }
204
+
205
+ /**
206
+ * Find individual user by app user ID
207
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
208
+ *
209
+ * @param {string} appUserId - App user ID to search for
210
+ * @returns {Promise<Object|null>} User object with string IDs or null
211
+ */
212
+ async findIndividualUserByAppUserId(appUserId) {
213
+ const user = await this.prisma.user.findFirst({
214
+ where: {
215
+ type: 'INDIVIDUAL',
216
+ appUserId,
217
+ },
218
+ });
219
+ return this._convertUserIds(user);
220
+ }
221
+
222
+ /**
223
+ * Find organization user by app org ID
224
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
225
+ *
226
+ * @param {string} appOrgId - App organization ID to search for
227
+ * @returns {Promise<Object|null>} User object with string IDs or null
228
+ */
229
+ async findOrganizationUserByAppOrgId(appOrgId) {
230
+ const user = await this.prisma.user.findFirst({
231
+ where: {
232
+ type: 'ORGANIZATION',
233
+ appOrgId,
234
+ },
235
+ });
236
+ return this._convertUserIds(user);
237
+ }
238
+
239
+ /**
240
+ * Find user by ID (any type)
241
+ * @param {string} userId - User ID (string from application layer)
242
+ * @returns {Promise<Object|null>} User object with string IDs or null
243
+ */
244
+ async findUserById(userId) {
245
+ const intId = this._convertId(userId);
246
+ const user = await this.prisma.user.findUnique({
247
+ where: { id: intId },
248
+ });
249
+ return this._convertUserIds(user);
250
+ }
251
+
252
+ /**
253
+ * Find individual user by email
254
+ * @param {string} email - Email to search for
255
+ * @returns {Promise<Object|null>} User object with string IDs or null
256
+ */
257
+ async findIndividualUserByEmail(email) {
258
+ const user = await this.prisma.user.findFirst({
259
+ where: {
260
+ type: 'INDIVIDUAL',
261
+ email,
262
+ },
263
+ });
264
+ return this._convertUserIds(user);
265
+ }
266
+
267
+ /**
268
+ * Update individual user
269
+ * @param {string} userId - User ID (string from application layer)
270
+ * @param {Object} updates - Fields to update (with string IDs from application layer)
271
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
272
+ * @returns {Promise<Object>} Updated user object with string IDs
273
+ */
274
+ async updateIndividualUser(userId, updates) {
275
+ const intId = this._convertId(userId);
276
+
277
+ const data = { ...updates };
278
+
279
+ if (data.organizationId !== undefined) {
280
+ data.organizationId = this._convertId(data.organizationId);
281
+ }
282
+ if (data.organization !== undefined) {
283
+ data.organizationId = this._convertId(data.organization);
284
+ delete data.organization;
285
+ }
286
+
287
+ if (
288
+ data.hashword !== undefined &&
289
+ data.hashword !== null &&
290
+ data.hashword !== ''
291
+ ) {
292
+ if (typeof data.hashword !== 'string') {
293
+ throw new Error('Password must be a string');
294
+ }
295
+
296
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
297
+ if (data.hashword.startsWith('$2')) {
298
+ throw new Error(
299
+ 'Password appears to be already hashed. Pass plain text password only.'
300
+ );
301
+ }
302
+
303
+ data.hashword = await bcrypt.hash(data.hashword, 10);
304
+ }
305
+
306
+ const user = await this.prisma.user.update({
307
+ where: { id: intId },
308
+ data,
309
+ });
310
+ return this._convertUserIds(user);
311
+ }
312
+
313
+ /**
314
+ * Update organization user
315
+ * @param {string} userId - User ID (string from application layer)
316
+ * @param {Object} updates - Fields to update
317
+ * @returns {Promise<Object>} Updated user object with string IDs
318
+ */
319
+ async updateOrganizationUser(userId, updates) {
320
+ const intId = this._convertId(userId);
321
+ const user = await this.prisma.user.update({
322
+ where: { id: intId },
323
+ data: updates,
324
+ });
325
+ return this._convertUserIds(user);
326
+ }
327
+
328
+ /**
329
+ * Delete user by ID
330
+ * @param {string} userId - User ID to delete (string from application layer)
331
+ * @returns {Promise<boolean>} True if deleted successfully
332
+ */
333
+ async deleteUser(userId) {
334
+ try {
335
+ const intId = this._convertId(userId);
336
+ await this.prisma.user.delete({
337
+ where: { id: intId },
338
+ });
339
+ return true;
340
+ } catch (error) {
341
+ if (error.code === 'P2025') {
342
+ // Record not found
343
+ return false;
344
+ }
345
+ throw error;
346
+ }
347
+ }
348
+ }
349
+
350
+ 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 };