@friggframework/core 2.0.0-next.92 → 2.0.0-next.93

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.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Process Commands
3
+ *
4
+ * Application Layer - Command factory for long-running process tracking.
5
+ *
6
+ * Wraps the Process use cases (create, get, update state, update metrics)
7
+ * behind the same `createXCommands()` surface used by the other domains so
8
+ * integration developers can track processes without touching repositories
9
+ * or the underlying ORM directly.
10
+ *
11
+ * @example
12
+ * const processCommands = createProcessCommands();
13
+ * const process = await processCommands.createProcess({
14
+ * userId: 'user-1',
15
+ * integrationId: 'integration-1',
16
+ * name: 'zoho-crm-contact-sync',
17
+ * type: 'CRM_SYNC',
18
+ * });
19
+ * await processCommands.updateProcessState(process.id, 'FETCHING_TOTAL');
20
+ * await processCommands.updateProcessMetrics(process.id, { processed: 100, success: 100 });
21
+ */
22
+
23
+ const {
24
+ createProcessRepository,
25
+ } = require('../../integrations/repositories/process-repository-factory');
26
+ const { CreateProcess } = require('../../integrations/use-cases/create-process');
27
+ const { GetProcess } = require('../../integrations/use-cases/get-process');
28
+ const {
29
+ UpdateProcessState,
30
+ } = require('../../integrations/use-cases/update-process-state');
31
+ const {
32
+ UpdateProcessMetrics,
33
+ } = require('../../integrations/use-cases/update-process-metrics');
34
+
35
+ const ERROR_CODE_MAP = {
36
+ PROCESS_NOT_FOUND: 404,
37
+ INVALID_PROCESS_DATA: 400,
38
+ };
39
+
40
+ function mapErrorToResponse(error) {
41
+ const status = ERROR_CODE_MAP[error?.code] || 500;
42
+ return {
43
+ error: status,
44
+ reason: error?.message,
45
+ code: error?.code,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Create process tracking commands.
51
+ *
52
+ * @param {Object} [params]
53
+ * @param {Object} [params.websocketService] - Optional WebSocket service
54
+ * forwarded to UpdateProcessMetrics for progress broadcasting.
55
+ * @returns {Object} Process commands object
56
+ */
57
+ function createProcessCommands({ websocketService } = {}) {
58
+ const processRepository = createProcessRepository();
59
+
60
+ const createProcessUseCase = new CreateProcess({ processRepository });
61
+ const getProcessUseCase = new GetProcess({ processRepository });
62
+ const updateProcessStateUseCase = new UpdateProcessState({
63
+ processRepository,
64
+ });
65
+ const updateProcessMetricsUseCase = new UpdateProcessMetrics({
66
+ processRepository,
67
+ websocketService,
68
+ });
69
+
70
+ return {
71
+ /**
72
+ * Create a new process record.
73
+ * @param {Object} processData - Process data (userId, integrationId, name, type, ...)
74
+ * @returns {Promise<Object>} Created process record
75
+ */
76
+ async createProcess(processData) {
77
+ try {
78
+ return await createProcessUseCase.execute(processData);
79
+ } catch (error) {
80
+ return mapErrorToResponse(error);
81
+ }
82
+ },
83
+
84
+ /**
85
+ * Retrieve a process by ID.
86
+ * @param {string} processId - Process ID
87
+ * @returns {Promise<Object|null>} Process record, or null if not found
88
+ */
89
+ async getProcess(processId) {
90
+ try {
91
+ return await getProcessUseCase.execute(processId);
92
+ } catch (error) {
93
+ return mapErrorToResponse(error);
94
+ }
95
+ },
96
+
97
+ /**
98
+ * Transition a process to a new state, merging optional context updates.
99
+ * @param {string} processId - Process ID
100
+ * @param {string} newState - New state value
101
+ * @param {Object} [contextUpdates={}] - Context fields to merge
102
+ * @returns {Promise<Object>} Updated process record
103
+ */
104
+ async updateProcessState(processId, newState, contextUpdates = {}) {
105
+ try {
106
+ return await updateProcessStateUseCase.execute(
107
+ processId,
108
+ newState,
109
+ contextUpdates
110
+ );
111
+ } catch (error) {
112
+ return mapErrorToResponse(error);
113
+ }
114
+ },
115
+
116
+ /**
117
+ * Apply a metrics update (counters + bounded error history) to a process.
118
+ * @param {string} processId - Process ID
119
+ * @param {Object} metricsUpdate - Metrics to add (processed, success, errors, skipped, errorDetails)
120
+ * @returns {Promise<Object>} Updated process record
121
+ */
122
+ async updateProcessMetrics(processId, metricsUpdate) {
123
+ try {
124
+ return await updateProcessMetricsUseCase.execute(
125
+ processId,
126
+ metricsUpdate
127
+ );
128
+ } catch (error) {
129
+ return mapErrorToResponse(error);
130
+ }
131
+ },
132
+ };
133
+ }
134
+
135
+ module.exports = { createProcessCommands };
@@ -7,6 +7,9 @@ const { createEntityCommands } = require('./commands/entity-commands');
7
7
  const {
8
8
  createCredentialCommands,
9
9
  } = require('./commands/credential-commands');
10
+ const {
11
+ createProcessCommands,
12
+ } = require('./commands/process-commands');
10
13
  const {
11
14
  createSchedulerCommands,
12
15
  } = require('./commands/scheduler-commands');
@@ -36,6 +39,8 @@ function createFriggCommands({ integrationClass }) {
36
39
 
37
40
  const credentialCommands = createCredentialCommands();
38
41
 
42
+ const processCommands = createProcessCommands();
43
+
39
44
  return {
40
45
  // Integration commands
41
46
  ...integrationCommands,
@@ -48,6 +53,9 @@ function createFriggCommands({ integrationClass }) {
48
53
 
49
54
  // Credential commands
50
55
  ...credentialCommands,
56
+
57
+ // Process commands
58
+ ...processCommands,
51
59
  };
52
60
  }
53
61
 
@@ -60,6 +68,7 @@ module.exports = {
60
68
  createUserCommands,
61
69
  createEntityCommands,
62
70
  createCredentialCommands,
71
+ createProcessCommands,
63
72
  createSchedulerCommands,
64
73
 
65
74
  // Legacy standalone function
package/index.js CHANGED
@@ -146,6 +146,7 @@ module.exports = {
146
146
  createUserCommands: application.createUserCommands,
147
147
  createEntityCommands: application.createEntityCommands,
148
148
  createCredentialCommands: application.createCredentialCommands,
149
+ createProcessCommands: application.createProcessCommands,
149
150
  createSchedulerCommands: application.createSchedulerCommands,
150
151
  findIntegrationContextByExternalEntityId:
151
152
  application.findIntegrationContextByExternalEntityId,
@@ -22,6 +22,8 @@
22
22
  * results: { aggregateData: { totalSynced: 0, totalFailed: 0 } }
23
23
  * });
24
24
  */
25
+ const { invalidProcessData } = require('./process-errors');
26
+
25
27
  class CreateProcess {
26
28
  /**
27
29
  * @param {Object} params
@@ -86,40 +88,40 @@ class CreateProcess {
86
88
  const missingFields = requiredFields.filter(field => !processData[field]);
87
89
 
88
90
  if (missingFields.length > 0) {
89
- throw new Error(
91
+ throw invalidProcessData(
90
92
  `Missing required fields for process creation: ${missingFields.join(', ')}`
91
93
  );
92
94
  }
93
95
 
94
96
  // Validate field types
95
97
  if (typeof processData.userId !== 'string') {
96
- throw new Error('userId must be a string');
98
+ throw invalidProcessData('userId must be a string');
97
99
  }
98
100
  if (typeof processData.integrationId !== 'string') {
99
- throw new Error('integrationId must be a string');
101
+ throw invalidProcessData('integrationId must be a string');
100
102
  }
101
103
  if (typeof processData.name !== 'string') {
102
- throw new Error('name must be a string');
104
+ throw invalidProcessData('name must be a string');
103
105
  }
104
106
  if (typeof processData.type !== 'string') {
105
- throw new Error('type must be a string');
107
+ throw invalidProcessData('type must be a string');
106
108
  }
107
109
 
108
110
  // Validate optional fields if provided
109
111
  if (processData.state && typeof processData.state !== 'string') {
110
- throw new Error('state must be a string');
112
+ throw invalidProcessData('state must be a string');
111
113
  }
112
114
  if (processData.context && typeof processData.context !== 'object') {
113
- throw new Error('context must be an object');
115
+ throw invalidProcessData('context must be an object');
114
116
  }
115
117
  if (processData.results && typeof processData.results !== 'object') {
116
- throw new Error('results must be an object');
118
+ throw invalidProcessData('results must be an object');
117
119
  }
118
120
  if (processData.childProcesses && !Array.isArray(processData.childProcesses)) {
119
- throw new Error('childProcesses must be an array');
121
+ throw invalidProcessData('childProcesses must be an array');
120
122
  }
121
123
  if (processData.parentProcessId && typeof processData.parentProcessId !== 'string') {
122
- throw new Error('parentProcessId must be a string');
124
+ throw invalidProcessData('parentProcessId must be a string');
123
125
  }
124
126
  }
125
127
  }
@@ -15,6 +15,8 @@
15
15
  * // or
16
16
  * const process = await getProcess.executeOrThrow(processId);
17
17
  */
18
+ const { invalidProcessData, processNotFound } = require('./process-errors');
19
+
18
20
  class GetProcess {
19
21
  /**
20
22
  * @param {Object} params
@@ -36,7 +38,7 @@ class GetProcess {
36
38
  async execute(processId) {
37
39
  // Validate input
38
40
  if (!processId || typeof processId !== 'string') {
39
- throw new Error('processId must be a non-empty string');
41
+ throw invalidProcessData('processId must be a non-empty string');
40
42
  }
41
43
 
42
44
  // Delegate to repository
@@ -58,7 +60,7 @@ class GetProcess {
58
60
  const process = await this.execute(processId);
59
61
 
60
62
  if (!process) {
61
- throw new Error(`Process not found: ${processId}`);
63
+ throw processNotFound(`Process not found: ${processId}`);
62
64
  }
63
65
 
64
66
  return process;
@@ -71,7 +73,7 @@ class GetProcess {
71
73
  */
72
74
  async executeMany(processIds) {
73
75
  if (!Array.isArray(processIds)) {
74
- throw new Error('processIds must be an array');
76
+ throw invalidProcessData('processIds must be an array');
75
77
  }
76
78
 
77
79
  const processes = await Promise.all(
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Process Error Helpers
3
+ *
4
+ * Constructs Errors carrying a stable `code` so the application layer
5
+ * (`createProcessCommands` → `mapErrorToResponse`) can translate domain
6
+ * failures into HTTP statuses:
7
+ * - INVALID_PROCESS_DATA → 400 (caller-supplied data is invalid)
8
+ * - PROCESS_NOT_FOUND → 404 (the referenced process does not exist)
9
+ *
10
+ * Repository/infrastructure failures are intentionally left uncoded so
11
+ * they surface as 500s.
12
+ */
13
+
14
+ function createCodedError(message, code) {
15
+ const error = new Error(message);
16
+ error.code = code;
17
+ return error;
18
+ }
19
+
20
+ function invalidProcessData(message) {
21
+ return createCodedError(message, 'INVALID_PROCESS_DATA');
22
+ }
23
+
24
+ function processNotFound(message) {
25
+ return createCodedError(message, 'PROCESS_NOT_FOUND');
26
+ }
27
+
28
+ module.exports = { invalidProcessData, processNotFound };
@@ -34,6 +34,8 @@
34
34
  * errorDetails: [{ contactId: 'abc', error: 'Missing email', timestamp: '...' }]
35
35
  * });
36
36
  */
37
+ const { invalidProcessData, processNotFound } = require('./process-errors');
38
+
37
39
  class UpdateProcessMetrics {
38
40
  /**
39
41
  * @param {Object} params
@@ -66,10 +68,10 @@ class UpdateProcessMetrics {
66
68
  */
67
69
  async execute(processId, metricsUpdate) {
68
70
  if (!processId || typeof processId !== 'string') {
69
- throw new Error('processId must be a non-empty string');
71
+ throw invalidProcessData('processId must be a non-empty string');
70
72
  }
71
73
  if (!metricsUpdate || typeof metricsUpdate !== 'object') {
72
- throw new Error('metricsUpdate must be an object');
74
+ throw invalidProcessData('metricsUpdate must be an object');
73
75
  }
74
76
 
75
77
  // Phase 1: atomic increments + bounded error history.
@@ -119,7 +121,7 @@ class UpdateProcessMetrics {
119
121
  }
120
122
 
121
123
  if (!updatedProcess) {
122
- throw new Error(`Process not found: ${processId}`);
124
+ throw processNotFound(`Process not found: ${processId}`);
123
125
  }
124
126
 
125
127
  // Phase 2: derived metrics (non-atomic, best-effort). Preserved
@@ -22,6 +22,8 @@
22
22
  * pagination: { pageSize: 100 }
23
23
  * });
24
24
  */
25
+ const { invalidProcessData, processNotFound } = require('./process-errors');
26
+
25
27
  class UpdateProcessState {
26
28
  /**
27
29
  * @param {Object} params
@@ -45,13 +47,13 @@ class UpdateProcessState {
45
47
  async execute(processId, newState, contextUpdates = {}) {
46
48
  // Validate inputs
47
49
  if (!processId || typeof processId !== 'string') {
48
- throw new Error('processId must be a non-empty string');
50
+ throw invalidProcessData('processId must be a non-empty string');
49
51
  }
50
52
  if (!newState || typeof newState !== 'string') {
51
- throw new Error('newState must be a non-empty string');
53
+ throw invalidProcessData('newState must be a non-empty string');
52
54
  }
53
55
  if (contextUpdates && typeof contextUpdates !== 'object') {
54
- throw new Error('contextUpdates must be an object');
56
+ throw invalidProcessData('contextUpdates must be an object');
55
57
  }
56
58
 
57
59
  // Route through the atomic path when the repo supports it AND we
@@ -82,10 +84,13 @@ class UpdateProcessState {
82
84
  { set, newState }
83
85
  );
84
86
  if (!updated) {
85
- throw new Error(`Process not found: ${processId}`);
87
+ throw processNotFound(`Process not found: ${processId}`);
86
88
  }
87
89
  return updated;
88
90
  } catch (error) {
91
+ if (error.code === 'PROCESS_NOT_FOUND') {
92
+ throw error;
93
+ }
89
94
  throw new Error(
90
95
  `Failed to update process state: ${error.message}`
91
96
  );
@@ -100,7 +105,7 @@ class UpdateProcessState {
100
105
  try {
101
106
  const process = await this.processRepository.findById(processId);
102
107
  if (!process) {
103
- throw new Error(`Process not found: ${processId}`);
108
+ throw processNotFound(`Process not found: ${processId}`);
104
109
  }
105
110
 
106
111
  const updates = { state: newState };
@@ -114,7 +119,7 @@ class UpdateProcessState {
114
119
  return await this.processRepository.update(processId, updates);
115
120
  } catch (error) {
116
121
  // Re-throw "Process not found" as-is; wrap other errors.
117
- if (error.message && error.message.startsWith('Process not found')) {
122
+ if (error.code === 'PROCESS_NOT_FOUND') {
118
123
  throw error;
119
124
  }
120
125
  throw new Error(`Failed to update process state: ${error.message}`);
@@ -140,7 +145,7 @@ class UpdateProcessState {
140
145
  async updateContextOnly(processId, contextUpdates) {
141
146
  const process = await this.processRepository.findById(processId);
142
147
  if (!process) {
143
- throw new Error(`Process not found: ${processId}`);
148
+ throw processNotFound(`Process not found: ${processId}`);
144
149
  }
145
150
 
146
151
  const updates = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.92",
4
+ "version": "2.0.0-next.93",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0-next.92",
42
- "@friggframework/prettier-config": "2.0.0-next.92",
43
- "@friggframework/test": "2.0.0-next.92",
41
+ "@friggframework/eslint-config": "2.0.0-next.93",
42
+ "@friggframework/prettier-config": "2.0.0-next.93",
43
+ "@friggframework/test": "2.0.0-next.93",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "25cf5cf934fa2b7e16a32b49dad50edc2b557136"
83
+ "gitHead": "abfd168927bbd6f1620ccbeb0a61225e19ce1a28"
84
84
  }