@oondemand/oon-core-back 0.3.62 → 0.3.64
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
package/src/api/core.routes.js
CHANGED
|
@@ -105,6 +105,156 @@ function runtimeSummary() {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const DASHBOARD_FILTER_OPERATORS = Object.freeze({
|
|
109
|
+
eq: "$eq",
|
|
110
|
+
ne: "$ne",
|
|
111
|
+
in: "$in",
|
|
112
|
+
nin: "$nin",
|
|
113
|
+
gt: "$gt",
|
|
114
|
+
gte: "$gte",
|
|
115
|
+
lt: "$lt",
|
|
116
|
+
lte: "$lte",
|
|
117
|
+
exists: "$exists",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function dashboardError(message, statusCode = 422) {
|
|
121
|
+
const error = new Error(message);
|
|
122
|
+
error.statusCode = statusCode;
|
|
123
|
+
return error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dashboardField(entry, field, usage) {
|
|
127
|
+
const normalized = String(field || "").trim();
|
|
128
|
+
if (!normalized || normalized.startsWith("$") || normalized.includes(".")) {
|
|
129
|
+
throw dashboardError(`Campo inválido para ${usage}.`);
|
|
130
|
+
}
|
|
131
|
+
if (normalized !== "_id" && !entry.metaFields?.[normalized]) {
|
|
132
|
+
throw dashboardError(`Campo não declarado em ${entry.name}: ${normalized}.`);
|
|
133
|
+
}
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function primitive(value) {
|
|
138
|
+
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeDashboardFilterValue(value) {
|
|
142
|
+
if (primitive(value)) return value;
|
|
143
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
144
|
+
throw dashboardError("Valor de filtro inválido.");
|
|
145
|
+
}
|
|
146
|
+
const output = {};
|
|
147
|
+
for (const [operator, operand] of Object.entries(value)) {
|
|
148
|
+
const mongoOperator = DASHBOARD_FILTER_OPERATORS[operator];
|
|
149
|
+
if (!mongoOperator) throw dashboardError(`Operador de filtro não permitido: ${operator}.`);
|
|
150
|
+
if (["in", "nin"].includes(operator)) {
|
|
151
|
+
if (!Array.isArray(operand) || !operand.every(primitive)) {
|
|
152
|
+
throw dashboardError(`${operator} exige uma lista de valores simples.`);
|
|
153
|
+
}
|
|
154
|
+
} else if (operator === "exists") {
|
|
155
|
+
if (typeof operand !== "boolean") throw dashboardError("exists exige true ou false.");
|
|
156
|
+
} else if (!primitive(operand)) {
|
|
157
|
+
throw dashboardError(`${operator} exige um valor simples.`);
|
|
158
|
+
}
|
|
159
|
+
output[mongoOperator] = operand;
|
|
160
|
+
}
|
|
161
|
+
return output;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeDashboardFilter(entry, raw = {}) {
|
|
165
|
+
if (!raw || Array.isArray(raw) || typeof raw !== "object") {
|
|
166
|
+
throw dashboardError("Filtros do Dashboard devem ser um objeto.");
|
|
167
|
+
}
|
|
168
|
+
const entries = Object.entries(raw);
|
|
169
|
+
if (entries.length > 20) throw dashboardError("O Dashboard aceita no máximo 20 filtros por indicador.");
|
|
170
|
+
return Object.fromEntries(entries.map(([field, value]) => [
|
|
171
|
+
dashboardField(entry, field, "filtro"),
|
|
172
|
+
normalizeDashboardFilterValue(value),
|
|
173
|
+
]));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function executeDashboardMetric(entry, query = {}) {
|
|
177
|
+
if (!entry?.mongooseModel) throw dashboardError("Model sem persistência para o Dashboard.");
|
|
178
|
+
const id = String(query.id || "").trim();
|
|
179
|
+
if (!id) throw dashboardError("Todo indicador do Dashboard deve possuir id.");
|
|
180
|
+
const kind = String(query.kind || "count");
|
|
181
|
+
if (!["count", "sum", "groupCount", "groupSum"].includes(kind)) {
|
|
182
|
+
throw dashboardError(`Agregação de Dashboard não permitida: ${kind}.`);
|
|
183
|
+
}
|
|
184
|
+
const filter = normalizeDashboardFilter(entry, query.filters || {});
|
|
185
|
+
|
|
186
|
+
if (kind === "count") {
|
|
187
|
+
return { id, kind, value: Number(await entry.mongooseModel.countDocuments(filter)) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const field = ["sum", "groupSum"].includes(kind)
|
|
191
|
+
? dashboardField(entry, query.field, "soma")
|
|
192
|
+
: undefined;
|
|
193
|
+
if (field) {
|
|
194
|
+
const fieldKind = entry.metaFields?.[field]?.kind;
|
|
195
|
+
if (!["number", "currency", "currencyConverted"].includes(fieldKind)) {
|
|
196
|
+
throw dashboardError(`O campo ${entry.name}.${field} não é numérico.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (kind === "sum") {
|
|
201
|
+
const [row] = await entry.mongooseModel.aggregate([
|
|
202
|
+
{ $match: filter },
|
|
203
|
+
{ $group: { _id: null, value: { $sum: { $ifNull: [`$${field}`, 0] } } } },
|
|
204
|
+
]);
|
|
205
|
+
return { id, kind, value: Number(row?.value || 0) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const groupBy = dashboardField(entry, query.groupBy, "agrupamento");
|
|
209
|
+
const accumulator = kind === "groupSum"
|
|
210
|
+
? { $sum: { $ifNull: [`$${field}`, 0] } }
|
|
211
|
+
: { $sum: 1 };
|
|
212
|
+
const rows = await entry.mongooseModel.aggregate([
|
|
213
|
+
{ $match: filter },
|
|
214
|
+
{ $group: { _id: `$${groupBy}`, value: accumulator } },
|
|
215
|
+
{ $sort: { _id: 1 } },
|
|
216
|
+
]);
|
|
217
|
+
return {
|
|
218
|
+
id,
|
|
219
|
+
kind,
|
|
220
|
+
groups: rows.map((row) => ({
|
|
221
|
+
key: row._id == null || row._id === "" ? "Sem status" : String(row._id),
|
|
222
|
+
value: Number(row.value || 0),
|
|
223
|
+
})),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function canReadDashboardModel(req, entry) {
|
|
228
|
+
const roles = entry.definition?.crud?.roles?.read || [];
|
|
229
|
+
if (!roles.length) return true;
|
|
230
|
+
const role = req.user?.tipo || req.user?.role;
|
|
231
|
+
return roles.includes(role);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
router.post("/dashboard/query", async (req, res, next) => {
|
|
235
|
+
try {
|
|
236
|
+
const queries = req.body?.queries;
|
|
237
|
+
if (!Array.isArray(queries) || !queries.length) {
|
|
238
|
+
throw dashboardError("Informe ao menos um indicador do Dashboard.", 400);
|
|
239
|
+
}
|
|
240
|
+
if (queries.length > 40) throw dashboardError("O Dashboard aceita no máximo 40 indicadores.", 400);
|
|
241
|
+
const ids = queries.map((query) => String(query?.id || ""));
|
|
242
|
+
if (new Set(ids).size !== ids.length) throw dashboardError("Ids de indicadores não podem se repetir.", 400);
|
|
243
|
+
|
|
244
|
+
const results = await Promise.all(queries.map(async (query) => {
|
|
245
|
+
const entry = registry.getModel(query.model);
|
|
246
|
+
if (!entry) throw dashboardError(`Model não registrada: ${query.model || "não informada"}.`, 404);
|
|
247
|
+
if (!canReadDashboardModel(req, entry)) {
|
|
248
|
+
throw dashboardError(`Sem permissão de leitura para ${entry.name}.`, 403);
|
|
249
|
+
}
|
|
250
|
+
return executeDashboardMetric(entry, query);
|
|
251
|
+
}));
|
|
252
|
+
res.json({ results });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
next(error);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
108
258
|
router.get("/metadata", (_req, res) => {
|
|
109
259
|
res.json({
|
|
110
260
|
service: { name: ctx.config.serviceName, version: ctx.config.serviceVersion },
|
|
@@ -152,3 +302,5 @@ router.get("/features", (_req, res) => res.json({ results: registry.listFeatures
|
|
|
152
302
|
|
|
153
303
|
module.exports = router;
|
|
154
304
|
module.exports.runtimeSummary = runtimeSummary;
|
|
305
|
+
module.exports.executeDashboardMetric = executeDashboardMetric;
|
|
306
|
+
module.exports.normalizeDashboardFilter = normalizeDashboardFilter;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const SENSITIVE_KEY = /(authorization|cookie|password|passwd|secret|token|api[-_]?key|app[-_]?key|app[-_]?secret|private[-_]?key|credential|senha)/i;
|
|
4
4
|
const DEFAULTS = {
|
|
5
|
-
maxDepth:
|
|
5
|
+
maxDepth: 12,
|
|
6
6
|
maxArrayItems: 20,
|
|
7
7
|
maxObjectKeys: 100,
|
|
8
8
|
maxStringLength: 4000,
|
|
@@ -106,6 +106,17 @@ function wait(ms) {
|
|
|
106
106
|
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
async function resolveCallParam(callDefinition, input = {}, context = {}) {
|
|
110
|
+
const declared = typeof callDefinition.param === "function"
|
|
111
|
+
? await callDefinition.param({ input, context, cursor: context.cursor })
|
|
112
|
+
: callDefinition.param;
|
|
113
|
+
return resolveTemplate(declared ?? [{}], {
|
|
114
|
+
input,
|
|
115
|
+
context,
|
|
116
|
+
cursor: context.cursor,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
function createOmieClient(options = {}) {
|
|
110
121
|
const credentials = options.credentials || {};
|
|
111
122
|
const appKey = String(credentials.appKey || "").trim();
|
|
@@ -148,11 +159,7 @@ function createOmieClient(options = {}) {
|
|
|
148
159
|
code: "OMIE_CALL_MISSING",
|
|
149
160
|
});
|
|
150
161
|
}
|
|
151
|
-
const resolvedParam =
|
|
152
|
-
input,
|
|
153
|
-
context,
|
|
154
|
-
cursor: context.cursor,
|
|
155
|
-
});
|
|
162
|
+
const resolvedParam = await resolveCallParam(callDefinition, input, context);
|
|
156
163
|
const body = {
|
|
157
164
|
call,
|
|
158
165
|
app_key: appKey,
|
|
@@ -247,6 +254,7 @@ module.exports = {
|
|
|
247
254
|
createOmieClient,
|
|
248
255
|
getPath,
|
|
249
256
|
normalizeEndpoint,
|
|
257
|
+
resolveCallParam,
|
|
250
258
|
resolveTemplate,
|
|
251
259
|
responseError,
|
|
252
260
|
};
|