Skip to content

Commit df5fa73

Browse files
authored
Merge pull request #13 from OpenPathfinder/feat/run-bulk-import
2 parents e0f8f2b + 4358a6b commit df5fa73

File tree

6 files changed

+173
-13
lines changed

6 files changed

+173
-13
lines changed

src/__tests__/cli-commands.test.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-env jest */
22

3-
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow, printBulkImportOperations } from '../cli-commands.js'
3+
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow, printBulkImportOperations, executeBulkImportOperation } from '../cli-commands.js'
44
import { getPackageJson } from '../utils.js'
5-
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem, APIBulkImportOperationItem } from '../types.js'
6-
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse, mockAPIWorkflowRunResponse, mockAPIBulkImportOperationResponse } from './fixtures.js'
5+
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIErrorResponse, APIChecklistItem, APICheckItem, APIWorkflowItem, APIOperationCompleted, APIBulkImportOperationItem } from '../types.js'
6+
import { mockApiHealthResponse, mockAPIProjectResponse, mockAPIGithubOrgResponse, mockAPIChecklistResponse, mockAPICheckResponse, mockAPIWorkflowResponse, mockAPIWorkflowRunResponse, mockAPIBulkImportOperationResponse, mockAPIBulkImportOperationRunResponse } from './fixtures.js'
77
import nock from 'nock'
88

99
const pkg = getPackageJson()
@@ -506,7 +506,7 @@ describe('CLI Commands', () => {
506506
})
507507

508508
describe('executeWorkflow', () => {
509-
let workflowRunResponse: APIWorkflowRunItem
509+
let workflowRunResponse: APIOperationCompleted
510510

511511
beforeEach(() => {
512512
nock.cleanAll()
@@ -643,4 +643,63 @@ describe('CLI Commands', () => {
643643
expect(result.messages).toHaveLength(1)
644644
})
645645
})
646+
describe('executeBulkImportOperation', () => {
647+
let mockBulkImportOperationResponse: APIOperationCompleted
648+
649+
beforeEach(() => {
650+
nock.cleanAll()
651+
mockBulkImportOperationResponse = mockAPIBulkImportOperationRunResponse
652+
})
653+
654+
it('should execute a bulk import operation successfully', async () => {
655+
// Mock API call
656+
nock('http://localhost:3000')
657+
.post('/api/v1/bulk-import')
658+
.reply(200, mockBulkImportOperationResponse)
659+
660+
// Execute the function
661+
const result = await executeBulkImportOperation('load-manual-checks', [{ type: 'annualDependencyRefresh', project_id: 1, is_subscribed: true }])
662+
663+
// Verify the result
664+
expect(result.success).toBe(true)
665+
expect(result.messages).toHaveLength(5) // 5 messages with details
666+
expect(result.messages[0]).toContain('Bulk import operation executed successfully in 2.50 seconds')
667+
expect(result.messages[1]).toContain('Status: completed')
668+
expect(result.messages[2]).toContain('Started:')
669+
expect(result.messages[3]).toContain('Finished:')
670+
expect(result.messages[4]).toContain('Result:')
671+
expect(nock.isDone()).toBe(true) // Verify all mocked endpoints were called
672+
})
673+
674+
it('should handle API errors gracefully', async () => {
675+
// Mock API error
676+
nock('http://localhost:3000')
677+
.post('/api/v1/bulk-import')
678+
.reply(404, { errors: [{ message: 'Bulk import operation not found' }] } as APIErrorResponse)
679+
680+
// Execute the function
681+
const result = await executeBulkImportOperation('load-manual-checks', [{ type: 'annualDependencyRefresh', project_id: 1, is_subscribed: true }])
682+
683+
// Verify the result
684+
expect(result.success).toBe(false)
685+
expect(result.messages[0]).toContain('❌ Failed to execute the bulk import operation')
686+
expect(result.messages).toHaveLength(1)
687+
})
688+
689+
it('should handle network errors gracefully', async () => {
690+
// Mock network error
691+
nock('http://localhost:3000')
692+
.post('/api/v1/bulk-import')
693+
.replyWithError('Network error')
694+
695+
// Execute the function
696+
const result = await executeBulkImportOperation('load-manual-checks', [{ type: 'annualDependencyRefresh', project_id: 1, is_subscribed: true }])
697+
698+
// Verify the result
699+
expect(result.success).toBe(false)
700+
expect(result.messages[0]).toContain('❌ Failed to execute the bulk import operation')
701+
expect(result.messages[0]).toContain('Network error')
702+
expect(result.messages).toHaveLength(1)
703+
})
704+
})
646705
})

