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

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 (305) hide show
  1. package/CLAUDE.md +702 -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/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  69. package/generated/prisma-mongodb/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  70. package/generated/prisma-mongodb/package.json +183 -0
  71. package/generated/prisma-mongodb/runtime/edge-esm.js +34 -0
  72. package/generated/prisma-mongodb/runtime/edge.js +34 -0
  73. package/generated/prisma-mongodb/runtime/index-browser.d.ts +370 -0
  74. package/generated/prisma-mongodb/runtime/index-browser.js +16 -0
  75. package/generated/prisma-mongodb/runtime/library.d.ts +3977 -0
  76. package/generated/prisma-mongodb/runtime/library.js +146 -0
  77. package/generated/prisma-mongodb/runtime/react-native.js +83 -0
  78. package/generated/prisma-mongodb/runtime/wasm-compiler-edge.js +84 -0
  79. package/generated/prisma-mongodb/runtime/wasm-engine-edge.js +36 -0
  80. package/generated/prisma-mongodb/schema.prisma +368 -0
  81. package/generated/prisma-mongodb/wasm-edge-light-loader.mjs +4 -0
  82. package/generated/prisma-mongodb/wasm-worker-loader.mjs +4 -0
  83. package/generated/prisma-mongodb/wasm.d.ts +1 -0
  84. package/generated/prisma-mongodb/wasm.js +342 -0
  85. package/generated/prisma-postgresql/client.d.ts +1 -0
  86. package/generated/prisma-postgresql/client.js +4 -0
  87. package/generated/prisma-postgresql/default.d.ts +1 -0
  88. package/generated/prisma-postgresql/default.js +4 -0
  89. package/generated/prisma-postgresql/edge.d.ts +1 -0
  90. package/generated/prisma-postgresql/edge.js +357 -0
  91. package/generated/prisma-postgresql/index-browser.js +339 -0
  92. package/generated/prisma-postgresql/index.d.ts +25135 -0
  93. package/generated/prisma-postgresql/index.js +382 -0
  94. package/generated/prisma-postgresql/libquery_engine-debian-openssl-3.0.x.so.node +0 -0
  95. package/generated/prisma-postgresql/libquery_engine-rhel-openssl-3.0.x.so.node +0 -0
  96. package/generated/prisma-postgresql/package.json +183 -0
  97. package/generated/prisma-postgresql/query_engine_bg.js +2 -0
  98. package/generated/prisma-postgresql/query_engine_bg.wasm +0 -0
  99. package/generated/prisma-postgresql/runtime/edge-esm.js +34 -0
  100. package/generated/prisma-postgresql/runtime/edge.js +34 -0
  101. package/generated/prisma-postgresql/runtime/index-browser.d.ts +370 -0
  102. package/generated/prisma-postgresql/runtime/index-browser.js +16 -0
  103. package/generated/prisma-postgresql/runtime/library.d.ts +3977 -0
  104. package/generated/prisma-postgresql/runtime/library.js +146 -0
  105. package/generated/prisma-postgresql/runtime/react-native.js +83 -0
  106. package/generated/prisma-postgresql/runtime/wasm-compiler-edge.js +84 -0
  107. package/generated/prisma-postgresql/runtime/wasm-engine-edge.js +36 -0
  108. package/generated/prisma-postgresql/schema.prisma +351 -0
  109. package/generated/prisma-postgresql/wasm-edge-light-loader.mjs +4 -0
  110. package/generated/prisma-postgresql/wasm-worker-loader.mjs +4 -0
  111. package/generated/prisma-postgresql/wasm.d.ts +1 -0
  112. package/generated/prisma-postgresql/wasm.js +364 -0
  113. package/handlers/WEBHOOKS.md +653 -0
  114. package/handlers/app-definition-loader.js +38 -0
  115. package/handlers/app-handler-helpers.js +57 -0
  116. package/handlers/backend-utils.js +262 -0
  117. package/handlers/database-migration-handler.js +227 -0
  118. package/handlers/integration-event-dispatcher.js +54 -0
  119. package/handlers/routers/HEALTHCHECK.md +342 -0
  120. package/handlers/routers/auth.js +15 -0
  121. package/handlers/routers/db-migration.handler.js +29 -0
  122. package/handlers/routers/db-migration.js +326 -0
  123. package/handlers/routers/health.js +516 -0
  124. package/handlers/routers/integration-defined-routers.js +45 -0
  125. package/handlers/routers/integration-webhook-routers.js +67 -0
  126. package/handlers/routers/user.js +63 -0
  127. package/handlers/routers/websocket.js +57 -0
  128. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  129. package/handlers/use-cases/check-integrations-health-use-case.js +44 -0
  130. package/handlers/workers/db-migration.js +352 -0
  131. package/handlers/workers/dlq-processor.js +63 -0
  132. package/handlers/workers/integration-defined-workers.js +23 -0
  133. package/index.js +82 -46
  134. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +184 -0
  135. package/infrastructure/scheduler/index.js +33 -0
  136. package/infrastructure/scheduler/mock-scheduler-adapter.js +143 -0
  137. package/infrastructure/scheduler/scheduler-service-factory.js +73 -0
  138. package/infrastructure/scheduler/scheduler-service-interface.js +47 -0
  139. package/integrations/WEBHOOK-QUICKSTART.md +151 -0
  140. package/integrations/index.js +12 -10
  141. package/integrations/integration-base.js +364 -55
  142. package/integrations/integration-router.js +376 -179
  143. package/integrations/options.js +1 -1
  144. package/integrations/repositories/integration-mapping-repository-documentdb.js +280 -0
  145. package/integrations/repositories/integration-mapping-repository-factory.js +57 -0
  146. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  147. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  148. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  149. package/integrations/repositories/integration-mapping-repository.js +156 -0
  150. package/integrations/repositories/integration-repository-documentdb.js +219 -0
  151. package/integrations/repositories/integration-repository-factory.js +51 -0
  152. package/integrations/repositories/integration-repository-interface.js +144 -0
  153. package/integrations/repositories/integration-repository-mongo.js +330 -0
  154. package/integrations/repositories/integration-repository-postgres.js +385 -0
  155. package/integrations/repositories/process-repository-documentdb.js +311 -0
  156. package/integrations/repositories/process-repository-factory.js +53 -0
  157. package/integrations/repositories/process-repository-interface.js +136 -0
  158. package/integrations/repositories/process-repository-mongo.js +262 -0
  159. package/integrations/repositories/process-repository-postgres.js +380 -0
  160. package/integrations/repositories/process-update-ops-shared.js +112 -0
  161. package/integrations/tests/doubles/config-capturing-integration.js +81 -0
  162. package/integrations/tests/doubles/dummy-integration-class.js +105 -0
  163. package/integrations/tests/doubles/test-integration-repository.js +112 -0
  164. package/integrations/use-cases/create-integration.js +83 -0
  165. package/integrations/use-cases/create-process.js +128 -0
  166. package/integrations/use-cases/delete-integration-for-user.js +101 -0
  167. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  168. package/integrations/use-cases/get-integration-for-user.js +78 -0
  169. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  170. package/integrations/use-cases/get-integration-instance.js +83 -0
  171. package/integrations/use-cases/get-integrations-for-user.js +88 -0
  172. package/integrations/use-cases/get-possible-integrations.js +27 -0
  173. package/integrations/use-cases/get-process.js +87 -0
  174. package/integrations/use-cases/index.js +19 -0
  175. package/integrations/use-cases/load-integration-context.js +71 -0
  176. package/integrations/use-cases/update-integration-messages.js +44 -0
  177. package/integrations/use-cases/update-integration-status.js +32 -0
  178. package/integrations/use-cases/update-integration.js +92 -0
  179. package/integrations/use-cases/update-process-metrics.js +205 -0
  180. package/integrations/use-cases/update-process-state.js +158 -0
  181. package/integrations/utils/map-integration-dto.js +37 -0
  182. package/jest-global-setup-noop.js +3 -0
  183. package/jest-global-teardown-noop.js +3 -0
  184. package/logs/logger.js +0 -4
  185. package/{module-plugin → modules}/index.js +0 -10
  186. package/modules/module-factory.js +56 -0
  187. package/modules/module.js +258 -0
  188. package/modules/repositories/module-repository-documentdb.js +335 -0
  189. package/modules/repositories/module-repository-factory.js +40 -0
  190. package/modules/repositories/module-repository-interface.js +129 -0
  191. package/modules/repositories/module-repository-mongo.js +408 -0
  192. package/modules/repositories/module-repository-postgres.js +453 -0
  193. package/modules/repositories/module-repository.js +345 -0
  194. package/modules/requester/api-key.js +52 -0
  195. package/modules/requester/oauth-2.js +396 -0
  196. package/modules/requester/requester.js +275 -0
  197. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  198. package/{module-plugin → modules}/test/mock-api/definition.js +14 -10
  199. package/modules/tests/doubles/test-module-factory.js +16 -0
  200. package/modules/tests/doubles/test-module-repository.js +39 -0
  201. package/modules/use-cases/get-entities-for-user.js +32 -0
  202. package/modules/use-cases/get-entity-options-by-id.js +71 -0
  203. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  204. package/modules/use-cases/get-module-instance-from-type.js +34 -0
  205. package/modules/use-cases/get-module.js +74 -0
  206. package/modules/use-cases/process-authorization-callback.js +177 -0
  207. package/modules/use-cases/refresh-entity-options.js +72 -0
  208. package/modules/use-cases/test-module-auth.js +72 -0
  209. package/modules/utils/map-module-dto.js +18 -0
  210. package/package.json +82 -50
  211. package/prisma-mongodb/schema.prisma +368 -0
  212. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  213. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  214. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  215. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +25 -0
  216. package/prisma-postgresql/migrations/20260422120000_add_entity_data_column/migration.sql +10 -0
  217. package/prisma-postgresql/migrations/20260422120001_create_process_table/migration.sql +48 -0
  218. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  219. package/prisma-postgresql/schema.prisma +351 -0
  220. package/queues/queuer-util.js +103 -21
  221. package/syncs/manager.js +468 -443
  222. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  223. package/syncs/repositories/sync-repository-factory.js +43 -0
  224. package/syncs/repositories/sync-repository-interface.js +109 -0
  225. package/syncs/repositories/sync-repository-mongo.js +239 -0
  226. package/syncs/repositories/sync-repository-postgres.js +319 -0
  227. package/syncs/sync.js +0 -1
  228. package/token/repositories/token-repository-documentdb.js +137 -0
  229. package/token/repositories/token-repository-factory.js +40 -0
  230. package/token/repositories/token-repository-interface.js +131 -0
  231. package/token/repositories/token-repository-mongo.js +219 -0
  232. package/token/repositories/token-repository-postgres.js +264 -0
  233. package/token/repositories/token-repository.js +219 -0
  234. package/types/associations/index.d.ts +0 -17
  235. package/types/core/index.d.ts +12 -4
  236. package/types/database/index.d.ts +10 -2
  237. package/types/encrypt/index.d.ts +5 -3
  238. package/types/integrations/index.d.ts +3 -8
  239. package/types/module-plugin/index.d.ts +17 -69
  240. package/types/syncs/index.d.ts +0 -17
  241. package/user/repositories/user-repository-documentdb.js +441 -0
  242. package/user/repositories/user-repository-factory.js +52 -0
  243. package/user/repositories/user-repository-interface.js +201 -0
  244. package/user/repositories/user-repository-mongo.js +308 -0
  245. package/user/repositories/user-repository-postgres.js +360 -0
  246. package/user/tests/doubles/test-user-repository.js +72 -0
  247. package/user/use-cases/authenticate-user.js +127 -0
  248. package/user/use-cases/authenticate-with-shared-secret.js +48 -0
  249. package/user/use-cases/create-individual-user.js +61 -0
  250. package/user/use-cases/create-organization-user.js +47 -0
  251. package/user/use-cases/create-token-for-user-id.js +30 -0
  252. package/user/use-cases/get-user-from-adopter-jwt.js +149 -0
  253. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  254. package/user/use-cases/get-user-from-x-frigg-headers.js +132 -0
  255. package/user/use-cases/login-user.js +122 -0
  256. package/user/user.js +125 -0
  257. package/utils/backend-path.js +38 -0
  258. package/utils/index.js +6 -0
  259. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  260. package/websocket/repositories/websocket-connection-repository-factory.js +44 -0
  261. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  262. package/websocket/repositories/websocket-connection-repository-mongo.js +156 -0
  263. package/websocket/repositories/websocket-connection-repository-postgres.js +196 -0
  264. package/websocket/repositories/websocket-connection-repository.js +161 -0
  265. package/assertions/is-equal.js +0 -17
  266. package/associations/model.js +0 -54
  267. package/database/models/IndividualUser.js +0 -76
  268. package/database/models/OrganizationUser.js +0 -29
  269. package/database/models/State.js +0 -9
  270. package/database/models/Token.js +0 -70
  271. package/database/models/UserModel.js +0 -7
  272. package/database/models/WebsocketConnection.js +0 -49
  273. package/database/mongo.js +0 -45
  274. package/database/mongoose.js +0 -5
  275. package/encrypt/Cryptor.test.js +0 -32
  276. package/encrypt/encrypt.js +0 -132
  277. package/encrypt/encrypt.test.js +0 -1069
  278. package/encrypt/test-encrypt.js +0 -107
  279. package/errors/base-error.test.js +0 -32
  280. package/errors/fetch-error.test.js +0 -79
  281. package/errors/halt-error.test.js +0 -11
  282. package/errors/validation-errors.test.js +0 -120
  283. package/integrations/create-frigg-backend.js +0 -31
  284. package/integrations/integration-factory.js +0 -251
  285. package/integrations/integration-mapping.js +0 -43
  286. package/integrations/integration-model.js +0 -46
  287. package/integrations/integration-user.js +0 -144
  288. package/integrations/test/integration-base.test.js +0 -144
  289. package/lambda/TimeoutCatcher.test.js +0 -68
  290. package/logs/logger.test.js +0 -76
  291. package/module-plugin/auther.js +0 -393
  292. package/module-plugin/credential.js +0 -22
  293. package/module-plugin/entity-manager.js +0 -70
  294. package/module-plugin/entity.js +0 -46
  295. package/module-plugin/manager.js +0 -169
  296. package/module-plugin/module-factory.js +0 -61
  297. package/module-plugin/requester/api-key.js +0 -36
  298. package/module-plugin/requester/oauth-2.js +0 -219
  299. package/module-plugin/requester/requester.js +0 -165
  300. package/module-plugin/requester/requester.test.js +0 -28
  301. package/module-plugin/test/auther.test.js +0 -97
  302. package/syncs/model.js +0 -62
  303. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  304. /package/{module-plugin → modules}/requester/basic.js +0 -0
  305. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,380 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ProcessRepositoryInterface,
