|
1 | | -import { User } from '@supabase/supabase-js'; |
2 | | -import { NextFunction, Request, Response } from 'express'; |
3 | | -import { Contacts } from '../db/interfaces/Contacts'; |
4 | | -import { Contact, ExportService } from '../db/types'; |
5 | | -import Billing from '../utils/billing-plugin'; |
6 | | -import { ExportOptions, ExportType } from '../services/export/types'; |
7 | | -import ExportFactory from '../services/export'; |
8 | | -import { |
9 | | - MiningSources, |
10 | | - OAuthMiningSourceCredentials |
11 | | -} from '../db/interfaces/MiningSources'; |
12 | | - |
13 | | -async function validateRequest( |
14 | | - req: Request, |
15 | | - res: Response, |
16 | | - miningSourceService: MiningSources |
17 | | -) { |
18 | | - const user = res.locals.user as User; |
19 | | - const partialExport = req.body.partialExport ?? false; |
20 | | - const updateEmptyFieldsOnly = req.body.updateEmptyFieldsOnly ?? false; |
21 | | - const exportType = (req.params.exportType ?? ExportType.CSV) as ExportType; |
22 | | - |
23 | | - const localeFromHeader = req.headers['accept-language']; |
24 | | - const delimiterOption = req.query.delimiter?.toString(); |
25 | | - |
26 | | - const validExportType = Object.values(ExportType).includes( |
27 | | - exportType as ExportType |
28 | | - ); |
29 | | - |
30 | | - if (!validExportType) { |
31 | | - throw new Error(`Invalid export type: ${exportType}`); |
32 | | - } |
33 | | - |
34 | | - let googleContactsOptions: ExportOptions['googleContactsOptions'] = { |
35 | | - userId: user.id, |
36 | | - accessToken: undefined, |
37 | | - refreshToken: undefined, |
38 | | - updateEmptyFieldsOnly |
39 | | - }; |
40 | | - |
41 | | - if (exportType === ExportType.GOOGLE_CONTACTS) { |
42 | | - const targetEmail = req.body.targetEmail || user.email; |
43 | | - const sources = await miningSourceService.getSourcesForUser( |
44 | | - user.id, |
45 | | - targetEmail as string |
46 | | - ); |
47 | | - const oauthCredentials = sources?.find((e) => e.email === targetEmail) |
48 | | - ?.credentials as OAuthMiningSourceCredentials; |
49 | | - |
50 | | - googleContactsOptions = { |
51 | | - ...googleContactsOptions, |
52 | | - accessToken: oauthCredentials?.accessToken, |
53 | | - refreshToken: oauthCredentials?.refreshToken |
54 | | - }; |
55 | | - } |
56 | | - |
57 | | - const { |
58 | | - ids, |
59 | | - exportAllContacts |
60 | | - }: { ids?: string[]; exportAllContacts: boolean } = req.body; |
61 | | - |
62 | | - if (!exportAllContacts && (!Array.isArray(ids) || !ids.length)) { |
63 | | - return { |
64 | | - userId: user.id, |
65 | | - exportType, |
66 | | - contactsToExport: null, |
67 | | - partialExport, |
68 | | - exportOptions: { |
69 | | - locale: localeFromHeader, |
70 | | - delimiter: undefined, |
71 | | - googleContactsOptions |
72 | | - } |
73 | | - }; |
74 | | - } |
75 | | - |
76 | | - const contactsToExport = exportAllContacts ? undefined : ids; |
77 | | - |
78 | | - return { |
79 | | - userId: user.id, |
80 | | - exportType, |
81 | | - contactsToExport, |
82 | | - partialExport, |
83 | | - exportOptions: { |
84 | | - locale: localeFromHeader, |
85 | | - delimiter: delimiterOption, |
86 | | - googleContactsOptions |
87 | | - } |
88 | | - }; |
89 | | -} |
90 | | - |
91 | | -async function respondWithContacts( |
92 | | - res: Response, |
93 | | - userId: string, |
94 | | - contacts: Contacts, |
95 | | - exportType: ExportType, |
96 | | - contactsToExport?: string[], |
97 | | - exportOption?: ExportOptions |
98 | | -) { |
99 | | - const selectedContacts = await contacts.getContacts(userId, contactsToExport); |
100 | | - |
101 | | - if (!selectedContacts.length) { |
102 | | - return res.sendStatus(204); // 204 No Content |
103 | | - } |
104 | | - |
105 | | - try { |
106 | | - const { content, contentType } = await ExportFactory.get(exportType).export( |
107 | | - selectedContacts, |
108 | | - exportOption |
109 | | - ); |
110 | | - |
111 | | - return res.header('Content-Type', contentType).status(200).send(content); |
112 | | - } catch (err) { |
113 | | - if ((err as Error).message === 'Invalid credentials.') { |
114 | | - return res.sendStatus(401); |
115 | | - } |
116 | | - throw err; |
117 | | - } |
118 | | -} |
119 | | - |
120 | | -async function verifyCredits( |
121 | | - userId: string, |
122 | | - contacts: Contacts, |
123 | | - contactsToExport?: string[] |
124 | | -) { |
125 | | - const newContacts = await contacts.getNonExportedContacts( |
126 | | - userId, |
127 | | - contactsToExport |
128 | | - ); |
129 | | - const previousExportedContacts = await contacts.getExportedContacts( |
130 | | - userId, |
131 | | - contactsToExport |
132 | | - ); |
133 | | - const creditsInfo = await Billing!.validateCustomerCredits( |
134 | | - userId, |
135 | | - newContacts.length |
136 | | - ); |
137 | | - const response = { |
138 | | - total: newContacts.length + previousExportedContacts.length, |
139 | | - available: Math.floor(creditsInfo.availableUnits), |
140 | | - availableAlready: previousExportedContacts.length |
141 | | - }; |
142 | | - return { |
143 | | - newContacts, |
144 | | - previousExportedContacts, |
145 | | - creditsInfo, |
146 | | - response |
147 | | - }; |
148 | | -} |
149 | | - |
150 | | -async function registerAndDeductCredits( |
151 | | - userId: string, |
152 | | - exportType: ExportType, |
153 | | - availableUnits: number, |
154 | | - contacts: Contacts, |
155 | | - availableContacts: Contact[] |
156 | | -) { |
157 | | - if (availableContacts.length) { |
158 | | - await contacts.registerExportedContacts( |
159 | | - availableContacts.map(({ id }) => id), |
160 | | - exportType as unknown as ExportService, |
161 | | - userId |
162 | | - ); |
163 | | - await Billing!.deductCustomerCredits(userId, availableUnits); |
164 | | - } |
165 | | -} |
166 | | - |
167 | | -async function respondWithConfirmedContacts( |
168 | | - res: Response, |
169 | | - userId: string, |
170 | | - exportType: ExportType, |
171 | | - contacts: Contacts, |
172 | | - newContacts: Contact[], |
173 | | - previousExportedContacts: Contact[], |
174 | | - availableUnits: number, |
175 | | - statusCode: number, |
176 | | - exportOptions?: ExportOptions |
177 | | -) { |
178 | | - const availableContacts = newContacts.slice(0, availableUnits); |
179 | | - const selectedContacts = [...previousExportedContacts, ...availableContacts]; |
180 | | - |
181 | | - try { |
182 | | - const { content, contentType } = await ExportFactory.get(exportType).export( |
183 | | - selectedContacts, |
184 | | - exportOptions |
185 | | - ); |
186 | | - |
187 | | - await registerAndDeductCredits( |
188 | | - userId, |
189 | | - exportType, |
190 | | - availableUnits, |
191 | | - contacts, |
192 | | - selectedContacts |
193 | | - ); |
194 | | - |
195 | | - return res |
196 | | - .header('Content-Type', contentType) |
197 | | - .status(statusCode) |
198 | | - .send(content); |
199 | | - } catch (err) { |
200 | | - if ((err as Error).message === 'Invalid credentials.') { |
201 | | - return res.sendStatus(401); |
202 | | - } |
203 | | - throw err; |
204 | | - } |
205 | | -} |
206 | | - |
207 | | -export default function initializeContactsController( |
208 | | - contacts: Contacts, |
209 | | - miningSources: MiningSources |
210 | | -) { |
211 | | - return { |
212 | | - async exportContactsCSV(req: Request, res: Response, next: NextFunction) { |
213 | | - const { |
214 | | - userId, |
215 | | - contactsToExport, |
216 | | - partialExport, |
217 | | - exportType, |
218 | | - exportOptions |
219 | | - } = await validateRequest(req, res, miningSources); |
220 | | - if (contactsToExport === null) { |
221 | | - return res.status(400).json({ |
222 | | - message: 'Parameter "ids" must be a non-empty list of person ids' |
223 | | - }); |
224 | | - } |
225 | | - |
226 | | - let statusCode = 200; |
227 | | - try { |
228 | | - if (!Billing) { |
229 | | - // No need to Verify Credits, Export. |
230 | | - return await respondWithContacts( |
231 | | - res, |
232 | | - userId, |
233 | | - contacts, |
234 | | - exportType, |
235 | | - contactsToExport, |
236 | | - exportOptions |
237 | | - ); |
238 | | - } |
239 | | - |
240 | | - const { newContacts, previousExportedContacts, creditsInfo, response } = |
241 | | - await verifyCredits(userId, contacts, contactsToExport); |
242 | | - |
243 | | - if ( |
244 | | - creditsInfo.hasDeficientCredits && |
245 | | - !previousExportedContacts.length |
246 | | - ) { |
247 | | - return res.status(402).json(response); |
248 | | - } |
249 | | - |
250 | | - if (creditsInfo.hasInsufficientCredits && newContacts.length) { |
251 | | - if (!partialExport) { |
252 | | - res.statusMessage = 'Confirm Partial Content'; |
253 | | - return res.status(266).json(response); // 266 Confirm Partial Content |
254 | | - } |
255 | | - statusCode = 206; // 206 Partial Content |
256 | | - } |
257 | | - |
258 | | - // Export confirmed contacts. |
259 | | - return await respondWithConfirmedContacts( |
260 | | - res, |
261 | | - userId, |
262 | | - exportType, |
263 | | - contacts, |
264 | | - newContacts, |
265 | | - previousExportedContacts, |
266 | | - creditsInfo.availableUnits, |
267 | | - statusCode, |
268 | | - exportOptions |
269 | | - ); |
270 | | - } catch (error) { |
271 | | - return next(error); |
272 | | - } |
273 | | - } |
274 | | - }; |
| 1 | +export default function initializeContactsController() { |
| 2 | + return {}; |
275 | 3 | } |
0 commit comments