src/__tests__/fixtures.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem, APIBulkImportOperationItem } from '../types.js'
1+
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIOperationCompleted, APIBulkImportOperationItem } from '../types.js'
22

33
export const mockApiHealthResponse: APIHealthResponse = {
44
status: 'ok',
@@ -131,7 +131,7 @@ export const mockAPIWorkflowResponse: APIWorkflowItem[] = [{
131131
schema: null
132132
}]
133133

134-
export const mockAPIWorkflowRunResponse: APIWorkflowRunItem = {
134+
export const mockAPIWorkflowRunResponse: APIOperationCompleted = {
135135
status: 'completed',
136136
started: '2025-06-21T10:05:00.000Z',
137137
finished: '2025-06-21T10:05:02.500Z',
@@ -144,3 +144,11 @@ export const mockAPIBulkImportOperationResponse: APIBulkImportOperationItem[] =
144144
description: 'Test bulk import operation description',
145145
schema: 'test-bulk-import-operation'
146146
}]
147+
148+
export const mockAPIBulkImportOperationRunResponse: APIOperationCompleted = {
149+
status: 'completed',
150+
started: '2025-06-21T10:05:00.000Z',
151+
finished: '2025-06-21T10:05:02.500Z',
152+
completed: '2025-06-21T10:05:02.500Z',
153+
result: { success: true, message: 'Bulk import operation completed successfully' }
154+
}

src/api-client.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getConfig } from './utils.js'
22
import { got } from 'got'
3-
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIWorkflowRunItem, APIBulkImportOperationItem } from './types.js'
3+
import { APIHealthResponse, APIProjectDetails, APIGithubOrgDetails, APIChecklistItem, APICheckItem, APIWorkflowItem, APIOperationCompleted, APIBulkImportOperationItem } from './types.js'
44

55
export const apiClient = () => {
66
const config = getConfig()
@@ -80,7 +80,7 @@ export const getAllWorkflows = async (): Promise<APIWorkflowItem[]> => {
8080
return response.body as APIWorkflowItem[]
8181
}
8282

83-
export const runWorkflow = async (workflowId: string, data: any): Promise<APIWorkflowRunItem> => {
83+
export const runWorkflow = async (workflowId: string, data: any): Promise<APIOperationCompleted> => {
8484
const client = apiClient()
8585
const payload = data ? { data } : {}
8686
const response = await client.post(`workflow/${workflowId}/execute`, {
@@ -90,7 +90,7 @@ export const runWorkflow = async (workflowId: string, data: any): Promise<APIWor
9090
if (response.statusCode !== 202) {
9191
throw new Error(`Failed to run the workflow: ${response.statusCode} ${response.body}`)
9292
}
93-
return response.body as APIWorkflowRunItem
93+
return response.body as APIOperationCompleted
9494
}
9595

9696
export const getAllBulkImportOperations = async (): Promise<APIBulkImportOperationItem[]> => {
@@ -101,3 +101,18 @@ export const getAllBulkImportOperations = async (): Promise<APIBulkImportOperati
101101
}
102102
return response.body as APIBulkImportOperationItem[]
103103
}
104+
105+
export const runBulkImportOperation = async (id: string, payload: any): Promise<APIOperationCompleted> => {
106+
const client = apiClient()
107+
const response = await client.post('bulk-import', {
108+
json: { id, payload },
109+
responseType: 'json',
110+
throwHttpErrors: false
111+
})
112+
113+
if (response.statusCode < 500 && response.statusCode >= 400) {
114+
throw new Error(`Failed to run the bulk import operation: ${response.statusCode} ${JSON.stringify(response.body, null, 2)}`)
115+
}
116+
117+
return response.body as APIOperationCompleted
118+
}

src/cli-commands.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CommandResult } from './types.js'
22
import { isApiAvailable, isApiCompatible, getPackageJson } from './utils.js'
3-
import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems, getAllChecks, getAllWorkflows, getAllBulkImportOperations, runWorkflow } from './api-client.js'
3+
import { getAPIDetails, createProject, addGithubOrgToProject, getAllChecklistItems, getAllChecks, getAllWorkflows, getAllBulkImportOperations, runWorkflow, runBulkImportOperation } from './api-client.js'
44

55
const pkg = getPackageJson()
66

@@ -192,3 +192,29 @@ export const printBulkImportOperations = async (): Promise<CommandResult> => {
192192
success
193193
}
194194
}
195+
196+
export const executeBulkImportOperation = async (id: string, data: any): Promise<CommandResult> => {
197+
const messages: string[] = []
198+
let success = true
199+
try {
200+
const results = await runBulkImportOperation(id, data)
201+
const startTime = new Date(results.started)
202+
const endTime = new Date(results.finished)
203+
const duration = endTime.getTime() - startTime.getTime()
204+
const durationStr = duration < 1000 ? `${duration} ms` : `${(duration / 1000).toFixed(2)} seconds`
205+
206+
messages.push(`Bulk import operation executed ${results.result.success ? 'successfully' : 'unsuccessfully'} in ${durationStr}`)
207+
messages.push(`- Status: ${results.status}`)
208+
messages.push(`- Started: ${startTime}`)
209+
messages.push(`- Finished: ${endTime}`)
210+
messages.push(`- Result: ${results.result.message}`)
211+
} catch (error) {
212+
messages.push(`❌ Failed to execute the bulk import operation: ${error instanceof Error ? error.message : 'Unknown error'}`)
213+
success = false
214+
}
215+
216+
return {
217+
messages,
218+
success
219+
}
220+
}

