Skip to content

Commit 3d0e8e2

Browse files
refactor
1 parent fd1a345 commit 3d0e8e2

File tree

2 files changed

+95
-66
lines changed

2 files changed

+95
-66
lines changed

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts

Lines changed: 87 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -71,76 +71,20 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr
7171
const finalOperation = await renderSingleTask<BulkOperation>({
7272
title: outputContent`Starting bulk operation...`,
7373
task: async (updateStatus) => {
74-
let lastObjectCount = ''
75-
while (true) {
76-
// eslint-disable-next-line no-await-in-loop
77-
const response = await adminRequestDoc<GetBulkOperationByIdQuery, {id: string}>({
78-
query: GetBulkOperationById,
79-
session: adminSession,
80-
variables: {id: result.id},
81-
version: '2026-01',
82-
})
83-
84-
if (!response.bulkOperation) {
85-
throw new Error('bulk operation not found')
86-
}
87-
88-
const op = response.bulkOperation as BulkOperation
89-
90-
if (op.status === 'RUNNING') {
91-
const currentObjectCount = String(op.objectCount)
92-
if (currentObjectCount !== lastObjectCount) {
93-
updateStatus(formatBulkOperationStatus(op))
94-
lastObjectCount = currentObjectCount
95-
}
96-
} else if (op.status === 'CREATED') {
97-
updateStatus(formatBulkOperationStatus(op))
98-
}
99-
100-
if (TERMINAL_STATUSES.includes(op.status)) {
101-
return op
102-
}
74+
const generator = watchBulkOperation(adminSession, result.id)
10375

76+
while (true) {
10477
// eslint-disable-next-line no-await-in-loop
105-
await sleep(POLL_INTERVAL_SECONDS)
78+
const {value: latestOperationState, done} = await generator.next()
79+
if (done) return latestOperationState
80+
updateStatus(formatBulkOperationStatus(latestOperationState))
10681
}
10782
},
10883
})
10984

110-
const url = finalOperation.url ?? ''
111-
112-
renderSuccess({
113-
headline: 'Bulk operation complete!',
114-
body: outputContent`ID: ${outputToken.cyan(finalOperation.id)}\nStatus: ${outputToken.yellow(
115-
finalOperation.status,
116-
)}\nObject count: ${outputToken.gray(
117-
String(finalOperation.objectCount),
118-
)}\n\nDownload results ${outputToken.link('here.', url)}`.value,
119-
})
85+
renderBulkOperationComplete(finalOperation)
12086
} else {
121-
const infoSections = [
122-
{
123-
title: 'Bulk operation started!',
124-
body: [
125-
{
126-
list: {
127-
items: [
128-
outputContent`ID: ${outputToken.cyan(result.id)}`.value,
129-
outputContent`Status: ${outputToken.yellow(result.status)}`.value,
130-
outputContent`Created: ${outputToken.gray(String(result.createdAt))}`.value,
131-
],
132-
},
133-
},
134-
],
135-
},
136-
]
137-
138-
renderInfo({customSections: infoSections})
139-
140-
renderSuccess({
141-
headline: 'Bulk operation started successfully!',
142-
body: 'Congrats!',
143-
})
87+
renderBulkOperationStarted(result)
14488
}
14589
}
14690
}
@@ -151,3 +95,83 @@ function isMutation(graphqlOperation: string): boolean {
15195

15296
return firstOperation?.kind === 'OperationDefinition' && firstOperation.operation === 'mutation'
15397
}
98+
99+
async function* watchBulkOperation(
100+
adminSession: ReturnType<typeof ensureAuthenticatedAdmin> extends Promise<infer T> ? T : never,
101+
operationId: string,
102+
): AsyncGenerator<BulkOperation, BulkOperation> {
103+
let lastObjectCount = ''
104+
105+
while (true) {
106+
// eslint-disable-next-line no-await-in-loop
107+
const response = await adminRequestDoc<GetBulkOperationByIdQuery, {id: string}>({
108+
query: GetBulkOperationById,
109+
session: adminSession,
110+
variables: {id: operationId},
111+
version: '2026-01',
112+
})
113+
114+
if (!response.bulkOperation) {
115+
throw new Error('bulk operation not found')
116+
}
117+
118+
const op = response.bulkOperation as BulkOperation
119+
120+
const shouldYield =
121+
op.status === 'CREATED' || (op.status === 'RUNNING' && String(op.objectCount) !== lastObjectCount)
122+
123+
if (shouldYield) {
124+
if (op.status === 'RUNNING') {
125+
lastObjectCount = String(op.objectCount)
126+
}
127+
yield op
128+
}
129+
130+
if (TERMINAL_STATUSES.includes(op.status)) {
131+
return op
132+
}
133+
134+
// eslint-disable-next-line no-await-in-loop
135+
await sleep(POLL_INTERVAL_SECONDS)
136+
}
137+
}
138+
139+
function renderBulkOperationComplete(operation: BulkOperation): void {
140+
const url = operation.url ?? ''
141+
142+
renderSuccess({
143+
headline: 'Bulk operation complete!',
144+
body: outputContent`ID: ${outputToken.cyan(operation.id)}\nStatus: ${outputToken.yellow(
145+
operation.status,
146+
)}\nObject count: ${outputToken.gray(String(operation.objectCount))}\n\nDownload results ${outputToken.link(
147+
'here.',
148+
url,
149+
)}`.value,
150+
})
151+
}
152+
153+
function renderBulkOperationStarted(result: {id: string; status: string; createdAt: unknown}): void {
154+
const infoSections = [
155+
{
156+
title: 'Bulk operation started!',
157+
body: [
158+
{
159+
list: {
160+
items: [
161+
outputContent`ID: ${outputToken.cyan(result.id)}`.value,
162+
outputContent`Status: ${outputToken.yellow(result.status)}`.value,
163+
outputContent`Created: ${outputToken.gray(String(result.createdAt))}`.value,
164+
],
165+
},
166+
},
167+
],
168+
},
169+
]
170+
171+
renderInfo({customSections: infoSections})
172+
173+
renderSuccess({
174+
headline: 'Bulk operation started successfully!',
175+
body: 'Congrats!',
176+
})
177+
}

packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function createMockOperation(overrides: Partial<BulkOperation> = {}): BulkOperat
2525

2626
describe('formatBulkOperationStatus', () => {
2727
test('formats RUNNING status with object count', () => {
28-
const result = formatBulkOperationStatus(createMockOperation({ status: 'RUNNING', objectCount: 42 }))
28+
const result = formatBulkOperationStatus(createMockOperation({status: 'RUNNING', objectCount: 42}))
2929
expect(result.value).toContain('Bulk operation in progress...')
3030
expect(result.value).toContain('(42 objects)')
3131
})
@@ -42,7 +42,9 @@ describe('formatBulkOperationStatus', () => {
4242
})
4343

4444
test('formats FAILED status with error code', () => {
45-
const result = formatBulkOperationStatus(createMockOperation({status: 'FAILED', objectCount: 10, errorCode: 'ACCESS_DENIED'}))
45+
const result = formatBulkOperationStatus(
46+
createMockOperation({status: 'FAILED', objectCount: 10, errorCode: 'ACCESS_DENIED'}),
47+
)
4648
expect(result.value).toContain('Bulk operation failed.')
4749
expect(result.value).toContain('(error: ACCESS_DENIED)')
4850
})
@@ -69,7 +71,10 @@ describe('formatBulkOperationStatus', () => {
6971
})
7072

7173
test('formats unknown status', () => {
72-
const result = formatBulkOperationStatus({...createMockOperation(), status: 'UNKNOWN_STATUS'} as unknown as BulkOperation)
74+
const result = formatBulkOperationStatus({
75+
...createMockOperation(),
76+
status: 'UNKNOWN_STATUS',
77+
} as unknown as BulkOperation)
7378
expect(result.value).toBe('Bulk operation status: UNKNOWN_STATUS')
7479
})
7580
})

0 commit comments

Comments
 (0)