@friggframework/core 2.0.0-next.91 → 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.
- package/application/commands/process-commands.js +135 -0
- package/application/index.js +9 -0
- package/index.js +1 -0
- package/integrations/use-cases/create-process.js +12 -10
- package/integrations/use-cases/get-process.js +5 -3
- package/integrations/use-cases/process-errors.js +28 -0
- package/integrations/use-cases/update-process-metrics.js +5 -3
- package/integrations/use-cases/update-process-state.js +12 -7
- package/package.json +5 -5
|
@@ -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 };
|
package/application/index.js
CHANGED
|
@@ -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
|
|
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
|
|
98
|
+
throw invalidProcessData('userId must be a string');
|
|
97
99
|
}
|
|
98
100
|
if (typeof processData.integrationId !== 'string') {
|
|
99
|
-
throw
|
|
101
|
+
throw invalidProcessData('integrationId must be a string');
|
|
100
102
|
}
|
|
101
103
|
if (typeof processData.name !== 'string') {
|
|
102
|
-
throw
|
|
104
|
+
throw invalidProcessData('name must be a string');
|
|
103
105
|
}
|
|
104
106
|
if (typeof processData.type !== 'string') {
|
|
105
|
-
throw
|
|
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
|
|
112
|
+
throw invalidProcessData('state must be a string');
|
|
111
113
|
}
|
|
112
114
|
if (processData.context && typeof processData.context !== 'object') {
|
|
113
|
-
throw
|
|
115
|
+
throw invalidProcessData('context must be an object');
|
|
114
116
|
}
|
|
115
117
|
if (processData.results && typeof processData.results !== 'object') {
|
|
116
|
-
throw
|
|
118
|
+
throw invalidProcessData('results must be an object');
|
|
117
119
|
}
|
|
118
120
|
if (processData.childProcesses && !Array.isArray(processData.childProcesses)) {
|
|
119
|
-
throw
|
|
121
|
+
throw invalidProcessData('childProcesses must be an array');
|
|
120
122
|
}
|
|
121
123
|
if (processData.parentProcessId && typeof processData.parentProcessId !== 'string') {
|
|
122
|
-
throw
|
|
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
|
|
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
|
|
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
|
|
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
|
|
71
|
+
throw invalidProcessData('processId must be a non-empty string');
|
|
70
72
|
}
|
|
71
73
|
if (!metricsUpdate || typeof metricsUpdate !== 'object') {
|
|
72
|
-
throw
|
|
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
|
|
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
|
|
50
|
+
throw invalidProcessData('processId must be a non-empty string');
|
|
49
51
|
}
|
|
50
52
|
if (!newState || typeof newState !== 'string') {
|
|
51
|
-
throw
|
|
53
|
+
throw invalidProcessData('newState must be a non-empty string');
|
|
52
54
|
}
|
|
53
55
|
if (contextUpdates && typeof contextUpdates !== 'object') {
|
|
54
|
-
throw
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
43
|
-
"@friggframework/test": "2.0.0-next.
|
|
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": "
|
|
83
|
+
"gitHead": "abfd168927bbd6f1620ccbeb0a61225e19ce1a28"
|
|
84
84
|
}
|