4
+ } = require('./process-repository-interface');
5
+ const { validateOps, splitPath } = require('./process-update-ops-shared');
6
+
7
+ /**
8
+ * PostgreSQL Process Repository Adapter
9
+ * Handles process persistence using Prisma with PostgreSQL
10
+ *
11
+ * PostgreSQL-specific characteristics:
12
+ * - Uses foreign key constraints for relations
13
+ * - JSONB type for context and results (efficient querying)
14
+ * - Array type for childProcesses references
15
+ * - Transactional support available if needed
16
+ *
17
+ * Design Philosophy:
18
+ * - Same interface as MongoDB repository
19
+ * - Prisma abstracts away most database-specific details
20
+ * - Minor differences in JSON handling internally managed by Prisma
21
+ */
22
+ class ProcessRepositoryPostgres extends ProcessRepositoryInterface {
23
+ constructor() {
24
+ super();
25
+ this.prisma = prisma;
26
+ }
27
+
28
+ /**
29
+ * Convert string ID to integer for PostgreSQL queries
30
+ * @private
31
+ * @param {string|number|null|undefined} id - ID to convert
32
+ * @returns {number|null|undefined} Integer ID or null/undefined
33
+ * @throws {Error} If ID cannot be converted to integer
34
+ */
35
+ _convertId(id) {
36
+ if (id === null || id === undefined) return id;
37
+ const parsed = parseInt(id, 10);
38
+ if (isNaN(parsed)) {
39
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
40
+ }
41
+ return parsed;
42
+ }
43
+
44
+ /**
45
+ * Create a new process record
46
+ * @param {Object} processData - Process data to create
47
+ * @returns {Promise<Object>} Created process record
48
+ */
49
+ async create(processData) {
50
+ const process = await this.prisma.process.create({
51
+ data: {
52
+ userId: this._convertId(processData.userId),
53
+ integrationId: this._convertId(processData.integrationId),
54
+ name: processData.name,
55
+ type: processData.type,
56
+ state: processData.state || 'INITIALIZING',
57
+ context: processData.context || {},
58
+ results: processData.results || {},
59
+ parentProcessId: this._convertId(processData.parentProcessId),
60
+ },
61
+ });
62
+
63
+ return this._toPlainObject(process);
64
+ }
65
+
66
+ /**
67
+ * Find a process by ID
68
+ * @param {string} processId - Process ID to find
69
+ * @returns {Promise<Object|null>} Process record or null if not found
70
+ */
71
+ async findById(processId) {
72
+ const process = await this.prisma.process.findUnique({
73
+ where: { id: this._convertId(processId) },
74
+ });
75
+
76
+ return process ? this._toPlainObject(process) : null;
77
+ }
78
+
79
+ /**
80
+ * Update a process record
81
+ * @param {string} processId - Process ID to update
82
+ * @param {Object} updates - Fields to update
83
+ * @returns {Promise<Object>} Updated process record
84
+ */
85
+ async update(processId, updates) {
86
+ // Prepare update data, excluding undefined values
87
+ const updateData = {};
88
+
89
+ if (updates.state !== undefined) {
90
+ updateData.state = updates.state;
91
+ }
92
+ if (updates.context !== undefined) {
93
+ updateData.context = updates.context;
94
+ }
95
+ if (updates.results !== undefined) {
96
+ updateData.results = updates.results;
97
+ }
98
+ if (updates.parentProcessId !== undefined) {
99
+ updateData.parentProcessId = this._convertId(
100
+ updates.parentProcessId
101
+ );
102
+ }
103
+
104
+ const process = await this.prisma.process.update({
105
+ where: { id: this._convertId(processId) },
106
+ data: updateData,
107
+ });
108
+
109
+ return this._toPlainObject(process);
110
+ }
111
+
112
+ /**
113
+ * Atomic process update — race-safe counterpart to `update()`.
114
+ *
115
+ * Compiles the `ProcessUpdateOps` into ONE `UPDATE "Process" ...
116
+ * RETURNING *` statement with nested `jsonb_set` calls for every
117
+ * context/results mutation. Postgres applies row-level locking
118
+ * during UPDATE, so concurrent callers on the same row serialize at
119
+ * the DB without any read-modify-write in Node.
120
+ *
121
+ * Path segments have been regex-validated upstream (see
122
+ * process-update-ops-shared.js); they are embedded directly into
123
+ * the SQL string. All values go through positional parameters.
124
+ *
125
+ * @param {string} processId
126
+ * @param {ProcessUpdateOps} ops
127
+ * @returns {Promise<Object|null>}
128
+ */
129
+ async applyProcessUpdate(processId, ops) {
130
+ const normalized = validateOps(ops);
131
+ const id = this._convertId(processId);
132
+
133
+ // Build the SQL expression for each JSON column. We start each
134
+ // column's expression from the column itself and wrap it in
135
+ // jsonb_set(...) calls — one wrap per operation targeting that
136
+ // column. If no op targets a column, we omit that SET clause so
137
+ // we don't issue a pointless self-assignment.
138
+ const params = [];
139
+ /** @type {(v:unknown)=>string} positional placeholder, 1-indexed */
140
+ const bind = (v) => {
141
+ params.push(v);
142
+ return `$${params.length}`;
143
+ };
144
+
145
+ const columnExpressions = this._buildColumnExpressions(
146
+ normalized,
147
+ bind
148
+ );
149
+ const setClauses = [];
150
+ for (const [column, expr] of Object.entries(columnExpressions)) {
151
+ setClauses.push(`"${column}" = ${expr}`);
152
+ }
153
+ if (normalized.newState !== null) {
154
+ setClauses.push(`"state" = ${bind(normalized.newState)}`);
155
+ }
156
+ setClauses.push(`"updatedAt" = NOW()`);
157
+
158
+ const idPlaceholder = bind(id);
159
+ const sql = `
160
+ UPDATE "Process"
161
+ SET ${setClauses.join(', ')}
162
+ WHERE "id" = ${idPlaceholder}
163
+ RETURNING *
164
+ `;
165
+
166
+ const rows = await this.prisma.$queryRawUnsafe(sql, ...params);
167
+ if (!rows || rows.length === 0) return null;
168
+ return this._toPlainObject(rows[0]);
169
+ }
170
+
171
+ /**
172
+ * Returns a map of column → SQL expression with all jsonb_set wraps
173
+ * applied. Used only by applyProcessUpdate.
174
+ * @private
175
+ */
176
+ _buildColumnExpressions(ops, bind) {
177
+ const byColumn = { context: null, results: null };
178
+
179
+ // Seed with the column itself (wrapped with COALESCE so that
180
+ // a NULL column doesn't break jsonb_set).
181
+ const seed = (col) =>
182
+ byColumn[col] ??
183
+ (byColumn[col] = `COALESCE("${col}", '{}'::jsonb)`);
184
+
185
+ /**
186
+ * Postgres `jsonb_set(target, path, value, create_missing=true)`
187
+ * only creates the LEAF segment if missing — intermediate segments
188
+ * that don't exist as objects cause the call to return `target`
189
+ * unchanged (silent no-op). For a path like `context.a.b.c` on a
190
+ * doc where `context.a` is missing, we'd bail on the write.
191
+ *
192
+ * This helper wraps `prev` in a chain of `jsonb_set` calls that
193
+ * ensure each intermediate prefix path is an object, preserving
194
+ * its contents if it's already present:
195
+ *
196
+ * ensureParents(prev, ['a','b','c'])
197
+ * ⇒ jsonb_set(
198
+ * jsonb_set(prev, '{a}', COALESCE(prev#>'{a}', '{}'::jsonb), true),
199
+ * '{a,b}', COALESCE(${that}#>'{a,b}', '{}'::jsonb), true)
200
+ *
201
+ * The caller then wraps this result with its own `jsonb_set` for
202
+ * the leaf segment. Depth-1 paths skip this entirely (no parents
203
+ * to synthesize).
204
+ */
205
+ const ensureParents = (prevExpr, segments) => {
206
+ let cur = prevExpr;
207
+ for (let i = 1; i < segments.length; i++) {
208
+ const parentPath = `'{${segments.slice(0, i).join(',')}}'`;
209
+ cur = `jsonb_set(${cur}, ${parentPath}, COALESCE(${cur} #> ${parentPath}, '{}'::jsonb), true)`;
210
+ }
211
+ return cur;
212
+ };
213
+
214
+ const wrapIncrement = (col, segments, delta) => {
215
+ const textPath = `'{${segments.join(',')}}'`;
216
+ const jsonbPath = `'{${segments.join(',')}}'`;
217
+ const prev = seed(col);
218
+ const guarded = ensureParents(prev, segments);
219
+ const nextValue = `to_jsonb(COALESCE((${guarded} #>> ${textPath})::numeric, 0) + ${bind(delta)})`;
220
+ byColumn[col] = `jsonb_set(${guarded}, ${jsonbPath}, ${nextValue}, true)`;
221
+ };
222
+
223
+ const wrapSet = (col, segments, value) => {
224
+ const jsonbPath = `'{${segments.join(',')}}'`;
225
+ const prev = seed(col);
226
+ const guarded = ensureParents(prev, segments);
227
+ // $n::jsonb — values are serialized to JSON by Prisma when
228
+ // passed as a parameter, then cast back into jsonb.
229
+ byColumn[col] = `jsonb_set(${guarded}, ${jsonbPath}, ${bind(JSON.stringify(value))}::jsonb, true)`;
230
+ };
231
+
232
+ const wrapPushSlice = (col, segments, spec) => {
233
+ const jsonbPath = `'{${segments.join(',')}}'`;
234
+ const prev = seed(col);
235
+ const guarded = ensureParents(prev, segments);
236
+ // Construct the sliced array in a CTE to evaluate `${newArr}`
237
+ // exactly ONCE (vs. the inline form that Postgres would still
238
+ // execute correctly but expand three times). Order is
239
+ // explicitly preserved by `jsonb_agg(... ORDER BY idx)`;
240
+ // without the ORDER BY, aggregate order is implementation-
241
+ // defined even with WITH ORDINALITY.
242
+ const sliced = `(
243
+ WITH combined AS (
244
+ SELECT COALESCE((${guarded} #> ${jsonbPath}), '[]'::jsonb) || ${bind(JSON.stringify(spec.values))}::jsonb AS arr
245
+ )
246
+ SELECT COALESCE(jsonb_agg(elem ORDER BY idx), '[]'::jsonb)
247
+ FROM combined,
248
+ jsonb_array_elements((SELECT arr FROM combined)) WITH ORDINALITY AS t(elem, idx)
249
+ WHERE idx > GREATEST(0, jsonb_array_length((SELECT arr FROM combined)) - ${bind(spec.keepLast)})
250
+ )`;
251
+ byColumn[col] = `jsonb_set(${guarded}, ${jsonbPath}, ${sliced}, true)`;
252
+ };
253
+
254
+ for (const [path, delta] of Object.entries(ops.increment)) {
255
+ const { column, segments } = splitPath(path);
256
+ wrapIncrement(column, segments, delta);
257
+ }
258
+ for (const [path, value] of Object.entries(ops.set)) {
259
+ const { column, segments } = splitPath(path);
260
+ wrapSet(column, segments, value);
261
+ }
262
+ for (const [path, spec] of Object.entries(ops.pushSlice)) {
263
+ const { column, segments } = splitPath(path);
264
+ wrapPushSlice(column, segments, spec);
265
+ }
266
+
267
+ const result = {};
268
+ for (const [col, expr] of Object.entries(byColumn)) {
269
+ if (expr !== null) result[col] = expr;
270
+ }
271
+ return result;
272
+ }
273
+
274
+ /**
275
+ * Find processes by integration and type
276
+ * @param {string} integrationId - Integration ID
277
+ * @param {string} type - Process type
278
+ * @returns {Promise<Array>} Array of process records
279
+ */
280
+ async findByIntegrationAndType(integrationId, type) {
281
+ const processes = await this.prisma.process.findMany({
282
+ where: {
283
+ integrationId: this._convertId(integrationId),
284
+ type,
285
+ },
286
+ orderBy: {
287
+ createdAt: 'desc',
288
+ },
289
+ });
290
+
291
+ return processes.map((p) => this._toPlainObject(p));
292
+ }
293
+
294
+ /**
295
+ * Find active processes (not in excluded states)
296
+ * @param {string} integrationId - Integration ID
297
+ * @param {string[]} [excludeStates=['COMPLETED', 'ERROR']] - States to exclude
298
+ * @returns {Promise<Array>} Array of active process records
299
+ */
300
+ async findActiveProcesses(
301
+ integrationId,
302
+ excludeStates = ['COMPLETED', 'ERROR']
303
+ ) {
304
+ const processes = await this.prisma.process.findMany({
305
+ where: {
306
+ integrationId: this._convertId(integrationId),
307
+ state: {
308
+ notIn: excludeStates,
309
+ },
310
+ },
311
+ orderBy: {
312
+ createdAt: 'desc',
313
+ },
314
+ });
315
+
316
+ return processes.map((p) => this._toPlainObject(p));
317
+ }
318
+
319
+ /**
320
+ * Find a process by name (most recent)
321
+ * @param {string} name - Process name
322
+ * @returns {Promise<Object|null>} Most recent process with given name, or null
323
+ */
324
+ async findByName(name) {
325
+ const process = await this.prisma.process.findFirst({
326
+ where: { name },
327
+ orderBy: {
328
+ createdAt: 'desc',
329
+ },
330
+ });
331
+
332
+ return process ? this._toPlainObject(process) : null;
333
+ }
334
+
335
+ /**
336
+ * Delete a process by ID
337
+ * @param {string} processId - Process ID to delete
338
+ * @returns {Promise<void>}
339
+ */
340
+ async deleteById(processId) {
341
+ await this.prisma.process.delete({
342
+ where: { id: this._convertId(processId) },
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Convert Prisma model to plain JavaScript object
348
+ * Ensures consistent API across repository implementations
349
+ * @private
350
+ * @param {Object} process - Prisma process model
351
+ * @returns {Object} Plain process object
352
+ */
353
+ _toPlainObject(process) {
354
+ return {
355
+ id: String(process.id),
356
+ userId: String(process.userId),
357
+ integrationId: String(process.integrationId),
358
+ name: process.name,
359
+ type: process.type,
360
+ state: process.state,
361
+ context: process.context,
362
+ results: process.results,
363
+ childProcesses: Array.isArray(process.childProcesses)
364
+ ? process.childProcesses.length > 0 &&
365
+ typeof process.childProcesses[0] === 'object' &&
366
+ process.childProcesses[0] !== null
367
+ ? process.childProcesses.map((child) => String(child.id))
368
+ : process.childProcesses
369
+ : [],
370
+ parentProcessId:
371
+ process.parentProcessId !== null
372
+ ? String(process.parentProcessId)
373
+ : null,
374
+ createdAt: process.createdAt,
375
+ updatedAt: process.updatedAt,
376
+ };
377
+ }
378
+ }
379
+
380
+ module.exports = { ProcessRepositoryPostgres };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Shared helpers for ProcessRepository.applyProcessUpdate() validation.
3
+ *
4
+ * These utilities are backend-agnostic: they enforce invariants on the
5
+ * `ProcessUpdateOps` shape BEFORE each adapter emits any SQL or database
6
+ * command. Keeping validation here means any bug we fix (e.g. tighter
7
+ * path regex, size cap) fixes all three adapters in one place.
8
+ *
9
+ * Imported by the Postgres, MongoDB, and DocumentDB adapters.
10
+ */
11
+
12
+ /**
13
+ * Allowed dot-path shape. Root must be `context` or `results`, and each
14
+ * segment after the first must be a JS-identifier-style token. Numeric
15
+ * segments (array indices) and bracket syntax are intentionally
16
+ * disallowed — array element mutation is exclusively handled via
17
+ * `pushSlice`, which targets a whole array at a path.
18
+ */
19
+ const PATH_REGEX = /^(context|results)(\.[a-zA-Z_][a-zA-Z0-9_]*)+$/;
20
+
21
+ /**
22
+ * Normalizes and validates a `ProcessUpdateOps` object. Returns a frozen
23
+ * copy with defaults applied and every key pre-validated. Throws synchronously
24
+ * on any shape error so adapters can fail fast before touching the DB.
25
+ *
26
+ * @param {Object} ops
27
+ * @returns {{
28
+ * increment: Record<string, number>,
29
+ * set: Record<string, unknown>,
30
+ * pushSlice: Record<string, { values: unknown[]; keepLast: number }>,
31
+ * newState: string|null,
32
+ * }}
33
+ */
34
+ function validateOps(ops) {
35
+ if (!ops || typeof ops !== 'object' || Array.isArray(ops)) {
36
+ throw new Error('applyProcessUpdate: ops must be an object');
37
+ }
38
+
39
+ const increment = ops.increment || {};
40
+ const set = ops.set || {};
41
+ const pushSlice = ops.pushSlice || {};
42
+ const newState = ops.newState ?? null;
43
+
44
+ for (const [path, delta] of Object.entries(increment)) {
45
+ assertPath(path, 'increment');
46
+ if (typeof delta !== 'number' || !Number.isFinite(delta)) {
47
+ throw new Error(
48
+ `applyProcessUpdate: increment['${path}'] must be a finite number, got ${typeof delta}`
49
+ );
50
+ }
51
+ }
52
+
53
+ for (const path of Object.keys(set)) {
54
+ assertPath(path, 'set');
55
+ }
56
+
57
+ for (const [path, spec] of Object.entries(pushSlice)) {
58
+ assertPath(path, 'pushSlice');
59
+ if (
60
+ !spec ||
61
+ typeof spec !== 'object' ||
62
+ !Array.isArray(spec.values) ||
63
+ typeof spec.keepLast !== 'number' ||
64
+ !Number.isInteger(spec.keepLast) ||
65
+ spec.keepLast <= 0
66
+ ) {
67
+ throw new Error(
68
+ `applyProcessUpdate: pushSlice['${path}'] must be { values: [], keepLast: positive integer }`
69
+ );
70
+ }
71
+ }
72
+
73
+ if (newState !== null && typeof newState !== 'string') {
74
+ throw new Error('applyProcessUpdate: newState must be a string');
75
+ }
76
+
77
+ const hasAnyOp =
78
+ Object.keys(increment).length > 0 ||
79
+ Object.keys(set).length > 0 ||
80
+ Object.keys(pushSlice).length > 0 ||
81
+ newState !== null;
82
+ if (!hasAnyOp) {
83
+ throw new Error(
84
+ 'applyProcessUpdate: at least one of increment/set/pushSlice/newState must be provided'
85
+ );
86
+ }
87
+
88
+ return Object.freeze({ increment, set, pushSlice, newState });
89
+ }
90
+
91
+ function assertPath(path, opName) {
92
+ if (!PATH_REGEX.test(path)) {
93
+ throw new Error(
94
+ `applyProcessUpdate: invalid path '${path}' in ${opName} (must match ${PATH_REGEX})`
95
+ );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Splits a validated path into `{ column, segments }`.
101
+ * `'context.pagination.pageCount'` → `{ column: 'context', segments: ['pagination', 'pageCount'] }`.
102
+ */
103
+ function splitPath(path) {
104
+ const [column, ...segments] = path.split('.');
105
+ return { column, segments };
106
+ }
107
+
108
+ module.exports = {
109
+ PATH_REGEX,
110
+ validateOps,
111
+ splitPath,
112
+ };
@@ -0,0 +1,81 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class ConfigCapturingModule {
4
+ static definition = {
5
+ getName: () => 'config-capturing-module'
6
+ };
7
+ }
8
+
9
+ class ConfigCapturingIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'config-capturing',
12
+ version: '1.0.0',
13
+ modules: {
14
+ primary: ConfigCapturingModule
15
+ },
16
+ display: {
17
+ label: 'Config Capturing Integration',
18
+ description: 'Test double for capturing config state during updates',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'test-icon'
21
+ }
22
+ };
23
+
24
+ static _capturedOnUpdateState = null;
25
+
26
+ static resetCaptures() {
27
+ this._capturedOnUpdateState = null;
28
+ }
29
+
30
+ static getCapturedOnUpdateState() {
31
+ return this._capturedOnUpdateState;
32
+ }
33
+
34
+ constructor(params) {
35
+ super(params);
36
+ this.integrationRepository = {
37
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
38
+ findIntegrationById: jest.fn().mockResolvedValue({}),
39
+ };
40
+ this.updateIntegrationStatus = {
41
+ execute: jest.fn().mockResolvedValue({})
42
+ };
43
+ this.updateIntegrationMessages = {
44
+ execute: jest.fn().mockResolvedValue({})
45
+ };
46
+ }
47
+
48
+ async initialize() {
49
+ this.registerEventHandlers();
50
+ }
51
+
52
+ async onUpdate(params) {
53
+ ConfigCapturingIntegration._capturedOnUpdateState = {
54
+ thisConfig: JSON.parse(JSON.stringify(this.config)),
55
+ paramsConfig: params.config
56
+ };
57
+
58
+ this.config = this._deepMerge(this.config, params.config);
59
+ }
60
+
61
+ _deepMerge(target, source) {
62
+ const result = { ...target };
63
+ for (const key of Object.keys(source)) {
64
+ if (
65
+ source[key] !== null &&
66
+ typeof source[key] === 'object' &&
67
+ !Array.isArray(source[key]) &&
68
+ target[key] !== null &&
69
+ typeof target[key] === 'object' &&
70
+ !Array.isArray(target[key])
71
+ ) {
72
+ result[key] = this._deepMerge(target[key], source[key]);
73
+ } else {
74
+ result[key] = source[key];
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+ }
80
+
81
+ module.exports = { ConfigCapturingIntegration };
@@ -0,0 +1,105 @@
1
+ const { IntegrationBase } = require('../../integration-base');
2
+
3
+ class DummyModule {
4
+ static definition = {
5
+ getName: () => 'dummy'
6
+ };
7
+ }
8
+
9
+ class DummyIntegration extends IntegrationBase {
10
+ static Definition = {
11
+ name: 'dummy',
12
+ version: '1.0.0',
13
+ modules: {
14
+ dummy: DummyModule
15
+ },
16
+ display: {
17
+ label: 'Dummy Integration',
18
+ description: 'A dummy integration for testing',
19
+ detailsUrl: 'https://example.com',
20
+ icon: 'dummy-icon'
21
+ }
22
+ };
23
+
24
+ static getOptionDetails() {
25
+ return {
26
+ name: this.Definition.name,
27
+ version: this.Definition.version,
28
+ display: this.Definition.display
29
+ };
30
+ }
31
+
32
+ constructor(params) {
33
+ super(params);
34
+ this.sendSpy = jest.fn();
35
+ this.eventCallHistory = [];
36
+ this.events = {};
37
+
38
+ this.integrationRepository = {
39
+ updateIntegrationById: jest.fn().mockResolvedValue({}),
40
+ findIntegrationById: jest.fn().mockResolvedValue({}),
41
+ };
42
+
43
+ this.updateIntegrationStatus = {
44
+ execute: jest.fn().mockResolvedValue({})
45
+ };
46
+
47
+ this.updateIntegrationMessages = {
48
+ execute: jest.fn().mockResolvedValue({})
49
+ };
50
+ }
51
+
52
+ async loadDynamicUserActions() {
53
+ return {};
54
+ }
55
+
56
+ async send(event, data) {
57
+ this.sendSpy(event, data);
58
+ this.eventCallHistory.push({ event, data, timestamp: Date.now() });
59
+ if (event === 'ON_UPDATE') {
60
+ await this.onUpdate(data);
61
+ }
62
+ return { event, data };
63
+ }
64
+
65
+ async initialize() {
66
+ return;
67
+ }
68
+
69
+ async onCreate({ integrationId }) {
70
+ return;
71
+ }
72
+
73
+ async onUpdate(params) {
74
+ this.config = this._deepMerge(this.config, params.config);
75
+ }
76
+
77
+ _deepMerge(target, source) {
78
+ const result = { ...target };
79
+ for (const key of Object.keys(source)) {
80
+ if (
81
+ source[key] !== null &&
82
+ typeof source[key] === 'object' &&
83
+ !Array.isArray(source[key]) &&
84
+ target[key] !== null &&
85
+ typeof target[key] === 'object' &&
86
+ !Array.isArray(target[key])
87
+ ) {
88
+ result[key] = this._deepMerge(target[key], source[key]);
89
+ } else {
90
+ result[key] = source[key];
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ async onDelete(params) {
97
+ return;
98
+ }
99
+
100
+ getConfig() {
101
+ return this.config || {};
102
+ }
103
+ }
104
+
105
+ module.exports = { DummyIntegration };