Skip to content

Commit 0096ce2

Browse files
committed
refactor: simplify export functionality code
- Remove unused imports and JSDoc comments - Create helper functions to reduce duplication (toTimestamp, toIsoDate, handleError, bugsnagLog) - Replace verbose try-catch blocks with .catch(() => {}) where errors are ignored - Extract duplicated error handling in API routes - Consolidate Bugsnag logger setup - Simplify loop conditions and object creation patterns Reduces lib/export.js from 660 to 391 lines (~40% reduction)
1 parent ad587a7 commit 0096ce2

File tree

3 files changed

+763
-319
lines changed

3 files changed

+763
-319
lines changed

lib/api-routes/export-routes.js

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const { Export } = require('../export');
5+
const Boom = require('@hapi/boom');
6+
const Joi = require('joi');
7+
const { failAction } = require('../tools');
8+
const { accountIdSchema, exportRequestSchema, exportStatusSchema, exportListSchema } = require('../schemas');
9+
10+
function handleError(request, err) {
11+
request.logger.error({ msg: 'API request failed', err });
12+
if (Boom.isBoom(err)) {
13+
throw err;
14+
}
15+
const error = Boom.boomify(err, { statusCode: err.statusCode || 500 });
16+
if (err.code) {
17+
error.output.payload.code = err.code;
18+
}
19+
throw error;
20+
}
21+
22+
async function init(args) {
23+
const { server, CORS_CONFIG } = args;
24+
25+
server.route({
26+
method: 'POST',
27+
path: '/v1/account/{account}/export',
28+
29+
async handler(request) {
30+
try {
31+
return await Export.create(request.params.account, {
32+
folders: request.payload.folders,
33+
startDate: request.payload.startDate,
34+
endDate: request.payload.endDate,
35+
textType: request.payload.textType,
36+
maxBytes: request.payload.maxBytes,
37+
includeAttachments: request.payload.includeAttachments
38+
});
39+
} catch (err) {
40+
handleError(request, err);
41+
}
42+
},
43+
44+
options: {
45+
description: 'Create export',
46+
notes: 'Creates a new bulk message export job. The export runs asynchronously and notifies via webhook when complete.',
47+
tags: ['api', 'Export'],
48+
49+
auth: {
50+
strategy: 'api-token',
51+
mode: 'required'
52+
},
53+
cors: CORS_CONFIG,
54+
55+
validate: {
56+
options: {
57+
stripUnknown: false,
58+
abortEarly: false,
59+
convert: true
60+
},
61+
failAction,
62+
63+
params: Joi.object({
64+
account: accountIdSchema.required()
65+
}).label('CreateExportParams'),
66+
67+
payload: exportRequestSchema
68+
},
69+
70+
response: {
71+
schema: Joi.object({
72+
exportId: Joi.string().example('exp_abc123def456').description('Export job identifier'),
73+
status: Joi.string().example('queued').description('Export status'),
74+
created: Joi.date().iso().example('2024-01-15T10:30:00Z').description('When export was created')
75+
}).label('CreateExportResponse'),
76+
failAction: 'log'
77+
}
78+
}
79+
});
80+
81+
server.route({
82+
method: 'GET',
83+
path: '/v1/account/{account}/export/{exportId}',
84+
85+
async handler(request) {
86+
try {
87+
const result = await Export.get(request.params.account, request.params.exportId);
88+
if (!result) {
89+
throw Boom.notFound('Export not found');
90+
}
91+
return result;
92+
} catch (err) {
93+
handleError(request, err);
94+
}
95+
},
96+
97+
options: {
98+
description: 'Get export status',
99+
notes: 'Returns the status and progress of an export job.',
100+
tags: ['api', 'Export'],
101+
102+
auth: {
103+
strategy: 'api-token',
104+
mode: 'required'
105+
},
106+
cors: CORS_CONFIG,
107+
108+
validate: {
109+
options: {
110+
stripUnknown: false,
111+
abortEarly: false,
112+
convert: true
113+
},
114+
failAction,
115+
116+
params: Joi.object({
117+
account: accountIdSchema.required(),
118+
exportId: Joi.string().max(256).required().example('exp_abc123def456').description('Export job identifier')
119+
}).label('GetExportParams')
120+
},
121+
122+
response: {
123+
schema: exportStatusSchema,
124+
failAction: 'log'
125+
}
126+
}
127+
});
128+
129+
server.route({
130+
method: 'GET',
131+
path: '/v1/account/{account}/export/{exportId}/download',
132+
133+
async handler(request, h) {
134+
try {
135+
const fileInfo = await Export.getFile(request.params.account, request.params.exportId);
136+
if (!fileInfo) {
137+
throw Boom.notFound('Export not found');
138+
}
139+
140+
return h
141+
.response(fs.createReadStream(fileInfo.filePath))
142+
.type('application/gzip')
143+
.header('Content-Disposition', `attachment; filename="${fileInfo.filename}"`)
144+
.header('Content-Encoding', 'identity');
145+
} catch (err) {
146+
handleError(request, err);
147+
}
148+
},
149+
150+
options: {
151+
description: 'Download export file',
152+
notes: 'Downloads the completed export file as gzip-compressed NDJSON.',
153+
tags: ['api', 'Export'],
154+
155+
plugins: {
156+
'hapi-swagger': {
157+
produces: ['application/gzip']
158+
}
159+
},
160+
161+
auth: {
162+
strategy: 'api-token',
163+
mode: 'required'
164+
},
165+
cors: CORS_CONFIG,
166+
167+
validate: {
168+
options: {
169+
stripUnknown: false,
170+
abortEarly: false,
171+
convert: true
172+
},
173+
failAction,
174+
175+
params: Joi.object({
176+
account: accountIdSchema.required(),
177+
exportId: Joi.string().max(256).required().example('exp_abc123def456').description('Export job identifier')
178+
}).label('DownloadExportParams')
179+
}
180+
}
181+
});
182+
183+
server.route({
184+
method: 'DELETE',
185+
path: '/v1/account/{account}/export/{exportId}',
186+
187+
async handler(request) {
188+
try {
189+
const deleted = await Export.delete(request.params.account, request.params.exportId);
190+
if (!deleted) {
191+
throw Boom.notFound('Export not found');
192+
}
193+
return { deleted: true };
194+
} catch (err) {
195+
handleError(request, err);
196+
}
197+
},
198+
199+
options: {
200+
description: 'Delete export',
201+
notes: 'Cancels a pending export or deletes a completed export. Removes both Redis data and the export file.',
202+
tags: ['api', 'Export'],
203+
204+
auth: {
205+
strategy: 'api-token',
206+
mode: 'required'
207+
},
208+
cors: CORS_CONFIG,
209+
210+
validate: {
211+
options: {
212+
stripUnknown: false,
213+
abortEarly: false,
214+
convert: true
215+
},
216+
failAction,
217+
218+
params: Joi.object({
219+
account: accountIdSchema.required(),
220+
exportId: Joi.string().max(256).required().example('exp_abc123def456').description('Export job identifier')
221+
}).label('DeleteExportParams')
222+
},
223+
224+
response: {
225+
schema: Joi.object({
226+
deleted: Joi.boolean().example(true).description('True if export was deleted')
227+
}).label('DeleteExportResponse'),
228+
failAction: 'log'
229+
}
230+
}
231+
});
232+
233+
server.route({
234+
method: 'GET',
235+
path: '/v1/account/{account}/exports',
236+
237+
async handler(request) {
238+
try {
239+
return await Export.list(request.params.account, {
240+
page: request.query.page,
241+
pageSize: request.query.pageSize
242+
});
243+
} catch (err) {
244+
handleError(request, err);
245+
}
246+
},
247+
248+
options: {
249+
description: 'List exports',
250+
notes: 'Lists all exports for an account with pagination.',
251+
tags: ['api', 'Export'],
252+
253+
auth: {
254+
strategy: 'api-token',
255+
mode: 'required'
256+
},
257+
cors: CORS_CONFIG,
258+
259+
validate: {
260+
options: {
261+
stripUnknown: false,
262+
abortEarly: false,
263+
convert: true
264+
},
265+
failAction,
266+
267+
params: Joi.object({
268+
account: accountIdSchema.required()
269+
}).label('ListExportsParams'),
270+
271+
query: Joi.object({
272+
page: Joi.number()
273+
.integer()
274+
.min(0)
275+
.max(1024 * 1024)
276+
.default(0)
277+
.example(0)
278+
.description('Page number (zero indexed)')
279+
.label('PageNumber'),
280+
pageSize: Joi.number().integer().min(1).max(1000).default(20).example(20).description('How many entries per page').label('PageSize')
281+
}).label('ListExportsQuery')
282+
},
283+
284+
response: {
285+
schema: exportListSchema,
286+
failAction: 'log'
287+
}
288+
}
289+
});
290+
}
291+
292+
module.exports = init;

0 commit comments

Comments
 (0)