@friggframework/core 2.0.0-next.8 → 2.0.0-next.80

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 (303) hide show
  1. package/CLAUDE.md +694 -0
  2. package/README.md +959 -50
  3. package/application/commands/README.md +451 -0
  4. package/application/commands/credential-commands.js +245 -0
  5. package/application/commands/entity-commands.js +336 -0
  6. package/application/commands/integration-commands.js +210 -0
  7. package/application/commands/scheduler-commands.js +263 -0
  8. package/application/commands/user-commands.js +283 -0
  9. package/application/index.js +73 -0
  10. package/assertions/index.js +0 -3
  11. package/core/CLAUDE.md +690 -0
  12. package/core/Worker.js +60 -24
  13. package/core/create-handler.js +79 -8
  14. package/credential/repositories/credential-repository-documentdb.js +304 -0
  15. package/credential/repositories/credential-repository-factory.js +54 -0
  16. package/credential/repositories/credential-repository-interface.js +98 -0
  17. package/credential/repositories/credential-repository-mongo.js +269 -0
  18. package/credential/repositories/credential-repository-postgres.js +287 -0
  19. package/credential/repositories/credential-repository.js +300 -0
  20. package/credential/use-cases/get-credential-for-user.js +25 -0
  21. package/credential/use-cases/update-authentication-status.js +15 -0
  22. package/database/MONGODB_TRANSACTION_FIX.md +198 -0
  23. package/database/adapters/lambda-invoker.js +97 -0
  24. package/database/config.js +154 -0
  25. package/database/documentdb-encryption-service.js +330 -0
  26. package/database/documentdb-utils.js +136 -0
  27. package/database/encryption/README.md +839 -0
  28. package/database/encryption/documentdb-encryption-service.md +3575 -0
  29. package/database/encryption/encryption-schema-registry.js +268 -0
  30. package/database/encryption/field-encryption-service.js +226 -0
  31. package/database/encryption/logger.js +79 -0
  32. package/database/encryption/prisma-encryption-extension.js +222 -0
  33. package/database/index.js +21 -21
  34. package/database/prisma.js +182 -0
  35. package/database/repositories/health-check-repository-documentdb.js +138 -0
  36. package/database/repositories/health-check-repository-factory.js +48 -0
  37. package/database/repositories/health-check-repository-interface.js +82 -0
  38. package/database/repositories/health-check-repository-mongodb.js +89 -0
  39. package/database/repositories/health-check-repository-postgres.js +82 -0
  40. package/database/repositories/migration-status-repository-s3.js +137 -0
  41. package/database/use-cases/check-database-health-use-case.js +29 -0
  42. package/database/use-cases/check-database-state-use-case.js +81 -0
  43. package/database/use-cases/check-encryption-health-use-case.js +83 -0
  44. package/database/use-cases/get-database-state-via-worker-use-case.js +61 -0
  45. package/database/use-cases/get-migration-status-use-case.js +93 -0
  46. package/database/use-cases/run-database-migration-use-case.js +139 -0
  47. package/database/use-cases/test-encryption-use-case.js +253 -0
  48. package/database/use-cases/trigger-database-migration-use-case.js +157 -0
  49. package/database/utils/mongodb-collection-utils.js +94 -0
  50. package/database/utils/mongodb-schema-init.js +108 -0
  51. package/database/utils/prisma-runner.js +477 -0
  52. package/database/utils/prisma-schema-parser.js +182 -0
  53. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +517 -0
  54. package/encrypt/Cryptor.js +34 -168
  55. package/encrypt/index.js +1 -2
  56. package/errors/client-safe-error.js +26 -0
  57. package/errors/fetch-error.js +15 -7
  58. package/errors/index.js +2 -0
  59. package/generated/prisma-mongodb/client.d.ts +1 -0
  60. package/generated/prisma-mongodb/client.js +4 -0
  61. package/generated/prisma-mongodb/default.d.ts +1 -0
  62. package/generated/prisma-mongodb/default.js +4 -0
  63. package/generated/prisma-mongodb/edge.d.ts +1 -0
  64. package/generated/prisma-mongodb/edge.js +335 -0
  65. package/generated/prisma-mongodb/index-browser.js +317 -0
  66. package/generated/prisma-mongodb/index.d.ts +22955 -0
  67. package/generated/prisma-mongodb/index.js +360 -0
  68. package/generated/prisma-mongodb/package.json +183 -0
  69. package/generated/prisma-mongodb/query-engine-debian-openssl-3.0.x +0 -0
  70. package/generated/prisma-mongodb/query-engine-rhel-openssl-3.0.x +0 -0
  71. package/generated/prisma-mongodb/runtime/binary.d.ts +1 -0
  72. package/generated/prisma-mongodb/runtime/binary.js +289 -0
  73. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  74. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  75. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  76. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  77. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  78. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  79. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  80. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  81. package/generated/prisma-mongodb/schema.prisma +362 -0
  82. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  84. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  85. package/generated/prisma-mongodb/wasm.js +342 -0
  86. package/generated/prisma-postgresql/client.d.ts +1 -0
  87. package/generated/prisma-postgresql/client.js +4 -0
  88. package/generated/prisma-postgresql/default.d.ts +1 -0
  89. package/generated/prisma-postgresql/default.js +4 -0
  90. package/generated/prisma-postgresql/edge.d.ts +1 -0
  91. package/generated/prisma-postgresql/edge.js +357 -0
  92. package/generated/prisma-postgresql/index-browser.js +339 -0
  93. package/generated/prisma-postgresql/index.d.ts +25131 -0
  94. package/generated/prisma-postgresql/index.js +382 -0
  95. package/generated/prisma-postgresql/package.json +183 -0
  96. package/generated/prisma-postgresql/query-engine-debian-openssl-3.0.x +0 -0
  97. package/generated/prisma-postgresql/query-engine-rhel-openssl-3.0.x +0 -0
  98. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  99. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  100. package/generated/prisma-postgresql/runtime/binary.d.ts +1 -0
  101. package/generated/prisma-postgresql/runtime/binary.js +289 -0
  102. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  103. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  104. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  105. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  106. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  107. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  108. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  109. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  110. package/generated/prisma-postgresql/schema.prisma +345 -0
  111. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  112. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  113. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  114. package/generated/prisma-postgresql/wasm.js +364 -0
  115. package/handlers/WEBHOOKS.md +653 -0
  116. package/handlers/app-definition-loader.js +38 -0
  117. package/handlers/app-handler-helpers.js +57 -0
  118. package/handlers/backend-utils.js +262 -0
  119. package/handlers/database-migration-handler.js +227 -0
  120. package/handlers/integration-event-dispatcher.js +54 -0
  121. package/handlers/routers/HEALTHCHECK.md +342 -0
  122. package/handlers/routers/auth.js +15 -0
  123. package/handlers/routers/db-migration.handler.js +29 -0
  124. package/handlers/routers/db-migration.js +326 -0
  125. package/handlers/routers/health.js +516 -0
  126. package/handlers/routers/integration-defined-routers.js +45 -0
  127. package/handlers/routers/integration-webhook-routers.js +67 -0
  128. package/handlers/routers/user.js +63 -0
  129. package/handlers/routers/websocket.js +57 -0
  130. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  131. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  132. package/handlers/workers/db-migration.js +352 -0
  133. package/handlers/workers/dlq-processor.js +63 -0
  134. package/handlers/workers/integration-defined-workers.js +23 -0
  135. package/index.js +82 -46
  136. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  137. package/infrastructure/scheduler/index.js +33 -0
  138. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  139. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  140. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  141. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  142. package/integrations/index.js +12 -10
  143. package/integrations/integration-base.js +364 -55
  144. package/integrations/integration-router.js +375 -179
  145. package/integrations/options.js +1 -1
  146. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  147. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  148. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  149. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  150. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  151. package/integrations/repositories/integration-mapping-repository.js +156 -0
  152. package/integrations/repositories/integration-repository-documentdb.js +219 -0
  153. package/integrations/repositories/integration-repository-factory.js +51 -0
  154. package/integrations/repositories/integration-repository-interface.js +144 -0
  155. package/integrations/repositories/integration-repository-mongo.js +330 -0
  156. package/integrations/repositories/integration-repository-postgres.js +385 -0
  157. package/integrations/repositories/process-repository-documentdb.js +243 -0
  158. package/integrations/repositories/process-repository-factory.js +53 -0
  159. package/integrations/repositories/process-repository-interface.js +90 -0
  160. package/integrations/repositories/process-repository-mongo.js +190 -0
  161. package/integrations/repositories/process-repository-postgres.js +217 -0
  162. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  163. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  164. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  165. package/integrations/use-cases/create-integration.js +83 -0
  166. package/integrations/use-cases/create-process.js +128 -0
  167. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  168. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  169. package/integrations/use-cases/get-integration-for-user.js +78 -0
  170. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  171. package/integrations/use-cases/get-integration-instance.js +83 -0
  172. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  173. package/integrations/use-cases/get-possible-integrations.js +27 -0
  174. package/integrations/use-cases/get-process.js +87 -0
  175. package/integrations/use-cases/index.js +19 -0
  176. package/integrations/use-cases/load-integration-context.js +71 -0
  177. package/integrations/use-cases/update-integration-messages.js +44 -0
  178. package/integrations/use-cases/update-integration-status.js +32 -0
  179. package/integrations/use-cases/update-integration.js +92 -0
  180. package/integrations/use-cases/update-process-metrics.js +201 -0
  181. package/integrations/use-cases/update-process-state.js +119 -0
  182. package/integrations/utils/map-integration-dto.js +37 -0
  183. package/jest-global-setup-noop.js +3 -0
  184. package/jest-global-teardown-noop.js +3 -0
  185. package/logs/logger.js +0 -4
  186. package/{module-plugin → modules}/index.js +0 -10
  187. package/modules/module-factory.js +56 -0
  188. package/modules/module.js +256 -0
  189. package/modules/repositories/module-repository-documentdb.js +335 -0
  190. package/modules/repositories/module-repository-factory.js +40 -0
  191. package/modules/repositories/module-repository-interface.js +129 -0
  192. package/modules/repositories/module-repository-mongo.js +408 -0
  193. package/modules/repositories/module-repository-postgres.js +453 -0
  194. package/modules/repositories/module-repository.js +345 -0
  195. package/modules/requester/api-key.js +52 -0
  196. package/modules/requester/oauth-2.js +396 -0
  197. package/{module-plugin → modules}/requester/requester.js +4 -2
  198. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  199. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  200. package/modules/tests/doubles/test-module-factory.js +16 -0
  201. package/modules/tests/doubles/test-module-repository.js +39 -0
  202. package/modules/use-cases/get-entities-for-user.js +32 -0
  203. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  204. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  205. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  206. package/modules/use-cases/get-module.js +74 -0
  207. package/modules/use-cases/process-authorization-callback.js +177 -0
  208. package/modules/use-cases/refresh-entity-options.js +72 -0
  209. package/modules/use-cases/test-module-auth.js +72 -0
  210. package/modules/utils/map-module-dto.js +18 -0
  211. package/package.json +82 -50
  212. package/prisma-mongodb/schema.prisma +362 -0
  213. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  214. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  215. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  216. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  217. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  218. package/prisma-postgresql/schema.prisma +345 -0
  219. package/queues/queuer-util.js +103 -21
  220. package/syncs/manager.js +468 -443
  221. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  222. package/syncs/repositories/sync-repository-factory.js +43 -0
  223. package/syncs/repositories/sync-repository-interface.js +109 -0
  224. package/syncs/repositories/sync-repository-mongo.js +239 -0
  225. package/syncs/repositories/sync-repository-postgres.js +319 -0
  226. package/syncs/sync.js +0 -1
  227. package/token/repositories/token-repository-documentdb.js +137 -0
  228. package/token/repositories/token-repository-factory.js +40 -0
  229. package/token/repositories/token-repository-interface.js +131 -0
  230. package/token/repositories/token-repository-mongo.js +219 -0
  231. package/token/repositories/token-repository-postgres.js +264 -0
  232. package/token/repositories/token-repository.js +219 -0
  233. package/types/associations/index.d.ts +0 -17
  234. package/types/core/index.d.ts +12 -4
  235. package/types/database/index.d.ts +10 -2
  236. package/types/encrypt/index.d.ts +5 -3
  237. package/types/integrations/index.d.ts +3 -8
  238. package/types/module-plugin/index.d.ts +17 -69
  239. package/types/syncs/index.d.ts +0 -17
  240. package/user/repositories/user-repository-documentdb.js +441 -0
  241. package/user/repositories/user-repository-factory.js +52 -0
  242. package/user/repositories/user-repository-interface.js +201 -0
  243. package/user/repositories/user-repository-mongo.js +308 -0
  244. package/user/repositories/user-repository-postgres.js +360 -0
  245. package/user/tests/doubles/test-user-repository.js +72 -0
  246. package/user/use-cases/authenticate-user.js +127 -0
  247. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  248. package/user/use-cases/create-individual-user.js +61 -0
  249. package/user/use-cases/create-organization-user.js +47 -0
  250. package/user/use-cases/create-token-for-user-id.js +30 -0
  251. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  252. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  253. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  254. package/user/use-cases/login-user.js +122 -0
  255. package/user/user.js +125 -0
  256. package/utils/backend-path.js +38 -0
  257. package/utils/index.js +6 -0
  258. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  259. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  260. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  261. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  262. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  263. package/websocket/repositories/websocket-connection-repository.js +161 -0
  264. package/assertions/is-equal.js +0 -17
  265. package/associations/model.js +0 -54
  266. package/database/models/IndividualUser.js +0 -76
  267. package/database/models/OrganizationUser.js +0 -29
  268. package/database/models/State.js +0 -9
  269. package/database/models/Token.js +0 -70
  270. package/database/models/UserModel.js +0 -7
  271. package/database/models/WebsocketConnection.js +0 -49
  272. package/database/mongo.js +0 -45
  273. package/database/mongoose.js +0 -5
  274. package/encrypt/Cryptor.test.js +0 -32
  275. package/encrypt/encrypt.js +0 -132
  276. package/encrypt/encrypt.test.js +0 -1069
  277. package/encrypt/test-encrypt.js +0 -107
  278. package/errors/base-error.test.js +0 -32
  279. package/errors/fetch-error.test.js +0 -79
  280. package/errors/halt-error.test.js +0 -11
  281. package/errors/validation-errors.test.js +0 -120
  282. package/integrations/create-frigg-backend.js +0 -31
  283. package/integrations/integration-factory.js +0 -251
  284. package/integrations/integration-mapping.js +0 -43
  285. package/integrations/integration-model.js +0 -46
  286. package/integrations/integration-user.js +0 -144
  287. package/integrations/test/integration-base.test.js +0 -144
  288. package/lambda/TimeoutCatcher.test.js +0 -68
  289. package/logs/logger.test.js +0 -76
  290. package/module-plugin/auther.js +0 -393
  291. package/module-plugin/credential.js +0 -22
  292. package/module-plugin/entity-manager.js +0 -70
  293. package/module-plugin/entity.js +0 -46
  294. package/module-plugin/manager.js +0 -169
  295. package/module-plugin/module-factory.js +0 -61
  296. package/module-plugin/requester/api-key.js +0 -36
  297. package/module-plugin/requester/oauth-2.js +0 -219
  298. package/module-plugin/requester/requester.test.js +0 -28
  299. package/module-plugin/test/auther.test.js +0 -97
  300. package/syncs/model.js +0 -62
  301. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  302. /package/{module-plugin → modules}/requester/basic.js +0 -0
  303. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,308 @@
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
+ const { ClientSafeError } = require('../../errors');
8
+
9
+ /**
10
+ * MongoDB User Repository Adapter
11
+ * Handles user operations with discriminator pattern support
12
+ *
13
+ * MongoDB-specific characteristics:
14
+ * - Uses String IDs (ObjectId)
15
+ * - No ID conversion needed (IDs are already strings)
16
+ * - IndividualUser/OrganizationUser discriminators → User model with type field
17
+ */
18
+ class UserRepositoryMongo extends UserRepositoryInterface {
19
+ constructor() {
20
+ super();
21
+ this.prisma = prisma;
22
+ this.tokenRepository = createTokenRepository();
23
+ }
24
+
25
+ /**
26
+ * Get session token from base64 buffer token
27
+ * Delegates to TokenRepository
28
+ *
29
+ * @param {string} token - Base64 buffer token
30
+ * @returns {Promise<Object>} Session token object with string IDs
31
+ */
32
+ async getSessionToken(token) {
33
+ const jsonToken =
34
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
35
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
36
+ jsonToken
37
+ );
38
+ return sessionToken;
39
+ }
40
+
41
+ /**
42
+ * Find organization user by ID
43
+ * Replaces: OrganizationUser.findById(userId)
44
+ *
45
+ * @param {string} userId - User ID
46
+ * @returns {Promise<Object|null>} User object with string IDs or null
47
+ */
48
+ async findOrganizationUserById(userId) {
49
+ return await this.prisma.user.findFirst({
50
+ where: {
51
+ id: userId,
52
+ type: 'ORGANIZATION',
53
+ },
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Find individual user by ID
59
+ * Replaces: IndividualUser.findById(userId)
60
+ *
61
+ * @param {string} userId - User ID
62
+ * @returns {Promise<Object|null>} User object with string IDs or null
63
+ */
64
+ async findIndividualUserById(userId) {
65
+ return await this.prisma.user.findFirst({
66
+ where: {
67
+ id: userId,
68
+ type: 'INDIVIDUAL',
69
+ },
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Create token with expiration
75
+ * Delegates to TokenRepository
76
+ *
77
+ * @param {string} userId - User ID
78
+ * @param {string} rawToken - Raw unhashed token
79
+ * @param {number} minutes - Minutes until expiration (default 120)
80
+ * @returns {Promise<string>} Base64 buffer token
81
+ */
82
+ async createToken(userId, rawToken, minutes = 120) {
83
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
84
+ userId,
85
+ rawToken,
86
+ minutes
87
+ );
88
+ return this.tokenRepository.createBase64BufferToken(
89
+ createdToken,
90
+ rawToken
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Create individual user
96
+ * Replaces: IndividualUser.create(params)
97
+ *
98
+ * @param {Object} params - User creation parameters
99
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
100
+ * @returns {Promise<Object>} Created user object with string IDs
101
+ */
102
+ async createIndividualUser(params) {
103
+ const data = {
104
+ type: 'INDIVIDUAL',
105
+ email: params.email,
106
+ username: params.username,
107
+ appUserId: params.appUserId,
108
+ organizationId: params.organization || params.organizationId,
109
+ };
110
+
111
+ if (
112
+ params.hashword !== undefined &&
113
+ params.hashword !== null &&
114
+ params.hashword !== ''
115
+ ) {
116
+ if (typeof params.hashword !== 'string') {
117
+ throw new ClientSafeError('Password must be a string', 400);
118
+ }
119
+
120
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
121
+ if (params.hashword.startsWith('$2')) {
122
+ throw new Error(
123
+ 'Password appears to be already hashed. Pass plain text password only.'
124
+ );
125
+ }
126
+
127
+ data.hashword = await bcrypt.hash(params.hashword, 10);
128
+ }
129
+
130
+ return await this.prisma.user.create({ data });
131
+ }
132
+
133
+ /**
134
+ * Create organization user
135
+ * Replaces: OrganizationUser.create(params)
136
+ *
137
+ * @param {Object} params - Organization creation parameters
138
+ * @returns {Promise<Object>} Created organization object with string IDs
139
+ */
140
+ async createOrganizationUser(params) {
141
+ return await this.prisma.user.create({
142
+ data: {
143
+ type: 'ORGANIZATION',
144
+ appOrgId: params.appOrgId,
145
+ name: params.name,
146
+ },
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Find individual user by username
152
+ * Replaces: IndividualUser.findOne({ username })
153
+ *
154
+ * @param {string} username - Username to search for
155
+ * @returns {Promise<Object|null>} User object with string IDs or null
156
+ */
157
+ async findIndividualUserByUsername(username) {
158
+ return await this.prisma.user.findFirst({
159
+ where: {
160
+ type: 'INDIVIDUAL',
161
+ username,
162
+ },
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Find individual user by app user ID
168
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
169
+ *
170
+ * @param {string} appUserId - App user ID to search for
171
+ * @returns {Promise<Object|null>} User object with string IDs or null
172
+ */
173
+ async findIndividualUserByAppUserId(appUserId) {
174
+ return await this.prisma.user.findFirst({
175
+ where: {
176
+ type: 'INDIVIDUAL',
177
+ appUserId,
178
+ },
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Find organization user by app org ID
184
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
185
+ *
186
+ * @param {string} appOrgId - App organization ID to search for
187
+ * @returns {Promise<Object|null>} User object with string IDs or null
188
+ */
189
+ async findOrganizationUserByAppOrgId(appOrgId) {
190
+ return await this.prisma.user.findFirst({
191
+ where: {
192
+ type: 'ORGANIZATION',
193
+ appOrgId,
194
+ },
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Find individual user by email
200
+ * @param {string} email - Email to search for
201
+ * @returns {Promise<Object|null>} User object with string IDs or null
202
+ */
203
+ async findIndividualUserByEmail(email) {
204
+ return await this.prisma.user.findFirst({
205
+ where: {
206
+ type: 'INDIVIDUAL',
207
+ email,
208
+ },
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Update individual user
214
+ * @param {string} userId - User ID
215
+ * @param {Object} updates - Fields to update
216
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
217
+ * @returns {Promise<Object>} Updated user object with string IDs
218
+ */
219
+ async updateIndividualUser(userId, updates) {
220
+ const data = { ...updates };
221
+
222
+ if (
223
+ data.hashword !== undefined &&
224
+ data.hashword !== null &&
225
+ data.hashword !== ''
226
+ ) {
227
+ if (typeof data.hashword !== 'string') {
228
+ throw new ClientSafeError('Password must be a string', 400);
229
+ }
230
+
231
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
232
+ if (data.hashword.startsWith('$2')) {
233
+ throw new Error(
234
+ 'Password appears to be already hashed. Pass plain text password only.'
235
+ );
236
+ }
237
+
238
+ data.hashword = await bcrypt.hash(data.hashword, 10);
239
+ }
240
+
241
+ return await this.prisma.user.update({
242
+ where: { id: userId },
243
+ data,
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Update organization user
249
+ * @param {string} userId - User ID
250
+ * @param {Object} updates - Fields to update
251
+ * @returns {Promise<Object>} Updated user object with string IDs
252
+ */
253
+ async updateOrganizationUser(userId, updates) {
254
+ return await this.prisma.user.update({
255
+ where: { id: userId },
256
+ data: updates,
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Delete user by ID
262
+ *
263
+ * NOTE: This only deletes the user record itself.
264
+ * Prisma's onDelete: Cascade does NOT work reliably with MongoDB (no database-level referential integrity).
265
+ * Integration developers MUST manually cascade delete related records before calling this method:
266
+ * 1. Delete integrations (via deleteIntegrationById)
267
+ * 2. Delete entities (via deleteEntityById)
268
+ * 3. Delete credentials (via deleteCredentialById)
269
+ * 4. Finally delete user (via deleteUserById)
270
+ *
271
+ * @param {string} userId - User ID to delete
272
+ * @returns {Promise<boolean>} True if deleted successfully
273
+ */
274
+ async deleteUser(userId) {
275
+ try {
276
+ await this.prisma.user.delete({
277
+ where: { id: userId },
278
+ });
279
+ return true;
280
+ } catch (error) {
281
+ if (error.code === 'P2025') {
282
+ // Record not found
283
+ return false;
284
+ }
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Link an individual user to an organization user
291
+ * @param {string} individualUserId - Individual user ID (MongoDB ObjectId string)
292
+ * @param {string} organizationUserId - Organization user ID (MongoDB ObjectId string)
293
+ * @returns {Promise<Object>} Updated individual user object
294
+ */
295
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
296
+ return await this.prisma.user.update({
297
+ where: {
298
+ id: individualUserId,
299
+ type: 'INDIVIDUAL',
300
+ },
301
+ data: {
302
+ organizationId: organizationUserId,
303
+ },
304
+ });
305
+ }
306
+ }
307
+
308
+ module.exports = { UserRepositoryMongo };
@@ -0,0 +1,360 @@
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
+ const { ClientSafeError } = require('../../errors');
8
+
9
+ /**
10
+ * PostgreSQL User Repository Adapter
11
+ * Handles user operations with discriminator pattern support
12
+ *
13
+ * PostgreSQL-specific characteristics:
14
+ * - Uses Int IDs with autoincrement
15
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
16
+ * - All returned IDs are converted to strings for application layer consistency
17
+ */
18
+ class UserRepositoryPostgres extends UserRepositoryInterface {
19
+ constructor() {
20
+ super();
21
+ this.prisma = prisma;
22
+ this.tokenRepository = createTokenRepository();
23
+ }
24
+
25
+ /**
26
+ * Convert string ID to integer for PostgreSQL queries
27
+ * @private
28
+ * @param {string|number|null|undefined} id - ID to convert
29
+ * @returns {number|null|undefined} Integer ID or null/undefined
30
+ * @throws {Error} If ID cannot be converted to integer
31
+ */
32
+ _convertId(id) {
33
+ if (id === null || id === undefined) return id;
34
+ const parsed = parseInt(id, 10);
35
+ if (isNaN(parsed)) {
36
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
37
+ }
38
+ return parsed;
39
+ }
40
+
41
+ /**
42
+ * Convert user object IDs to strings
43
+ * @private
44
+ * @param {Object|null} user - User object from database
45
+ * @returns {Object|null} User with string IDs
46
+ */
47
+ _convertUserIds(user) {
48
+ if (!user) return user;
49
+ return {
50
+ ...user,
51
+ id: user.id?.toString(),
52
+ organizationId: user.organizationId?.toString(),
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Get session token from base64 buffer token
58
+ * Delegates to TokenRepository
59
+ *
60
+ * @param {string} token - Base64 buffer token
61
+ * @returns {Promise<Object>} Session token object with string IDs
62
+ */
63
+ async getSessionToken(token) {
64
+ const jsonToken =
65
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
66
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
67
+ jsonToken
68
+ );
69
+ return sessionToken;
70
+ }
71
+
72
+ /**
73
+ * Find organization user by ID
74
+ * Replaces: OrganizationUser.findById(userId)
75
+ *
76
+ * @param {string} userId - User ID (string from application layer)
77
+ * @returns {Promise<Object|null>} User object with string IDs or null
78
+ */
79
+ async findOrganizationUserById(userId) {
80
+ const intId = this._convertId(userId);
81
+ const user = await this.prisma.user.findFirst({
82
+ where: {
83
+ id: intId,
84
+ type: 'ORGANIZATION',
85
+ },
86
+ });
87
+ return this._convertUserIds(user);
88
+ }
89
+
90
+ /**
91
+ * Find individual user by ID
92
+ * Replaces: IndividualUser.findById(userId)
93
+ *
94
+ * @param {string} userId - User ID (string from application layer)
95
+ * @returns {Promise<Object|null>} User object with string IDs or null
96
+ */
97
+ async findIndividualUserById(userId) {
98
+ const intId = this._convertId(userId);
99
+ const user = await this.prisma.user.findFirst({
100
+ where: {
101
+ id: intId,
102
+ type: 'INDIVIDUAL',
103
+ },
104
+ });
105
+ return this._convertUserIds(user);
106
+ }
107
+
108
+ /**
109
+ * Create token with expiration
110
+ * Delegates to TokenRepository
111
+ *
112
+ * @param {string} userId - User ID (string from application layer)
113
+ * @param {string} rawToken - Raw unhashed token
114
+ * @param {number} minutes - Minutes until expiration (default 120)
115
+ * @returns {Promise<string>} Base64 buffer token
116
+ */
117
+ async createToken(userId, rawToken, minutes = 120) {
118
+ const createdToken = await this.tokenRepository.createTokenWithExpire(
119
+ userId,
120
+ rawToken,
121
+ minutes
122
+ );
123
+ return this.tokenRepository.createBase64BufferToken(
124
+ createdToken,
125
+ rawToken
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Create individual user
131
+ * Replaces: IndividualUser.create(params)
132
+ *
133
+ * @param {Object} params - User creation parameters (with string IDs from application layer)
134
+ * @param {string} [params.hashword] - Plain text password (will be bcrypt hashed automatically)
135
+ * @returns {Promise<Object>} Created user object with string IDs
136
+ */
137
+ async createIndividualUser(params) {
138
+ const data = {
139
+ type: 'INDIVIDUAL',
140
+ email: params.email,
141
+ username: params.username,
142
+ appUserId: params.appUserId,
143
+ organizationId: this._convertId(
144
+ params.organization || params.organizationId
145
+ ),
146
+ };
147
+
148
+ if (
149
+ params.hashword !== undefined &&
150
+ params.hashword !== null &&
151
+ params.hashword !== ''
152
+ ) {
153
+ if (typeof params.hashword !== 'string') {
154
+ throw new ClientSafeError('Password must be a string', 400);
155
+ }
156
+
157
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
158
+ if (params.hashword.startsWith('$2')) {
159
+ throw new Error(
160
+ 'Password appears to be already hashed. Pass plain text password only.'
161
+ );
162
+ }
163
+
164
+ data.hashword = await bcrypt.hash(params.hashword, 10);
165
+ }
166
+
167
+ const user = await this.prisma.user.create({ data });
168
+ return this._convertUserIds(user);
169
+ }
170
+
171
+ /**
172
+ * Create organization user
173
+ * Replaces: OrganizationUser.create(params)
174
+ *
175
+ * @param {Object} params - Organization creation parameters
176
+ * @returns {Promise<Object>} Created organization object with string IDs
177
+ */
178
+ async createOrganizationUser(params) {
179
+ const user = await this.prisma.user.create({
180
+ data: {
181
+ type: 'ORGANIZATION',
182
+ appOrgId: params.appOrgId,
183
+ name: params.name,
184
+ },
185
+ });
186
+ return this._convertUserIds(user);
187
+ }
188
+
189
+ /**
190
+ * Find individual user by username
191
+ * Replaces: IndividualUser.findOne({ username })
192
+ *
193
+ * @param {string} username - Username to search for
194
+ * @returns {Promise<Object|null>} User object with string IDs or null
195
+ */
196
+ async findIndividualUserByUsername(username) {
197
+ const user = await this.prisma.user.findFirst({
198
+ where: {
199
+ type: 'INDIVIDUAL',
200
+ username,
201
+ },
202
+ });
203
+ return this._convertUserIds(user);
204
+ }
205
+
206
+ /**
207
+ * Find individual user by app user ID
208
+ * Replaces: IndividualUser.getUserByAppUserId(appUserId)
209
+ *
210
+ * @param {string} appUserId - App user ID to search for
211
+ * @returns {Promise<Object|null>} User object with string IDs or null
212
+ */
213
+ async findIndividualUserByAppUserId(appUserId) {
214
+ const user = await this.prisma.user.findFirst({
215
+ where: {
216
+ type: 'INDIVIDUAL',
217
+ appUserId,
218
+ },
219
+ });
220
+ return this._convertUserIds(user);
221
+ }
222
+
223
+ /**
224
+ * Find organization user by app org ID
225
+ * Replaces: OrganizationUser.getUserByAppOrgId(appOrgId)
226
+ *
227
+ * @param {string} appOrgId - App organization ID to search for
228
+ * @returns {Promise<Object|null>} User object with string IDs or null
229
+ */
230
+ async findOrganizationUserByAppOrgId(appOrgId) {
231
+ const user = await this.prisma.user.findFirst({
232
+ where: {
233
+ type: 'ORGANIZATION',
234
+ appOrgId,
235
+ },
236
+ });
237
+ return this._convertUserIds(user);
238
+ }
239
+
240
+ /**
241
+ * Find individual user by email
242
+ * @param {string} email - Email to search for
243
+ * @returns {Promise<Object|null>} User object with string IDs or null
244
+ */
245
+ async findIndividualUserByEmail(email) {
246
+ const user = await this.prisma.user.findFirst({
247
+ where: {
248
+ type: 'INDIVIDUAL',
249
+ email,
250
+ },
251
+ });
252
+ return this._convertUserIds(user);
253
+ }
254
+
255
+ /**
256
+ * Update individual user
257
+ * @param {string} userId - User ID (string from application layer)
258
+ * @param {Object} updates - Fields to update (with string IDs from application layer)
259
+ * @param {string} [updates.hashword] - Plain text password (will be bcrypt hashed automatically)
260
+ * @returns {Promise<Object>} Updated user object with string IDs
261
+ */
262
+ async updateIndividualUser(userId, updates) {
263
+ const intId = this._convertId(userId);
264
+
265
+ const data = { ...updates };
266
+
267
+ if (data.organizationId !== undefined) {
268
+ data.organizationId = this._convertId(data.organizationId);
269
+ }
270
+ if (data.organization !== undefined) {
271
+ data.organizationId = this._convertId(data.organization);
272
+ delete data.organization;
273
+ }
274
+
275
+ if (
276
+ data.hashword !== undefined &&
277
+ data.hashword !== null &&
278
+ data.hashword !== ''
279
+ ) {
280
+ if (typeof data.hashword !== 'string') {
281
+ throw new ClientSafeError('Password must be a string', 400);
282
+ }
283
+
284
+ // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
285
+ if (data.hashword.startsWith('$2')) {
286
+ throw new Error(
287
+ 'Password appears to be already hashed. Pass plain text password only.'
288
+ );
289
+ }
290
+
291
+ data.hashword = await bcrypt.hash(data.hashword, 10);
292
+ }
293
+
294
+ const user = await this.prisma.user.update({
295
+ where: { id: intId },
296
+ data,
297
+ });
298
+ return this._convertUserIds(user);
299
+ }
300
+
301
+ /**
302
+ * Update organization user
303
+ * @param {string} userId - User ID (string from application layer)
304
+ * @param {Object} updates - Fields to update
305
+ * @returns {Promise<Object>} Updated user object with string IDs
306
+ */
307
+ async updateOrganizationUser(userId, updates) {
308
+ const intId = this._convertId(userId);
309
+ const user = await this.prisma.user.update({
310
+ where: { id: intId },
311
+ data: updates,
312
+ });
313
+ return this._convertUserIds(user);
314
+ }
315
+
316
+ /**
317
+ * Delete user by ID
318
+ * @param {string} userId - User ID to delete (string from application layer)
319
+ * @returns {Promise<boolean>} True if deleted successfully
320
+ */
321
+ async deleteUser(userId) {
322
+ try {
323
+ const intId = this._convertId(userId);
324
+ await this.prisma.user.delete({
325
+ where: { id: intId },
326
+ });
327
+ return true;
328
+ } catch (error) {
329
+ if (error.code === 'P2025') {
330
+ // Record not found
331
+ return false;
332
+ }
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Link an individual user to an organization user
339
+ * @param {string} individualUserId - Individual user ID (string from application layer)
340
+ * @param {string} organizationUserId - Organization user ID (string from application layer)
341
+ * @returns {Promise<Object>} Updated individual user with string IDs
342
+ */
343
+ async linkIndividualToOrganization(individualUserId, organizationUserId) {
344
+ const intIndividualId = this._convertId(individualUserId);
345
+ const intOrganizationId = this._convertId(organizationUserId);
346
+
347
+ const user = await this.prisma.user.update({
348
+ where: {
349
+ id: intIndividualId,
350
+ type: 'INDIVIDUAL',
351
+ },
352
+ data: {
353
+ organizationId: intOrganizationId,
354
+ },
355
+ });
356
+ return this._convertUserIds(user);
357
+ }
358
+ }
359
+
360
+ module.exports = { UserRepositoryPostgres };