@oondemand/oon-core-back 0.4.24 → 0.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.4.24",
3
+ "version": "0.4.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -125,14 +125,50 @@ function translateProviderActivationError(error, fallback) {
125
125
 
126
126
  const timedOut = error.code === "ECONNABORTED";
127
127
  const statusCode = timedOut ? 504 : error.response?.status || 502;
128
+ const payload = error.response?.data;
129
+ const envelope = payload?.error && typeof payload.error === "object"
130
+ ? payload.error
131
+ : payload;
132
+ const responseHeaders = error.response?.headers;
133
+ const headerRequestId = typeof responseHeaders?.get === "function"
134
+ ? responseHeaders.get("x-request-id") || responseHeaders.get("x-correlation-id")
135
+ : responseHeaders?.["x-request-id"] || responseHeaders?.["x-correlation-id"];
136
+ const requestId = String(
137
+ envelope?.requestId
138
+ || envelope?.details?.requestId
139
+ || headerRequestId
140
+ || "",
141
+ ).trim();
142
+ const upstreamDetails = envelope?.details;
143
+ const details = upstreamDetails && typeof upstreamDetails === "object"
144
+ ? { ...upstreamDetails, ...(requestId ? { requestId } : {}) }
145
+ : {
146
+ ...(upstreamDetails !== undefined ? { upstream: upstreamDetails } : {}),
147
+ ...(requestId ? { requestId } : {}),
148
+ };
128
149
  const message =
129
- error.response?.data?.message ||
130
- error.response?.data?.error?.message ||
131
- (timedOut
150
+ envelope?.message
151
+ || payload?.message
152
+ || (timedOut
132
153
  ? "A Central de Ativações não respondeu dentro do prazo."
133
154
  : fallback);
134
155
 
135
- return new GenericError(message, { statusCode });
156
+ return new GenericError(message, {
157
+ statusCode,
158
+ code: envelope?.code || (timedOut ? "ACTIVATION_PROVIDER_TIMEOUT" : "ACTIVATION_PROVIDER_FAILED"),
159
+ details: Object.keys(details).length ? details : null,
160
+ });
161
+ }
162
+
163
+ async function activationProviderPost(...args) {
164
+ try {
165
+ return await providerPost(...args);
166
+ } catch (error) {
167
+ throw translateProviderActivationError(
168
+ error,
169
+ "A Central de Ativações recusou a operação.",
170
+ );
171
+ }
136
172
  }
137
173
 
138
174
  function idOf(value) {
@@ -522,7 +558,7 @@ router.post(
522
558
  });
523
559
 
