@friggframework/core 2.0.0--canary.520.6641121.0 → 2.0.0--canary.517.35ee143.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 (34) hide show
  1. package/admin-scripts/index.js +52 -0
  2. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  3. package/admin-scripts/repositories/admin-process-repository-factory.js +51 -0
  4. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  5. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  6. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  7. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  8. package/admin-scripts/repositories/script-schedule-repository-factory.js +51 -0
  9. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  10. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  11. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  12. package/application/commands/admin-script-commands.js +344 -0
  13. package/database/use-cases/check-database-state-use-case.js +2 -2
  14. package/database/use-cases/trigger-database-migration-use-case.js +1 -1
  15. package/generated/prisma-mongodb/edge.js +47 -4
  16. package/generated/prisma-mongodb/index-browser.js +37 -1
  17. package/generated/prisma-mongodb/index.d.ts +2775 -52
  18. package/generated/prisma-mongodb/index.js +47 -4
  19. package/generated/prisma-mongodb/package.json +1 -1
  20. package/generated/prisma-mongodb/schema.prisma +69 -0
  21. package/generated/prisma-mongodb/wasm.js +47 -4
  22. package/generated/prisma-postgresql/edge.js +46 -4
  23. package/generated/prisma-postgresql/index-browser.js +36 -1
  24. package/generated/prisma-postgresql/index.d.ts +3414 -83
  25. package/generated/prisma-postgresql/index.js +46 -4
  26. package/generated/prisma-postgresql/package.json +1 -1
  27. package/generated/prisma-postgresql/schema.prisma +68 -0
  28. package/generated/prisma-postgresql/wasm.js +46 -4
  29. package/handlers/middleware/admin-auth.js +53 -0
  30. package/handlers/routers/db-migration.handler.js +1 -1
  31. package/handlers/routers/db-migration.js +18 -33
  32. package/package.json +5 -5
  33. package/prisma-mongodb/schema.prisma +69 -0
  34. package/prisma-postgresql/schema.prisma +68 -0
