@oondemand/oon-core-back 0.3.53 → 0.3.54
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 +1 -1
- package/src/boot/database.js +36 -0
- package/src/core/middlewares/error.js +89 -6
package/package.json
CHANGED
package/src/boot/database.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const mongoose = require("mongoose");
|
|
2
|
+
const registry = require("../core/registry");
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Conexão com o MongoDB a partir das envs padrão da Central.
|
|
@@ -51,9 +52,42 @@ function databaseLabel(uri) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function shouldSyncDeclaredIndexes() {
|
|
56
|
+
return String(process.env.OON_SYNC_INDEXES ?? "true").toLowerCase() !== "false";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mantém os índices físicos do MongoDB alinhados às models registradas.
|
|
61
|
+
*
|
|
62
|
+
* `createIndexes()` apenas cria índices ausentes e deixa índices removidos do
|
|
63
|
+
* domínio ativos indefinidamente. `syncIndexes()` também elimina esses índices
|
|
64
|
+
* obsoletos, evitando restrições antigas como um `codigo_1` único continuar
|
|
65
|
+
* bloqueando novos registros depois que o campo saiu do manifesto.
|
|
66
|
+
*/
|
|
67
|
+
async function syncDeclaredIndexes(entries = registry.listModels()) {
|
|
68
|
+
if (!shouldSyncDeclaredIndexes()) return [];
|
|
69
|
+
|
|
70
|
+
const results = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const model = entry?.mongooseModel;
|
|
73
|
+
if (!model || typeof model.syncIndexes !== "function") continue;
|
|
74
|
+
|
|
75
|
+
const dropped = await model.syncIndexes();
|
|
76
|
+
const normalized = Array.isArray(dropped) ? dropped : [];
|
|
77
|
+
results.push({ model: entry.name, dropped: normalized });
|
|
78
|
+
|
|
79
|
+
if (normalized.length) {
|
|
80
|
+
console.log(`[OonCore] Índices obsoletos removidos de ${entry.name}: ${normalized.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
async function connectDB() {
|
|
55
88
|
const uri = buildMongoUri();
|
|
56
89
|
await mongoose.connect(uri, connectionOptions());
|
|
90
|
+
await syncDeclaredIndexes();
|
|
57
91
|
console.log(`Conectado ao MongoDB (${databaseLabel(uri)})`);
|
|
58
92
|
return mongoose.connection;
|
|
59
93
|
}
|
|
@@ -63,4 +97,6 @@ module.exports = {
|
|
|
63
97
|
buildMongoUri,
|
|
64
98
|
connectionOptions,
|
|
65
99
|
databaseLabel,
|
|
100
|
+
shouldSyncDeclaredIndexes,
|
|
101
|
+
syncDeclaredIndexes,
|
|
66
102
|
};
|
|
@@ -1,27 +1,94 @@
|
|
|
1
1
|
const helpers = require("../utils/helpers");
|
|
2
2
|
const GenericError = require("../errors/GenericError");
|
|
3
3
|
|
|
4
|
+
function firstDuplicateField(error) {
|
|
5
|
+
const source = error?.keyPattern || error?.keyValue;
|
|
6
|
+
const direct = source && typeof source === "object" ? Object.keys(source)[0] : undefined;
|
|
7
|
+
if (direct) return direct;
|
|
8
|
+
|
|
9
|
+
const index = String(error?.message || "").match(/index:\s+([^\s]+)\s+dup key/i)?.[1];
|
|
10
|
+
return index?.replace(/_\d+$/, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function duplicateIndexName(error) {
|
|
14
|
+
return String(error?.message || "").match(/index:\s+([^\s]+)\s+dup key/i)?.[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fieldLabel(field) {
|
|
18
|
+
if (!field) return "o campo informado";
|
|
19
|
+
return field
|
|
20
|
+
.replace(/Id$/, "")
|
|
21
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
22
|
+
.replace(/[_-]+/g, " ")
|
|
23
|
+
.replace(/^./, (letter) => letter.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function duplicateKeyDetails(error, requestId) {
|
|
27
|
+
const field = firstDuplicateField(error);
|
|
28
|
+
const index = duplicateIndexName(error);
|
|
29
|
+
const value = field && error?.keyValue && typeof error.keyValue === "object"
|
|
30
|
+
? error.keyValue[field]
|
|
31
|
+
: undefined;
|
|
32
|
+
const staleNullIndex = field && value == null;
|
|
33
|
+
const message = staleNullIndex
|
|
34
|
+
? `A estrutura do banco ainda possui uma restrição antiga para ${fieldLabel(field)}. Os índices precisam ser sincronizados antes de tentar novamente.`
|
|
35
|
+
: field
|
|
36
|
+
? `Já existe um registro com o mesmo valor em ${fieldLabel(field)}.`
|
|
37
|
+
: "Já existe um registro com os mesmos dados únicos.";
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
code: staleNullIndex ? "STALE_UNIQUE_INDEX" : "DUPLICATE_KEY",
|
|
41
|
+
message,
|
|
42
|
+
...(field ? { field } : {}),
|
|
43
|
+
...(index ? { index } : {}),
|
|
44
|
+
...(requestId ? { requestId } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
4
48
|
/**
|
|
5
|
-
* Error handler central. Trata GenericError (domínio), erros
|
|
6
|
-
*
|
|
49
|
+
* Error handler central. Trata GenericError (domínio), erros conhecidos do
|
|
50
|
+
* Mongo/Mongoose e cai num 500 genérico para o restante.
|
|
7
51
|
*/
|
|
8
|
-
const errorMiddleware = (error,
|
|
52
|
+
const errorMiddleware = (error, req, res, next) => {
|
|
9
53
|
if (!error) return next();
|
|
10
54
|
|
|
55
|
+
const requestId = req?.correlationId;
|
|
56
|
+
|
|
11
57
|
if (error instanceof GenericError) {
|
|
58
|
+
const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
|
|
59
|
+
? { ...error.details, ...(requestId ? { requestId } : {}) }
|
|
60
|
+
: error.details;
|
|
12
61
|
return helpers.sendErrorResponse({
|
|
13
62
|
res,
|
|
14
63
|
statusCode: error.statusCode,
|
|
15
|
-
error:
|
|
64
|
+
error: details,
|
|
16
65
|
message: error.message,
|
|
17
66
|
});
|
|
18
67
|
}
|
|
19
68
|
|
|
69
|
+
if (error?.code === 11000 || error?.name === "MongoServerError" && error?.code === 11000) {
|
|
70
|
+
const details = duplicateKeyDetails(error, requestId);
|
|
71
|
+
return helpers.sendErrorResponse({
|
|
72
|
+
res,
|
|
73
|
+
statusCode: 409,
|
|
74
|
+
error: details,
|
|
75
|
+
message: details.message,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
20
79
|
if (error.name === "ValidationError") {
|
|
21
80
|
return helpers.sendErrorResponse({
|
|
22
81
|
res,
|
|
23
82
|
statusCode: 422,
|
|
24
|
-
error:
|
|
83
|
+
error: {
|
|
84
|
+
code: "VALIDATION_ERROR",
|
|
85
|
+
message: "Erro de validação.",
|
|
86
|
+
details: Object.entries(error.errors || {}).map(([field, item]) => ({
|
|
87
|
+
field,
|
|
88
|
+
message: item.message,
|
|
89
|
+
})),
|
|
90
|
+
...(requestId ? { requestId } : {}),
|
|
91
|
+
},
|
|
25
92
|
message: "Erro de validação.",
|
|
26
93
|
});
|
|
27
94
|
}
|
|
@@ -30,17 +97,33 @@ const errorMiddleware = (error, _req, res, next) => {
|
|
|
30
97
|
return helpers.sendErrorResponse({
|
|
31
98
|
res,
|
|
32
99
|
statusCode: 400,
|
|
100
|
+
error: {
|
|
101
|
+
code: "INVALID_IDENTIFIER",
|
|
102
|
+
message: "Identificador inválido.",
|
|
103
|
+
...(error.path ? { field: error.path } : {}),
|
|
104
|
+
...(requestId ? { requestId } : {}),
|
|
105
|
+
},
|
|
33
106
|
message: "Identificador inválido.",
|
|
34
107
|
});
|
|
35
108
|
}
|
|
36
109
|
|
|
37
110
|
console.log("🆘 [UNHANDLED ERROR]:", error?.message || error);
|
|
111
|
+
const technicalMessage = process.env.NODE_ENV === "production"
|
|
112
|
+
? undefined
|
|
113
|
+
: String(error?.message || error || "");
|
|
38
114
|
return helpers.sendErrorResponse({
|
|
39
115
|
res,
|
|
40
116
|
statusCode: 500,
|
|
41
|
-
error:
|
|
117
|
+
error: {
|
|
118
|
+
code: "INTERNAL_ERROR",
|
|
119
|
+
message: "Houve um erro inesperado!",
|
|
120
|
+
...(technicalMessage ? { technicalMessage } : {}),
|
|
121
|
+
...(requestId ? { requestId } : {}),
|
|
122
|
+
},
|
|
42
123
|
message: "Houve um erro inesperado!",
|
|
43
124
|
});
|
|
44
125
|
};
|
|
45
126
|
|
|
46
127
|
module.exports = errorMiddleware;
|
|
128
|
+
module.exports.firstDuplicateField = firstDuplicateField;
|
|
129
|
+
module.exports.duplicateKeyDetails = duplicateKeyDetails;
|