@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0

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 (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
@@ -0,0 +1,320 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ScriptExecutionRepositoryInterface,
4
+ } = require('./script-execution-repository-interface');
5
+
6
+ /**
7
+ * PostgreSQL Script Execution Repository Adapter
8
+ * Handles script execution persistence using Prisma with PostgreSQL
9
+ *
10
+ * PostgreSQL-specific characteristics:
11
+ * - Uses Int IDs with autoincrement
12
+ * - Requires ID conversion: String (app layer) ↔ Int (database)
13
+ * - All returned IDs are converted to strings for application layer consistency
14
+ * - logs field is Json[] - supports push operations
15
+ */
16
+ class ScriptExecutionRepositoryPostgres extends ScriptExecutionRepositoryInterface {
17
+ constructor() {
18
+ super();
19
+ this.prisma = prisma;
20
+ }
21
+
22
+ /**
23
+ * Convert string ID to integer for PostgreSQL queries
24
+ * @private
25
+ * @param {string|number|null|undefined} id - ID to convert
26
+ * @returns {number|null|undefined} Integer ID or null/undefined
27
+ * @throws {Error} If ID cannot be converted to integer
28
+ */
29
+ _convertId(id) {
30
+ if (id === null || id === undefined) return id;
31
+ const parsed = Number.parseInt(id, 10);
32
+ if (Number.isNaN(parsed)) {
33
+ throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
34
+ }
35
+ return parsed;
36
+ }
37
+
38
+ /**
39
+ * Convert execution object IDs to strings
40
+ * @private
41
+ * @param {Object|null} execution - Execution object from database
42
+ * @returns {Object|null} Execution with string IDs
43
+ */
44
+ _convertExecutionIds(execution) {
45
+ if (!execution) return execution;
46
+ return {
47
+ ...execution,
48
+ id: execution.id?.toString(),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Create a new script execution record
54
+ *
55
+ * @param {Object} params - Execution creation parameters
56
+ * @param {string} params.scriptName - Name of the script being executed
57
+ * @param {string} [params.scriptVersion] - Version of the script
58
+ * @param {string} params.trigger - Trigger type
59
+ * @param {string} [params.mode] - Execution mode ('sync' or 'async', default 'async')
60
+ * @param {Object} [params.input] - Input parameters for the script
61
+ * @param {Object} [params.audit] - Audit information
62
+ * @param {string} [params.audit.apiKeyName] - Name of API key used
63
+ * @param {string} [params.audit.apiKeyLast4] - Last 4 chars of API key
64
+ * @param {string} [params.audit.ipAddress] - IP address of requester
65
+ * @returns {Promise<Object>} The created execution record with string ID
66
+ */
67
+ async createExecution({
68
+ scriptName,
69
+ scriptVersion,
70
+ trigger,
71
+ mode,
72
+ input,
73
+ audit,
74
+ }) {
75
+ const data = {
76
+ scriptName,
77
+ scriptVersion,
78
+ trigger,
79
+ mode: mode || 'async',
80
+ input,
81
+ logs: [],
82
+ };
83
+
84
+ // Map audit object to separate fields
85
+ if (audit) {
86
+ if (audit.apiKeyName) data.auditApiKeyName = audit.apiKeyName;
87
+ if (audit.apiKeyLast4) data.auditApiKeyLast4 = audit.apiKeyLast4;
88
+ if (audit.ipAddress) data.auditIpAddress = audit.ipAddress;
89
+ }
90
+
91
+ const execution = await this.prisma.scriptExecution.create({
92
+ data,
93
+ });
94
+
95
+ return this._convertExecutionIds(execution);
96
+ }
97
+
98
+ /**
99
+ * Find an execution by its ID
100
+ *
101
+ * @param {string|number} id - The execution ID
102
+ * @returns {Promise<Object|null>} The execution record with string ID or null if not found
103
+ */
104
+ async findExecutionById(id) {
105
+ const intId = this._convertId(id);
106
+ const execution = await this.prisma.scriptExecution.findUnique({
107
+ where: { id: intId },
108
+ });
109
+
110
+ return this._convertExecutionIds(execution);
111
+ }
112
+
113
+ /**
114
+ * Find all executions for a specific script
115
+ *
116
+ * @param {string} scriptName - The script name to filter by
117
+ * @param {Object} [options] - Query options
118
+ * @param {number} [options.limit] - Maximum number of results
119
+ * @param {number} [options.offset] - Number of results to skip
120
+ * @param {string} [options.sortBy] - Field to sort by
121
+ * @param {string} [options.sortOrder] - Sort order ('asc' or 'desc')
122
+ * @returns {Promise<Array>} Array of execution records with string IDs
123
+ */
124
+ async findExecutionsByScriptName(scriptName, options = {}) {
125
+ const {
126
+ limit,
127
+ offset,
128
+ sortBy = 'createdAt',
129
+ sortOrder = 'desc',
130
+ } = options;
131
+
132
+ const executions = await this.prisma.scriptExecution.findMany({
133
+ where: { scriptName },
134
+ orderBy: { [sortBy]: sortOrder },
135
+ take: limit,
136
+ skip: offset,
137
+ });
138
+
139
+ return executions.map((execution) =>
140
+ this._convertExecutionIds(execution)
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Find all executions with a specific status
146
+ *
147
+ * @param {string} status - Status to filter by
148
+ * @param {Object} [options] - Query options
149
+ * @param {number} [options.limit] - Maximum number of results
150
+ * @param {number} [options.offset] - Number of results to skip
151
+ * @param {string} [options.sortBy] - Field to sort by
152
+ * @param {string} [options.sortOrder] - Sort order ('asc' or 'desc')
153
+ * @returns {Promise<Array>} Array of execution records with string IDs
154
+ */
155
+ async findExecutionsByStatus(status, options = {}) {
156
+ const {
157
+ limit,
158
+ offset,
159
+ sortBy = 'createdAt',
160
+ sortOrder = 'desc',
161
+ } = options;
162
+
163
+ const executions = await this.prisma.scriptExecution.findMany({
164
+ where: { status },
165
+ orderBy: { [sortBy]: sortOrder },
166
+ take: limit,
167
+ skip: offset,
168
+ });
169
+
170
+ return executions.map((execution) =>
171
+ this._convertExecutionIds(execution)
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Update the status of an execution
177
+ *
178
+ * @param {string|number} id - The execution ID
179
+ * @param {string} status - New status value
180
+ * @returns {Promise<Object>} Updated execution record with string ID
181
+ */
182
+ async updateExecutionStatus(id, status) {
183
+ const intId = this._convertId(id);
184
+ const execution = await this.prisma.scriptExecution.update({
185
+ where: { id: intId },
186
+ data: { status },
187
+ });
188
+
189
+ return this._convertExecutionIds(execution);
190
+ }
191
+
192
+ /**
193
+ * Update the output result of an execution
194
+ *
195
+ * @param {string|number} id - The execution ID
196
+ * @param {Object} output - Output data from the script
197
+ * @returns {Promise<Object>} Updated execution record with string ID
198
+ */
199
+ async updateExecutionOutput(id, output) {
200
+ const intId = this._convertId(id);
201
+ const execution = await this.prisma.scriptExecution.update({
202
+ where: { id: intId },
203
+ data: { output },
204
+ });
205
+
206
+ return this._convertExecutionIds(execution);
207
+ }
208
+
209
+ /**
210
+ * Update the error details of a failed execution
211
+ *
212
+ * @param {string|number} id - The execution ID
213
+ * @param {Object} error - Error information
214
+ * @param {string} error.name - Error name/type
215
+ * @param {string} error.message - Error message
216
+ * @param {string} [error.stack] - Error stack trace
217
+ * @returns {Promise<Object>} Updated execution record with string ID
218
+ */
219
+ async updateExecutionError(id, error) {
220
+ const intId = this._convertId(id);
221
+ const execution = await this.prisma.scriptExecution.update({
222
+ where: { id: intId },
223
+ data: {
224
+ errorName: error.name,
225
+ errorMessage: error.message,
226
+ errorStack: error.stack,
227
+ },
228
+ });
229
+
230
+ return this._convertExecutionIds(execution);
231
+ }
232
+
233
+ /**
234
+ * Update the performance metrics of an execution
235
+ *
236
+ * @param {string|number} id - The execution ID
237
+ * @param {Object} metrics - Performance metrics
238
+ * @param {Date} [metrics.startTime] - Execution start time
239
+ * @param {Date} [metrics.endTime] - Execution end time
240
+ * @param {number} [metrics.durationMs] - Duration in milliseconds
241
+ * @returns {Promise<Object>} Updated execution record with string ID
242
+ */
243
+ async updateExecutionMetrics(id, metrics) {
244
+ const intId = this._convertId(id);
245
+ const data = {};
246
+ if (metrics.startTime !== undefined)
247
+ data.metricsStartTime = metrics.startTime;
248
+ if (metrics.endTime !== undefined)
249
+ data.metricsEndTime = metrics.endTime;
250
+ if (metrics.durationMs !== undefined)
251
+ data.metricsDurationMs = metrics.durationMs;
252
+
253
+ const execution = await this.prisma.scriptExecution.update({
254
+ where: { id: intId },
255
+ data,
256
+ });
257
+
258
+ return this._convertExecutionIds(execution);
259
+ }
260
+
261
+ /**
262
+ * Append a log entry to an execution's log array
263
+ *
264
+ * @param {string|number} id - The execution ID
265
+ * @param {Object} logEntry - Log entry to append
266
+ * @param {string} logEntry.level - Log level ('debug', 'info', 'warn', 'error')
267
+ * @param {string} logEntry.message - Log message
268
+ * @param {Object} [logEntry.data] - Additional log data
269
+ * @param {string} logEntry.timestamp - ISO timestamp
270
+ * @returns {Promise<Object>} Updated execution record with string ID
271
+ */
272
+ async appendExecutionLog(id, logEntry) {
273
+ const intId = this._convertId(id);
274
+
275
+ // Get current execution
276
+ const execution = await this.prisma.scriptExecution.findUnique({
277
+ where: { id: intId },
278
+ });
279
+
280
+ if (!execution) {
281
+ throw new Error(`Execution ${id} not found`);
282
+ }
283
+
284
+ // Append log entry to logs array (copy to avoid mutating original)
285
+ const logs = Array.isArray(execution.logs) ? [...execution.logs] : [];
286
+ logs.push(logEntry);
287
+
288
+ // Update with new logs array
289
+ const updated = await this.prisma.scriptExecution.update({
290
+ where: { id: intId },
291
+ data: { logs },
292
+ });
293
+
294
+ return this._convertExecutionIds(updated);
295
+ }
296
+
297
+ /**
298
+ * Delete all executions older than a specific date
299
+ * Used for cleanup and retention policies
300
+ *
301
+ * @param {Date} date - Delete executions older than this date
302
+ * @returns {Promise<Object>} Deletion result with count
303
+ */
304
+ async deleteExecutionsOlderThan(date) {
305
+ const result = await this.prisma.scriptExecution.deleteMany({
306
+ where: {
307
+ createdAt: {
308
+ lt: date,
309
+ },
310
+ },
311
+ });
312
+
313
+ return {
314
+ acknowledged: true,
315
+ deletedCount: result.count,
316
+ };
317
+ }
318
+ }
319
+
320
+ module.exports = { ScriptExecutionRepositoryPostgres };
@@ -0,0 +1,21 @@
1
+ const {
2
+ ScriptScheduleRepositoryMongo,
3
+ } = require('./script-schedule-repository-mongo');
4
+
5
+ /**
6
+ * DocumentDB Script Schedule Repository Adapter
7
+ * Handles script schedule persistence using Prisma with AWS DocumentDB
8
+ *
9
+ * DocumentDB is MongoDB-compatible with some limitations:
10
+ * - Uses MongoDB wire protocol
11
+ * - Same Prisma schema as MongoDB
12
+ * - Inherits all MongoDB repository methods
13
+ *
14
+ * For schedule operations, DocumentDB and MongoDB behavior is identical.
15
+ */
16
+ class ScriptScheduleRepositoryDocumentDB extends ScriptScheduleRepositoryMongo {
17
+ // Inherits all methods from MongoDB implementation
18
+ // DocumentDB is MongoDB-compatible for these operations
19
+ }
20
+
21
+ module.exports = { ScriptScheduleRepositoryDocumentDB };
@@ -0,0 +1,49 @@
1
+ const { ScriptScheduleRepositoryPostgres } = require('./script-schedule-repository-postgres');
2
+ const config = require('../../database/config');
3
+
4
+ /**
5
+ * Script Schedule Repository Factory
6
+ * Creates the appropriate repository adapter based on database type
7
+ *
8
+ * This implements the Factory pattern for Hexagonal Architecture:
9
+ * - Reads database type from app definition (backend/index.js)
10
+ * - Returns correct adapter (MongoDB, DocumentDB, or PostgreSQL)
11
+ * - Provides clear error for unsupported databases
12
+ *
13
+ * Usage:
14
+ * ```javascript
15
+ * const repository = createScriptScheduleRepository();
16
+ * ```
17
+ *
18
+ * @returns {ScriptScheduleRepositoryInterface} Configured repository adapter
19
+ * @throws {Error} If database type is not supported
20
+ */
21
+ function createScriptScheduleRepository() {
22
+ const dbType = config.DB_TYPE;
23
+
24
+ switch (dbType) {
25
+ case 'mongodb':
26
+ const { ScriptScheduleRepositoryMongo } = require('./script-schedule-repository-mongo');
27
+ return new ScriptScheduleRepositoryMongo();
28
+
29
+ case 'postgresql':
30
+ return new ScriptScheduleRepositoryPostgres();
31
+
32
+ case 'documentdb':
33
+ const { ScriptScheduleRepositoryDocumentDB } = require('./script-schedule-repository-documentdb');
34
+ return new ScriptScheduleRepositoryDocumentDB();
35
+
36
+ default:
37
+ throw new Error(
38
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
39
+ );
40
+ }
41
+ }
42
+
43
+ module.exports = {
44
+ createScriptScheduleRepository,
45
+ // Export adapters for direct testing
46
+ get ScriptScheduleRepositoryMongo() { return require('./script-schedule-repository-mongo').ScriptScheduleRepositoryMongo; },
47
+ ScriptScheduleRepositoryPostgres,
48
+ get ScriptScheduleRepositoryDocumentDB() { return require('./script-schedule-repository-documentdb').ScriptScheduleRepositoryDocumentDB; },
49
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Script Schedule Repository Interface
3
+ * Abstract base class defining the contract for script schedule persistence adapters
4
+ *
5
+ * This follows the Port in Hexagonal Architecture:
6
+ * - Domain layer depends on this abstraction
7
+ * - Concrete adapters implement this interface
8
+ * - Use cases receive repositories via dependency injection
9
+ *
10
+ * Script schedules support Phase 2 hybrid scheduling:
11
+ * - Database overrides take precedence over Definition defaults
12
+ * - EventBridge rules provisioned for enabled schedules
13
+ * - lastTriggeredAt and nextTriggerAt for monitoring
14
+ *
15
+ * @abstract
16
+ */
17
+ class ScriptScheduleRepositoryInterface {
18
+ /**
19
+ * Find a schedule by script name
20
+ *
21
+ * @param {string} scriptName - The script name
22
+ * @returns {Promise<Object|null>} Schedule record or null if not found
23
+ * @abstract
24
+ */
25
+ async findScheduleByScriptName(scriptName) {
26
+ throw new Error('Method findScheduleByScriptName must be implemented by subclass');
27
+ }
28
+
29
+ /**
30
+ * Create or update a schedule (upsert)
31
+ *
32
+ * @param {Object} params - Schedule parameters
33
+ * @param {string} params.scriptName - Name of the script
34
+ * @param {boolean} params.enabled - Whether schedule is enabled
35
+ * @param {string} params.cronExpression - Cron expression
36
+ * @param {string} [params.timezone] - Timezone (default 'UTC')
37
+ * @param {string} [params.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
38
+ * @param {string} [params.externalScheduleName] - External scheduler name
39
+ * @returns {Promise<Object>} Created or updated schedule record
40
+ * @abstract
41
+ */
42
+ async upsertSchedule({ scriptName, enabled, cronExpression, timezone, externalScheduleId, externalScheduleName }) {
43
+ throw new Error('Method upsertSchedule must be implemented by subclass');
44
+ }
45
+
46
+ /**
47
+ * Delete a schedule by script name
48
+ *
49
+ * @param {string} scriptName - The script name
50
+ * @returns {Promise<Object>} Deletion result
51
+ * @abstract
52
+ */
53
+ async deleteSchedule(scriptName) {
54
+ throw new Error('Method deleteSchedule must be implemented by subclass');
55
+ }
56
+
57
+ /**
58
+ * Update external scheduler information
59
+ *
60
+ * @param {string} scriptName - The script name
61
+ * @param {Object} externalInfo - External schedule information
62
+ * @param {string} [externalInfo.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
63
+ * @param {string} [externalInfo.externalScheduleName] - External scheduler name
64
+ * @returns {Promise<Object>} Updated schedule record
65
+ * @abstract
66
+ */
67
+ async updateScheduleExternalInfo(scriptName, { externalScheduleId, externalScheduleName }) {
68
+ throw new Error('Method updateScheduleExternalInfo must be implemented by subclass');
69
+ }
70
+
71
+ /**
72
+ * Update last triggered timestamp
73
+ *
74
+ * @param {string} scriptName - The script name
75
+ * @param {Date} [timestamp] - Trigger timestamp (default: now)
76
+ * @returns {Promise<Object>} Updated schedule record
77
+ * @abstract
78
+ */
79
+ async updateScheduleLastTriggered(scriptName, timestamp) {
80
+ throw new Error('Method updateScheduleLastTriggered must be implemented by subclass');
81
+ }
82
+
83
+ /**
84
+ * Update next trigger timestamp
85
+ *
86
+ * @param {string} scriptName - The script name
87
+ * @param {Date} timestamp - Next trigger timestamp
88
+ * @returns {Promise<Object>} Updated schedule record
89
+ * @abstract
90
+ */
91
+ async updateScheduleNextTrigger(scriptName, timestamp) {
92
+ throw new Error('Method updateScheduleNextTrigger must be implemented by subclass');
93
+ }
94
+
95
+ /**
96
+ * List all schedules
97
+ *
98
+ * @param {Object} [options] - Query options
99
+ * @param {boolean} [options.enabledOnly] - Only return enabled schedules
100
+ * @returns {Promise<Array>} Array of schedule records
101
+ * @abstract
102
+ */
103
+ async listSchedules(options = {}) {
104
+ throw new Error('Method listSchedules must be implemented by subclass');
105
+ }
106
+ }
107
+
108
+ module.exports = { ScriptScheduleRepositoryInterface };
@@ -0,0 +1,179 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ScriptScheduleRepositoryInterface,
4
+ } = require('./script-schedule-repository-interface');
5
+
6
+ /**
7
+ * MongoDB Script Schedule Repository Adapter
8
+ * Handles script schedule persistence using Prisma with MongoDB
9
+ *
10
+ * MongoDB-specific characteristics:
11
+ * - IDs are strings with @db.ObjectId
12
+ * - scriptName has unique index
13
+ * - Supports upsert operations natively
14
+ */
15
+ class ScriptScheduleRepositoryMongo extends ScriptScheduleRepositoryInterface {
16
+ constructor() {
17
+ super();
18
+ this.prisma = prisma;
19
+ }
20
+
21
+ /**
22
+ * Find a schedule by script name
23
+ *
24
+ * @param {string} scriptName - The script name
25
+ * @returns {Promise<Object|null>} Schedule record or null if not found
26
+ */
27
+ async findScheduleByScriptName(scriptName) {
28
+ const schedule = await this.prisma.scriptSchedule.findUnique({
29
+ where: { scriptName },
30
+ });
31
+
32
+ return schedule;
33
+ }
34
+
35
+ /**
36
+ * Create or update a schedule (upsert)
37
+ *
38
+ * @param {Object} params - Schedule parameters
39
+ * @param {string} params.scriptName - Name of the script
40
+ * @param {boolean} params.enabled - Whether schedule is enabled
41
+ * @param {string} params.cronExpression - Cron expression
42
+ * @param {string} [params.timezone] - Timezone (default 'UTC')
43
+ * @param {string} [params.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
44
+ * @param {string} [params.externalScheduleName] - External scheduler name
45
+ * @returns {Promise<Object>} Created or updated schedule record
46
+ */
47
+ async upsertSchedule({ scriptName, enabled, cronExpression, timezone, externalScheduleId, externalScheduleName }) {
48
+ const data = {
49
+ enabled,
50
+ cronExpression,
51
+ timezone: timezone || 'UTC',
52
+ };
53
+
54
+ // Only set external scheduler fields if provided
55
+ if (externalScheduleId !== undefined) data.externalScheduleId = externalScheduleId;
56
+ if (externalScheduleName !== undefined) data.externalScheduleName = externalScheduleName;
57
+
58
+ const schedule = await this.prisma.scriptSchedule.upsert({
59
+ where: { scriptName },
60
+ update: data,
61
+ create: {
62
+ scriptName,
63
+ ...data,
64
+ },
65
+ });
66
+
67
+ return schedule;
68
+ }
69
+
70
+ /**
71
+ * Delete a schedule by script name
72
+ *
73
+ * @param {string} scriptName - The script name
74
+ * @returns {Promise<Object>} Deletion result
75
+ */
76
+ async deleteSchedule(scriptName) {
77
+ try {
78
+ const schedule = await this.prisma.scriptSchedule.delete({
79
+ where: { scriptName },
80
+ });
81
+
82
+ return {
83
+ acknowledged: true,
84
+ deletedCount: 1,
85
+ deleted: schedule,
86
+ };
87
+ } catch (error) {
88
+ // Return 0 count if not found
89
+ if (error.code === 'P2025') {
90
+ return {
91
+ acknowledged: true,
92
+ deletedCount: 0,
93
+ };
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Update external scheduler information
101
+ *
102
+ * @param {string} scriptName - The script name
103
+ * @param {Object} externalInfo - External schedule information
104
+ * @param {string} [externalInfo.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
105
+ * @param {string} [externalInfo.externalScheduleName] - External scheduler name
106
+ * @returns {Promise<Object>} Updated schedule record
107
+ */
108
+ async updateScheduleExternalInfo(scriptName, { externalScheduleId, externalScheduleName }) {
109
+ const data = {};
110
+ if (externalScheduleId !== undefined) data.externalScheduleId = externalScheduleId;
111
+ if (externalScheduleName !== undefined) data.externalScheduleName = externalScheduleName;
112
+
113
+ const schedule = await this.prisma.scriptSchedule.update({
114
+ where: { scriptName },
115
+ data,
116
+ });
117
+
118
+ return schedule;
119
+ }
120
+
121
+ /**
122
+ * Update last triggered timestamp
123
+ *
124
+ * @param {string} scriptName - The script name
125
+ * @param {Date} [timestamp] - Trigger timestamp (default: now)
126
+ * @returns {Promise<Object>} Updated schedule record
127
+ */
128
+ async updateScheduleLastTriggered(scriptName, timestamp) {
129
+ const schedule = await this.prisma.scriptSchedule.update({
130
+ where: { scriptName },
131
+ data: {
132
+ lastTriggeredAt: timestamp || new Date(),
133
+ },
134
+ });
135
+
136
+ return schedule;
137
+ }
138
+
139
+ /**
140
+ * Update next trigger timestamp
141
+ *
142
+ * @param {string} scriptName - The script name
143
+ * @param {Date} timestamp - Next trigger timestamp
144
+ * @returns {Promise<Object>} Updated schedule record
145
+ */
146
+ async updateScheduleNextTrigger(scriptName, timestamp) {
147
+ const schedule = await this.prisma.scriptSchedule.update({
148
+ where: { scriptName },
149
+ data: {
150
+ nextTriggerAt: timestamp,
151
+ },
152
+ });
153
+
154
+ return schedule;
155
+ }
156
+
157
+ /**
158
+ * List all schedules
159
+ *
160
+ * @param {Object} [options] - Query options
161
+ * @param {boolean} [options.enabledOnly] - Only return enabled schedules
162
+ * @returns {Promise<Array>} Array of schedule records
163
+ */
164
+ async listSchedules(options = {}) {
165
+ const where = {};
166
+ if (options.enabledOnly) {
167
+ where.enabled = true;
168
+ }
169
+
170
+ const schedules = await this.prisma.scriptSchedule.findMany({
171
+ where,
172
+ orderBy: { scriptName: 'asc' },
173
+ });
174
+
175
+ return schedules;
176
+ }
177
+ }
178
+
179
+ module.exports = { ScriptScheduleRepositoryMongo };