524
560
  try {
525
- const response = await providerPost(
561
+ const response = await activationProviderPost(
526
562
  req,
527
563
  PATHS.validar,
528
564
  providerPayload(req, codigoAtivacao),
@@ -601,14 +637,14 @@ router.post(
601
637
  });
602
638
  }
603
639
 
604
- const preview = await providerPost(
640
+ const preview = await activationProviderPost(
605
641
  req,
606
642
  PATHS.validar,
607
643
  providerPayload(req, codigoAtivacao),
608
644
  );
609
645
  assertApp(preview.data);
610
646
 
611
- const activated = await providerPost(
647
+ const activated = await activationProviderPost(
612
648
  req,
613
649
  PATHS.ativar,
614
650
  providerPayload(req, codigoAtivacao, { nomeInstancia, metadata: {} }),
@@ -682,7 +718,7 @@ async function complete(req, instance, config) {
682
718
  });
683
719
  }
684
720
 
685
- await providerPost(
721
+ await activationProviderPost(
686
722
  req,
687
723
  PATHS.concluir,
688
724
  {
@@ -746,3 +782,4 @@ router.post(
746
782
  );
747
783
 
748
784
  module.exports = router;
785
+ module.exports.translateProviderActivationError = translateProviderActivationError;
@@ -4,6 +4,12 @@ const { mongo } = require("mongoose");
4
4
 
5
5
  const DIGITALOCEAN_API = "https://api.digitalocean.com/v2";
6
6
  const DIGITALOCEAN_HOST_SUFFIX = ".mongo.ondigitalocean.com";
7
+ const DIGITALOCEAN_RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
8
+ const DIGITALOCEAN_SERVER_STATUS = new Set([500, 502, 503, 504]);
9
+
10
+ function delay(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
7
13
 
8
14
  function randomSecret(bytes = 32) {
9
15
  return require("node:crypto").randomBytes(bytes).toString("base64url");
@@ -177,58 +183,108 @@ async function digitalOceanRequest(
177
183
  body,
178
184
  expected = [200],
179
185
  },
180
- { fetchImpl = globalThis.fetch } = {},
186
+ {
187
+ fetchImpl = globalThis.fetch,
188
+ sleepImpl = delay,
189
+ retryAttempts = 3,
190
+ retryBaseDelayMs = 1000,
191
+ } = {},
181
192
  ) {
182
193
  if (!fetchImpl) throw new Error("fetch não disponível para a DigitalOcean API.");
183
- const controller = new AbortController();
184
- const timeout = setTimeout(() => controller.abort(), 20000);
185
- let response;
186
- try {
187
- response = await fetchImpl(
188
- `${DIGITALOCEAN_API}/databases/${encodeURIComponent(clusterId)}${path}`,
189
- {
190
- method,
191
- headers: {
192
- Authorization: `Bearer ${token}`,
193
- Accept: "application/json",
194
- ...(body ? { "Content-Type": "application/json" } : {}),
194
+ const attempts = Math.max(1, Number(retryAttempts) || 3);
195
+
196
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), 20000);
199
+ let response;
200
+ try {
201
+ response = await fetchImpl(
202
+ `${DIGITALOCEAN_API}/databases/${encodeURIComponent(clusterId)}${path}`,
203
+ {
204
+ method,
205
+ headers: {
206
+ Authorization: `Bearer ${token}`,
207
+ Accept: "application/json",
208
+ ...(body ? { "Content-Type": "application/json" } : {}),
209
+ },
210
+ body: body ? JSON.stringify(body) : undefined,
211
+ signal: controller.signal,
195
212
  },
196
- body: body ? JSON.stringify(body) : undefined,
197
- signal: controller.signal,
198
- },
199
- );
200
- } catch (error) {
201
- if (error.name === "AbortError") {
202
- throw new Error("Timeout ao acessar a DigitalOcean API.");
213
+ );
214
+ } catch (error) {
215
+ if (attempt < attempts) {
216
+ await sleepImpl(Math.min(10000, retryBaseDelayMs * 2 ** (attempt - 1)));
217
+ continue;
218
+ }
219
+ if (error.name === "AbortError") {
220
+ throw new Error("Timeout ao acessar a DigitalOcean API.");
221
+ }
222
+ throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
223
+ } finally {
224
+ clearTimeout(timeout);
225
+ }
226
+
227
+ const payload = await response.json().catch(() => ({}));
228
+ if (expected.includes(response.status)) {
229
+ return { status: response.status, payload };
203
230
  }
204
- throw new Error(`Falha ao acessar a DigitalOcean API: ${error.message}`);
205
- } finally {
206
- clearTimeout(timeout);
207
- }
208
231
 
209
- const payload = await response.json().catch(() => ({}));
210
- if (!expected.includes(response.status)) {
232
+ if (
233
+ DIGITALOCEAN_RETRYABLE_STATUS.has(response.status)
234
+ && attempt < attempts
235
+ ) {
236
+ const retryAfter = Number(response.headers?.get?.("retry-after"));
237
+ const backoff = Number.isFinite(retryAfter) && retryAfter > 0
238
+ ? retryAfter * 1000
239
+ : retryBaseDelayMs * 2 ** (attempt - 1);
240
+ await sleepImpl(Math.min(10000, backoff));
241
+ continue;
242
+ }
243
+
244
+ const providerMessage = String(payload.message || payload.id || "erro")
245
+ .replace(/[\r\n]+/g, " ")
246
+ .slice(0, 300);
247
+ const providerRequestId = String(payload.request_id || "")
248
+ .replace(/[^A-Za-z0-9_.:-]/g, "")
249
+ .slice(0, 120);
211
250
  const error = new Error(
212
- `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${payload.id || "erro"}).`,
251
+ `DigitalOcean API rejeitou ${method} ${path}: HTTP ${response.status} (${providerMessage})`
252
+ + (providerRequestId ? ` [requestId: ${providerRequestId}]` : "")
253
+ + ".",
213
254
  );
214
255
  error.status = response.status;
215
256
  error.payload = payload;
257
+ error.providerRequestId = providerRequestId || undefined;
216
258
  throw error;
217
259
  }
218
- return { status: response.status, payload };
260
+
261
+ throw new Error("DigitalOcean API não concluiu a solicitação.");
219
262
  }
220
263
 
221
264
  async function ensureDigitalOceanDatabase(options, deps) {
222
265
  const path = `/dbs/${encodeURIComponent(options.dbName)}`;
266
+ let exists = false;
223
267
  try {
224
268
  await digitalOceanRequest(
225
269
  { ...options, path, expected: [200] },
226
270
  deps,
227
271
  );
228
- return;
272
+ exists = true;
229
273
  } catch (error) {
230
- if (error.status !== 404) throw error;
274
+ if (error.status === 404) {
275
+ exists = false;
276
+ } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
277
+ const listed = await digitalOceanRequest(
278
+ { ...options, path: "/dbs", expected: [200] },
279
+ deps,
280
+ );
281
+ exists = Array.isArray(listed.payload.dbs)
282
+ && listed.payload.dbs.some((item) => item?.name === options.dbName);
283
+ } else {
284
+ throw error;
285
+ }
231
286
  }
287
+ if (exists) return;
232
288
 
233
289
  await digitalOceanRequest(
234
290
  {
@@ -253,15 +309,28 @@ function digitalOceanUserSettings(dbName) {
253
309
 
254
310
  async function provisionDigitalOceanUser(options, deps) {
255
311
  const userPath = `/users/${encodeURIComponent(options.username)}`;
256
- let exists = true;
312
+ let exists = false;
257
313
  try {
258
314
  await digitalOceanRequest(
259
315
  { ...options, path: userPath, expected: [200] },
260
316
  deps,
261
317
  );
318
+ exists = true;
262
319
  } catch (error) {
263
- if (error.status !== 404) throw error;
264
- exists = false;
320
+ if (error.status === 404) {
321
+ exists = false;
322
+ } else if (DIGITALOCEAN_SERVER_STATUS.has(error.status)) {
323
+ const listed = await digitalOceanRequest(
324
+ { ...options, path: "/users", expected: [200] },
325
+ deps,
326
+ );
327
+ exists = Array.isArray(listed.payload.users)
328
+ && listed.payload.users.some(
329
+ (item) => item?.name === options.username,
330
+ );
331
+ } else {
332
+ throw error;
333
+ }
265
334
  }
266
335
 
267
336
  const settings = digitalOceanUserSettings(options.dbName);
@@ -378,6 +447,8 @@ async function provisionMongo(options, deps = {}) {
378
447
  module.exports = {
379
448
  DIGITALOCEAN_API,
380
449
  DIGITALOCEAN_HOST_SUFFIX,
450
+ DIGITALOCEAN_RETRYABLE_STATUS,
451
+ DIGITALOCEAN_SERVER_STATUS,
381
452
  mongoIdentity,
382
453
  mongoHost,
383
454
  resolveMongoProvisioner,