@@ -0,0 +1,210 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ ScriptScheduleRepositoryInterface,
4
+ } = require('./script-schedule-repository-interface');
5
+
6
+ /**
7
+ * PostgreSQL Script Schedule Repository Adapter
8
+ * Handles script schedule 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
+ * - scriptName has unique index
15
+ */
16
+ class ScriptScheduleRepositoryPostgres extends ScriptScheduleRepositoryInterface {
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 schedule object IDs to strings
40
+ * @private
41
+ * @param {Object|null} schedule - Schedule object from database
42
+ * @returns {Object|null} Schedule with string IDs
43
+ */
44
+ _convertScheduleIds(schedule) {
45
+ if (!schedule) return schedule;
46
+ return {
47
+ ...schedule,
48
+ id: schedule.id?.toString(),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Find a schedule by script name
54
+ *
55
+ * @param {string} scriptName - The script name
56
+ * @returns {Promise<Object|null>} Schedule record with string ID or null if not found
57
+ */
58
+ async findScheduleByScriptName(scriptName) {
59
+ const schedule = await this.prisma.scriptSchedule.findUnique({
60
+ where: { scriptName },
61
+ });
62
+
63
+ return this._convertScheduleIds(schedule);
64
+ }
65
+
66
+ /**
67
+ * Create or update a schedule (upsert)
68
+ *
69
+ * @param {Object} params - Schedule parameters
70
+ * @param {string} params.scriptName - Name of the script
71
+ * @param {boolean} params.enabled - Whether schedule is enabled
72
+ * @param {string} params.cronExpression - Cron expression
73
+ * @param {string} [params.timezone] - Timezone (default 'UTC')
74
+ * @param {string} [params.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
75
+ * @param {string} [params.externalScheduleName] - External scheduler name
76
+ * @returns {Promise<Object>} Created or updated schedule record with string ID
77
+ */
78
+ async upsertSchedule({ scriptName, enabled, cronExpression, timezone, externalScheduleId, externalScheduleName }) {
79
+ const data = {
80
+ enabled,
81
+ cronExpression,
82
+ timezone: timezone || 'UTC',
83
+ };
84
+
85
+ // Only set external scheduler fields if provided
86
+ if (externalScheduleId !== undefined) data.externalScheduleId = externalScheduleId;
87
+ if (externalScheduleName !== undefined) data.externalScheduleName = externalScheduleName;
88
+
89
+ const schedule = await this.prisma.scriptSchedule.upsert({
90
+ where: { scriptName },
91
+ update: data,
92
+ create: {
93
+ scriptName,
94
+ ...data,
95
+ },
96
+ });
97
+
98
+ return this._convertScheduleIds(schedule);
99
+ }
100
+
101
+ /**
102
+ * Delete a schedule by script name
103
+ *
104
+ * @param {string} scriptName - The script name
105
+ * @returns {Promise<Object>} Deletion result
106
+ */
107
+ async deleteSchedule(scriptName) {
108
+ try {
109
+ const schedule = await this.prisma.scriptSchedule.delete({
110
+ where: { scriptName },
111
+ });
112
+
113
+ return {
114
+ acknowledged: true,
115
+ deletedCount: 1,
116
+ deleted: this._convertScheduleIds(schedule),
117
+ };
118
+ } catch (error) {
119
+ // Return 0 count if not found
120
+ if (error.code === 'P2025') {
121
+ return {
122
+ acknowledged: true,
123
+ deletedCount: 0,
124
+ };
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Update external scheduler information
132
+ *
133
+ * @param {string} scriptName - The script name
134
+ * @param {Object} externalInfo - External schedule information
135
+ * @param {string} [externalInfo.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
136
+ * @param {string} [externalInfo.externalScheduleName] - External scheduler name
137
+ * @returns {Promise<Object>} Updated schedule record with string ID
138
+ */
139
+ async updateScheduleExternalInfo(scriptName, { externalScheduleId, externalScheduleName }) {
140
+ const data = {};
141
+ if (externalScheduleId !== undefined) data.externalScheduleId = externalScheduleId;
142
+ if (externalScheduleName !== undefined) data.externalScheduleName = externalScheduleName;
143
+
144
+ const schedule = await this.prisma.scriptSchedule.update({
145
+ where: { scriptName },
146
+ data,
147
+ });
148
+
149
+ return this._convertScheduleIds(schedule);
150
+ }
151
+
152
+ /**
153
+ * Update last triggered timestamp
154
+ *
155
+ * @param {string} scriptName - The script name
156
+ * @param {Date} [timestamp] - Trigger timestamp (default: now)
157
+ * @returns {Promise<Object>} Updated schedule record with string ID
158
+ */
159
+ async updateScheduleLastTriggered(scriptName, timestamp) {
160
+ const schedule = await this.prisma.scriptSchedule.update({
161
+ where: { scriptName },
162
+ data: {
163
+ lastTriggeredAt: timestamp || new Date(),
164
+ },
165
+ });
166
+
167
+ return this._convertScheduleIds(schedule);
168
+ }
169
+
170
+ /**
171
+ * Update next trigger timestamp
172
+ *
173
+ * @param {string} scriptName - The script name
174
+ * @param {Date} timestamp - Next trigger timestamp
175
+ * @returns {Promise<Object>} Updated schedule record with string ID
176
+ */
177
+ async updateScheduleNextTrigger(scriptName, timestamp) {
178
+ const schedule = await this.prisma.scriptSchedule.update({
179
+ where: { scriptName },
180
+ data: {
181
+ nextTriggerAt: timestamp,
182
+ },
183
+ });
184
+
185
+ return this._convertScheduleIds(schedule);
186
+ }
187
+
188
+ /**
189
+ * List all schedules
190
+ *
191
+ * @param {Object} [options] - Query options
192
+ * @param {boolean} [options.enabledOnly] - Only return enabled schedules
193
+ * @returns {Promise<Array>} Array of schedule records with string IDs
194
+ */
195
+ async listSchedules(options = {}) {
196
+ const where = {};
197
+ if (options.enabledOnly) {
198
+ where.enabled = true;
199
+ }
200
+
201
+ const schedules = await this.prisma.scriptSchedule.findMany({
202
+ where,
203
+ orderBy: { scriptName: 'asc' },
204
+ });
205
+
206
+ return schedules.map((schedule) => this._convertScheduleIds(schedule));
207
+ }
208
+ }
209
+
210
+ module.exports = { ScriptScheduleRepositoryPostgres };
@@ -0,0 +1,344 @@
1
+ const ERROR_CODE_MAP = {
2
+ SCRIPT_NOT_FOUND: 404,
3
+ EXECUTION_NOT_FOUND: 404,
4
+ };
5
+
6
+ function mapErrorToResponse(error) {
7
+ const status = ERROR_CODE_MAP[error?.code] || 500;
8
+ return { error: status, reason: error?.message, code: error?.code };
9
+ }
10
+
11
+ /**
12
+ * Create admin script commands
13
+ * Provides command pattern API for admin script management
14
+ *
15
+ * This follows the Command pattern from integration-commands.js:
16
+ * - Creates repositories via factory functions
17
+ * - Maps errors to HTTP-friendly responses
18
+ * - Returns data or error objects (never throws)
19
+ *
20
+ * WHY SEPARATE FROM integration-commands.js:
21
+ * These commands are intentionally separate because they serve different domains:
22
+ * - integration-commands: User-context operations on integrations
23
+ * - Requires integrationClass constructor parameter
24
+ * - Works with userId, entityIds, integration contexts
25
+ * - Uses IntegrationRepository, ModuleRepository
26
+ * - admin-script-commands: System/admin operations without user context
27
+ * - No user context required
28
+ * - Works with AdminProcess, ScriptSchedule
29
+ * - Uses AdminProcessRepository, ScriptScheduleRepository
30
+ *
31
+ * Merging them would violate SRP and create coupling between
32
+ * user-facing integration code and admin/system code.
33
+ *
34
+ * Authentication:
35
+ * - Uses ENV-based ADMIN_API_KEY (see handlers/middleware/admin-auth.js)
36
+ * - No database-backed API keys (simplified from original design)
37
+ *
38
+ * @returns {Object} Command methods for admin scripts
39
+ */
40
+ function createAdminScriptCommands() {
41
+ // Lazy-load repository factories to avoid circular dependencies
42
+ const { createAdminProcessRepository } = require('../../admin-scripts/repositories/admin-process-repository-factory');
43
+ const { createScriptScheduleRepository } = require('../../admin-scripts/repositories/script-schedule-repository-factory');
44
+
45
+ const adminProcessRepository = createAdminProcessRepository();
46
+ const scheduleRepository = createScriptScheduleRepository();
47
+
48
+ return {
49
+ // ==================== Admin Process Management Commands ====================
50
+
51
+ /**
52
+ * Create a new admin process record
53
+ *
54
+ * @param {Object} params - Process creation parameters
55
+ * @param {string} params.scriptName - Name of script being executed
56
+ * @param {string} [params.scriptVersion] - Script version
57
+ * @param {string} params.trigger - Trigger type ('MANUAL', 'SCHEDULED', 'QUEUE', 'WEBHOOK')
58
+ * @param {string} [params.mode] - Execution mode ('sync' or 'async', default 'async')
59
+ * @param {Object} [params.input] - Input parameters
60
+ * @param {Object} [params.audit] - Audit information (apiKeyName, apiKeyLast4, ipAddress)
61
+ * @returns {Promise<Object>} Created admin process record
62
+ */
63
+ async createAdminProcess({
64
+ scriptName,
65
+ scriptVersion,
66
+ trigger,
67
+ mode,
68
+ input,
69
+ audit,
70
+ }) {
71
+ try {
72
+ const process = await adminProcessRepository.createAdminProcess({
73
+ scriptName,
74
+ scriptVersion,
75
+ trigger,
76
+ mode: mode || 'async',
77
+ input,
78
+ audit,
79
+ });
80
+ return process;
81
+ } catch (error) {
82
+ return mapErrorToResponse(error);
83
+ }
84
+ },
85
+
86
+ /**
87
+ * Find an admin process by ID
88
+ *
89
+ * @param {string|number} processId - The admin process ID
90
+ * @returns {Promise<Object>} Admin process record or error
91
+ */
92
+ async findAdminProcessById(processId) {
93
+ try {
94
+ const process = await adminProcessRepository.findAdminProcessById(processId);
95
+ if (!process) {
96
+ const error = new Error(`Execution ${processId} not found`);
97
+ error.code = 'EXECUTION_NOT_FOUND';
98
+ return mapErrorToResponse(error);
99
+ }
100
+ return process;
101
+ } catch (error) {
102
+ return mapErrorToResponse(error);
103
+ }
104
+ },
105
+
106
+ /**
107
+ * Find all admin processes for a specific script
108
+ *
109
+ * @param {string} scriptName - Script name to filter by
110
+ * @param {Object} [options] - Query options (limit, offset, sortBy, sortOrder)
111
+ * @returns {Promise<Array>} Array of admin process records
112
+ */
113
+ async findAdminProcessesByName(scriptName, options = {}) {
114
+ try {
115
+ const processes = await adminProcessRepository.findAdminProcessesByName(
116
+ scriptName,
117
+ options
118
+ );
119
+ return processes;
120
+ } catch (error) {
121
+ // Return empty array on error (non-critical)
122
+ return [];
123
+ }
124
+ },
125
+
126
+ /**
127
+ * Update admin process state
128
+ *
129
+ * @param {string|number} processId - The admin process ID
130
+ * @param {string} state - New state ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED')
131
+ * @returns {Promise<Object>} Updated admin process record
132
+ */
133
+ async updateAdminProcessState(processId, state) {
134
+ try {
135
+ const updated = await adminProcessRepository.updateAdminProcessState(
136
+ processId,
137
+ state
138
+ );
139
+ return updated;
140
+ } catch (error) {
141
+ return mapErrorToResponse(error);
142
+ }
143
+ },
144
+
145
+ /**
146
+ * Append a log entry to an admin process's results.logs array
147
+ *
148
+ * @param {string|number} processId - The admin process ID
149
+ * @param {Object} logEntry - Log entry { level, message, data, timestamp }
150
+ * @returns {Promise<Object>} Updated admin process record
151
+ */
152
+ async appendAdminProcessLog(processId, logEntry) {
153
+ try {
154
+ const updated = await adminProcessRepository.appendAdminProcessLog(
155
+ processId,
156
+ logEntry
157
+ );
158
+ return updated;
159
+ } catch (error) {
160
+ return mapErrorToResponse(error);
161
+ }
162
+ },
163
+
164
+ /**
165
+ * Complete an admin process
166
+ * Updates state, output, error, and metrics
167
+ *
168
+ * @param {string|number} processId - The admin process ID
169
+ * @param {Object} params - Completion parameters
170
+ * @param {string} [params.state] - Final state ('COMPLETED', 'FAILED')
171
+ * @param {Object} [params.output] - Script output/result (stored in results.output)
172
+ * @param {Object} [params.error] - Error details { name, message, stack } (stored in results.error)
173
+ * @param {Object} [params.metrics] - Performance metrics { startTime, endTime, durationMs } (stored in results.metrics)
174
+ * @returns {Promise<Object>} { success: true } or error
175
+ */
176
+ async completeAdminProcess(processId, { state, output, error, metrics }) {
177
+ try {
178
+ // Update each field independently (partial updates allowed)
179
+ if (state) {
180
+ await adminProcessRepository.updateAdminProcessState(processId, state);
181
+ }
182
+ if (output !== undefined) {
183
+ await adminProcessRepository.updateAdminProcessOutput(processId, output);
184
+ }
185
+ if (error) {
186
+ await adminProcessRepository.updateAdminProcessError(processId, error);
187
+ }
188
+ if (metrics) {
189
+ await adminProcessRepository.updateAdminProcessMetrics(processId, metrics);
190
+ }
191
+
192
+ return { success: true };
193
+ } catch (err) {
194
+ return mapErrorToResponse(err);
195
+ }
196
+ },
197
+
198
+ /**
199
+ * Find recent admin processes across all scripts
200
+ *
201
+ * @param {Object} [options] - Query options
202
+ * @param {number} [options.limit] - Maximum results (default 20)
203
+ * @param {string} [options.state] - Filter by state
204
+ * @param {Date} [options.since] - Filter by created date
205
+ * @returns {Promise<Array>} Array of recent admin processes
206
+ */
207
+ async findRecentAdminProcesses(options = {}) {
208
+ try {
209
+ const { limit = 20, state, since } = options;
210
+
211
+ // If state filter provided, use state query
212
+ if (state) {
213
+ return await adminProcessRepository.findAdminProcessesByState(state, {
214
+ limit,
215
+ sortBy: 'createdAt',
216
+ sortOrder: 'desc',
217
+ });
218
+ }
219
+
220
+ // Otherwise, use generic recent query (would need to be added to interface)
221
+ // For now, fall back to empty array if no state filter
222
+ return [];
223
+ } catch (error) {
224
+ return [];
225
+ }
226
+ },
227
+
228
+ // ==================== Schedule Management Commands ====================
229
+
230
+ /**
231
+ * Get schedule by script name
232
+ * Returns database override or null
233
+ *
234
+ * @param {string} scriptName - The script name
235
+ * @returns {Promise<Object|null>} Schedule record or null
236
+ */
237
+ async getScheduleByScriptName(scriptName) {
238
+ try {
239
+ const schedule = await scheduleRepository.findScheduleByScriptName(scriptName);
240
+ return schedule;
241
+ } catch (error) {
242
+ return mapErrorToResponse(error);
243
+ }
244
+ },
245
+
246
+ /**
247
+ * Create or update a schedule (upsert)
248
+ *
249
+ * @param {Object} params - Schedule parameters
250
+ * @param {string} params.scriptName - Name of the script
251
+ * @param {boolean} params.enabled - Whether schedule is enabled
252
+ * @param {string} params.cronExpression - Cron expression
253
+ * @param {string} [params.timezone] - Timezone (default 'UTC')
254
+ * @returns {Promise<Object>} Created or updated schedule
255
+ */
256
+ async upsertSchedule({ scriptName, enabled, cronExpression, timezone }) {
257
+ try {
258
+ const schedule = await scheduleRepository.upsertSchedule({
259
+ scriptName,
260
+ enabled,
261
+ cronExpression,
262
+ timezone,
263
+ });
264
+ return schedule;
265
+ } catch (error) {
266
+ return mapErrorToResponse(error);
267
+ }
268
+ },
269
+
270
+ /**
271
+ * Delete a schedule by script name
272
+ *
273
+ * @param {string} scriptName - The script name
274
+ * @returns {Promise<Object>} Deletion result
275
+ */
276
+ async deleteSchedule(scriptName) {
277
+ try {
278
+ const result = await scheduleRepository.deleteSchedule(scriptName);
279
+ return result;
280
+ } catch (error) {
281
+ return mapErrorToResponse(error);
282
+ }
283
+ },
284
+
285
+ /**
286
+ * Update external scheduler information
287
+ *
288
+ * @param {string} scriptName - The script name
289
+ * @param {Object} externalInfo - External schedule information
290
+ * @param {string} [externalInfo.externalScheduleId] - External scheduler ID (e.g., AWS ARN)
291
+ * @param {string} [externalInfo.externalScheduleName] - External scheduler name
292
+ * @returns {Promise<Object>} Updated schedule
293
+ */
294
+ async updateScheduleExternalInfo(scriptName, { externalScheduleId, externalScheduleName }) {
295
+ try {
296
+ const schedule = await scheduleRepository.updateScheduleExternalInfo(scriptName, {
297
+ externalScheduleId,
298
+ externalScheduleName,
299
+ });
300
+ return schedule;
301
+ } catch (error) {
302
+ return mapErrorToResponse(error);
303
+ }
304
+ },
305
+
306
+ /**
307
+ * Update last triggered timestamp
308
+ * Called when a schedule triggers
309
+ *
310
+ * @param {string} scriptName - The script name
311
+ * @param {Date} [timestamp] - Trigger timestamp (default: now)
312
+ * @returns {Promise<Object>} Updated schedule
313
+ */
314
+ async updateScheduleLastTriggered(scriptName, timestamp) {
315
+ try {
316
+ const schedule = await scheduleRepository.updateScheduleLastTriggered(
317
+ scriptName,
318
+ timestamp
319
+ );
320
+ return schedule;
321
+ } catch (error) {
322
+ return mapErrorToResponse(error);
323
+ }
324
+ },
325
+
326
+ /**
327
+ * List all schedules
328
+ *
329
+ * @param {Object} [options] - Query options
330
+ * @param {boolean} [options.enabledOnly] - Only return enabled schedules
331
+ * @returns {Promise<Array>} Array of schedule records
332
+ */
333
+ async listSchedules(options = {}) {
334
+ try {
335
+ const schedules = await scheduleRepository.listSchedules(options);
336
+ return schedules;
337
+ } catch (error) {
338
+ return [];
339
+ }
340
+ },
341
+ };
342
+ }
343
+
344
+ module.exports = { createAdminScriptCommands };
@@ -62,12 +62,12 @@ class CheckDatabaseStateUseCase {
62
62
  // Add error if present
63
63
  if (state.error) {
64
64
  response.error = state.error;
65
- response.recommendation = 'Run POST /db-migrate to initialize database';
65
+ response.recommendation = 'Run POST /admin/db-migrate to initialize database';
66
66
  }
67
67
 
68
68
  // Add recommendation if migrations pending
69
69
  if (!state.upToDate && state.pendingMigrations > 0) {
70
- response.recommendation = `Run POST /db-migrate to apply ${state.pendingMigrations} pending migration(s)`;
70
+ response.recommendation = `Run POST /admin/db-migrate to apply ${state.pendingMigrations} pending migration(s)`;
71
71
  }
72
72
 
73
73
  return response;
@@ -99,7 +99,7 @@ class TriggerDatabaseMigrationUseCase {
99
99
  success: true,
100
100
  migrationId: migrationStatus.migrationId,
101
101
  state: migrationStatus.state,
102
- statusUrl: `/db-migrate/${migrationStatus.migrationId}`,
102
+ statusUrl: `/admin/db-migrate/${migrationStatus.migrationId}`,
103
103
  s3Key: `migrations/${migrationStatus.stage}/${migrationStatus.migrationId}.json`,
104
104
  message: 'Database migration queued successfully',
105
105
  };