src/index.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { Command } from 'commander'
44
// @ts-ignore
55
import { stringToArray } from '@ulisesgascon/string-to-array'
66
import { handleCommandResult, validateData } from './utils.js'
7-
import { getAllWorkflows } from './api-client.js'
7+
import { getAllWorkflows, getAllBulkImportOperations } from './api-client.js'
88
import fs from 'fs'
9-
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow, printBulkImportOperations } from './cli-commands.js'
9+
import { getVersion, runDoctor, addProjectWithGithubOrgs, printChecklists, printChecks, printWorkflows, executeWorkflow, printBulkImportOperations, executeBulkImportOperation } from './cli-commands.js'
1010

1111
const program = new Command()
1212

@@ -37,6 +37,58 @@ bulkImport
3737
handleCommandResult(result)
3838
})
3939

40+
bulkImport
41+
.command('run')
42+
.description('Run a bulk import operation')
43+
.requiredOption('-i, --id <id>', 'Bulk import operation ID')
44+
.option('-d, --data <data>', 'Data to pass to the bulk import operation')
45+
.option('-f, --file <file>', 'File containing the data to be parsed')
46+
.action(async (options) => {
47+
// @TODO: Move to utils and include tests when the backend has one workflow that requires additional data
48+
const operations = await getAllBulkImportOperations()
49+
const operation = operations.find((operation) => operation.id === options.id)
50+
let data: any | undefined
51+
if (!operation) {
52+
throw new Error(`Invalid bulk import operation ID (${options.id}). Available operations: ${operations.map(o => o.id).join(', ')}`)
53+
}
54+
55+
// Check if workflow requires additional data and if it is provided or requires collection
56+
if (!options.data && !options.file) {
57+
throw new Error('Bulk import operation requires additional data. Please provide data using -d or -f option')
58+
} else if (options.data && options.file) {
59+
throw new Error('Please provide either -d or -f, not both')
60+
} else if (options.data) {
61+
try {
62+
data = JSON.parse(options.data)
63+
} catch (error) {
64+
throw new Error(`Failed to parse provided JSON data: ${error instanceof Error ? error.message : 'Unknown error'}`)
65+
}
66+
} else if (options.file) {
67+
try {
68+
const fileContent = fs.readFileSync(options.file, 'utf-8')
69+
data = JSON.parse(fileContent)
70+
} catch (error) {
71+
throw new Error(`Failed to read or parse file: ${error instanceof Error ? error.message : 'Unknown error'}`)
72+
}
73+
}
74+
75+
if (!operation.schema) {
76+
throw new Error('Bulk import operation does not have a JSON schema for data validation. This is an API error')
77+
}
78+
79+
// If data is provided, validate against JSON Schema
80+
if (data && operation.schema) {
81+
const schema = JSON.parse(operation.schema)
82+
const result = await validateData(data, schema)
83+
if (!result.success) {
84+
throw new Error(`Data validation failed: ${result.messages[0]}`)
85+
}
86+
}
87+
88+
const result = await executeBulkImportOperation(options.id, data)
89+
handleCommandResult(result)
90+
})
91+
4092
const workflow = program
4193
.command('workflow')
4294
.description('Compliance workflow management')

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export interface APIWorkflowItem {
175175
/**
176176
* Workflow Run Schema
177177
*/
178-
export interface APIWorkflowRunItem {
178+
export interface APIOperationCompleted {
179179
status: string;
180180
started: string;
181181
finished: string;

0 commit comments

Comments
 (0)