Skip to content

Commit 1437e7b

Browse files
committed
feat: add command bulk-import run
1 parent aed7d36 commit 1437e7b

File tree

3 files changed

+144
-7
lines changed

3 files changed

+144
-7
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/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. 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')

0 commit comments

Comments
 (0)