@oondemand/oon-core-back 0.7.8 → 0.7.9

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/README.md CHANGED
@@ -128,3 +128,8 @@ provedor de aplicativos.
128
128
  Publicar primeiro o suporte da Central (consulta e prova de identidade), depois
129
129
  lançar front/back do SDK juntos e atualizar consumidores. O bloqueio de integração
130
130
  comercial permanece até C3a/C3b; C2 não executa cutover nem reenvia convites.
131
+
132
+
133
+ ## Contenção MongoDB
134
+
135
+ O backend governa o pool e os timeouts dos métodos Mongoose cobertos. Veja a [política distribuída](../create-central-oon/docs/DATABASE_CONTAINMENT.md) e as [evidências/gates de adoção](../../docs/database-containment-validation.md). A release precisa ser adotada e republicada por cada consumidor; não fornece isolamento físico nem limite global do cluster.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const express = require("express");
4
- const mongoose = require("mongoose");
4
+ const mongoose = require("../core/database/mongoose");
5
5
  const ctx = require("../core/context");
6
6
  const { runtimeSummary } = require("./core.routes");
7
7
  const { isLocalRuntime, localRuntimeSummary } = require("../core/runtime/localDevelopment");
@@ -79,3 +79,4 @@ module.exports = router;
79
79
  module.exports.databaseState = databaseState;
80
80
  module.exports.readinessSummary = readinessSummary;
81
81
  module.exports.versionSummary = versionSummary;
82
+
@@ -1,5 +1,8 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../core/database/mongoose");
2
2
  const registry = require("../core/registry");
3
+ const ctx = require("../core/context");
4
+ const { DEFAULTS, poolOptions } = require("../core/database/policy");
5
+ const { attachPoolMetrics } = require("../core/database/metrics");
3
6
 
4
7
  function buildMongoUri() {
5
8
  const cfg = {
@@ -20,7 +23,8 @@ function buildMongoUri() {
20
23
  }
21
24
 
22
25
  function connectionOptions() {
23
- if (process.env.MONGO_URI) return {};
26
+ const pool = poolOptions();
27
+ if (process.env.MONGO_URI) return pool;
24
28
 
25
29
  const hasUser = Boolean(process.env.DB_USER);
26
30
  const hasPassword = Boolean(process.env.DB_PASSWORD);
@@ -30,8 +34,8 @@ function connectionOptions() {
30
34
  }
31
35
 
32
36
  return hasUser
33
- ? { user: process.env.DB_USER, pass: process.env.DB_PASSWORD }
34
- : {};
37
+ ? { ...pool, user: process.env.DB_USER, pass: process.env.DB_PASSWORD }
38
+ : pool;
35
39
  }
36
40
 
37
41
  function databaseLabel(uri) {
@@ -71,7 +75,21 @@ async function syncDeclaredIndexes(entries = registry.listModels()) {
71
75
 
72
76
  async function connectDB() {
73
77
  const uri = buildMongoUri();
74
- await mongoose.connect(uri, connectionOptions());
78
+ const options = connectionOptions();
79
+ // Explicit driver options override conflicting URI pool options; tested with the real driver.
80
+ const connecting = mongoose.connect(uri, options);
81
+ const client = mongoose.connection.getClient();
82
+ const metrics = client && attachPoolMetrics(client, mongoose.connection, {
83
+ app: ctx.config.appCode || ctx.config.slug || process.env.APP_CODE,
84
+ environment: process.env.APP_ENVIRONMENT || process.env.NODE_ENV,
85
+ });
86
+ try {
87
+ await connecting;
88
+ } catch (error) {
89
+ metrics?.dispose();
90
+ throw error;
91
+ }
92
+ console.log("[OonCore] database policy", JSON.stringify({ ...DEFAULTS, uncoveredModels: Object.values(mongoose.models).filter(model => !model.schema[Symbol.for("oon.database.policy.schema.v1")]).length }));
75
93
  await syncDeclaredIndexes();
76
94
  console.log(`Conectado ao MongoDB (${databaseLabel(uri)})`);
77
95
  return mongoose.connection;
@@ -85,3 +103,4 @@ module.exports = {
85
103
  shouldSyncDeclaredIndexes,
86
104
  syncDeclaredIndexes,
87
105
  };
106
+
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  const crypto = require("node:crypto");
3
- const mongoose = require("mongoose");
3
+ const mongoose = require("../../core/database/mongoose");
4
4
  const ctx = require("../../core/context");
5
5
  const GenericError = require("../../core/errors/GenericError");
6
6
  const MAX_BYTES = 16384;
@@ -111,3 +111,4 @@ async function remove({ name, context, correlationId } = {}) {
111
111
  return safe(async () => { const s = resolveScope(context, name); await audit("remove", s, context, correlationId); await models().Secret.deleteOne({ _id: id(s) }); return { state: "absent" }; });
112
112
  }
113
113
  module.exports = Object.freeze({ put, status, use, remove });
114
+
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const mongoose = require("mongoose");
3
+ const mongoose = require("../../core/database/mongoose");
4
4
 
5
5
  const envelope = { keyId: String, version: Number, algorithm: String, iv: String, authTag: String, ciphertext: String };
6
6
  const common = { scopeKey: { type: String, required: true, index: true } };
@@ -19,3 +19,4 @@ function getModels() {
19
19
  }
20
20
  async function syncIndexes() { const models = getModels(); return Promise.all(Object.values(models).map((entry) => entry.syncIndexes())); }
21
21
  module.exports = { getModels, syncIndexes };
22
+
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ function databaseError(error) {
4
+ if (error?.name === "MongoWaitQueueTimeoutError") return {
5
+ code: "DATABASE_POOL_BUSY", statusCode: 503,
6
+ message: "O banco está ocupado. Aguarde antes de tentar novamente.",
7
+ };
8
+ if (error?.code === 50 && error?.name === "MongoServerError") return {
9
+ code: "DATABASE_OPERATION_TIMEOUT", statusCode: 504,
10
+ message: "A operação excedeu o tempo permitido. Consulte o estado antes de repetir uma alteração.",
11
+ };
12
+ return null;
13
+ }
14
+
15
+ module.exports = { databaseError };
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ const monitors = new WeakMap();
4
+ const connections = new WeakMap();
5
+
6
+ function observeOperationError(connection, error) {
7
+ if (error?.name === "MongoServerError" && error.code === 50) {
8
+ const monitor = connections.get(connection);
9
+ if (monitor) monitor.operationTimeout();
10
+ }
11
+ }
12
+
13
+ function safeLabel(value) {
14
+ return typeof value === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(value) ? value : "unknown";
15
+ }
16
+
17
+ function attachPoolMetrics(client, connection, { app, environment, emit = record => console.log("[OonCore] database", JSON.stringify(record)), intervalMS = 60000 } = {}) {
18
+ if (monitors.has(client)) return monitors.get(client);
19
+ const inUse = new Set();
20
+ let waiting = 0;
21
+ let counters;
22
+ const reset = () => { counters = { checkouts: 0, checkoutFailures: 0, checkoutTimeouts: 0, mongooseOperationTimeouts: 0, waitTotalMS: 0, waitMaxMS: 0, peakWaiting: waiting, peakInUse: inUse.size }; };
23
+ reset();
24
+ const key = event => `${event.address}/${event.connectionId}`;
25
+ const duration = event => {
26
+ const ms = Number.isFinite(event.durationMS) && event.durationMS >= 0 ? event.durationMS : 0;
27
+ counters.waitTotalMS += ms;
28
+ counters.waitMaxMS = Math.max(counters.waitMaxMS, ms);
29
+ };
30
+ const listeners = {
31
+ connectionCheckOutStarted() { waiting++; counters.peakWaiting = Math.max(counters.peakWaiting, waiting); },
32
+ connectionCheckedOut(event) { waiting = Math.max(0, waiting - 1); inUse.add(key(event)); counters.checkouts++; counters.peakInUse = Math.max(counters.peakInUse, inUse.size); duration(event); },
33
+ connectionCheckOutFailed(event) { waiting = Math.max(0, waiting - 1); counters.checkoutFailures++; if (event.reason === "timeout") counters.checkoutTimeouts++; duration(event); },
34
+ connectionCheckedIn(event) { inUse.delete(key(event)); },
35
+ connectionClosed(event) { inUse.delete(key(event)); },
36
+ };
37
+ for (const [event, listener] of Object.entries(listeners)) client.on(event, listener);
38
+ const snapshot = () => ({ app: safeLabel(app), environment: safeLabel(environment), pid: process.pid, intervalMS, inUse: inUse.size, waiting, ...counters });
39
+ const flush = () => {
40
+ if (counters.checkouts || counters.checkoutFailures || counters.mongooseOperationTimeouts || inUse.size || waiting) {
41
+ try { emit(snapshot()); } catch { /* Telemetry must never fail a database operation. */ }
42
+ }
43
+ reset();
44
+ };
45
+ const timer = setInterval(flush, intervalMS);
46
+ timer.unref();
47
+ let disposed = false;
48
+ const dispose = () => {
49
+ if (disposed) return;
50
+ disposed = true;
51
+ clearInterval(timer);
52
+ flush();
53
+ for (const [event, listener] of Object.entries(listeners)) client.removeListener(event, listener);
54
+ connection.removeListener("close", dispose);
55
+ monitors.delete(client);
56
+ if (connections.get(connection) === monitor) connections.delete(connection);
57
+ inUse.clear(); waiting = 0;
58
+ };
59
+ const monitor = { snapshot, flush, dispose, operationTimeout() { counters.mongooseOperationTimeouts++; } };
60
+ monitors.set(client, monitor);
61
+ connections.set(connection, monitor);
62
+ connection.once("close", dispose);
63
+ return monitor;
64
+ }
65
+
66
+ module.exports = { attachPoolMetrics, observeOperationError };
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+
3
+ const mongoose = require("mongoose");
4
+ const { boundedMaxTimeMS } = require("./policy");
5
+ const { observeOperationError } = require("./metrics");
6
+ const INSTALLED = Symbol.for("oon.database.policy.installed.v1");
7
+ const COVERED = Symbol.for("oon.database.policy.schema.v1");
8
+ const QUERY_METHODS = Object.freeze([
9
+ "find", "findOne", "countDocuments", "estimatedDocumentCount", "distinct",
10
+ "findOneAndUpdate", "findOneAndReplace", "findOneAndDelete",
11
+ "updateOne", "updateMany", "replaceOne", "deleteOne", "deleteMany",
12
+ ]);
13
+
14
+ function install(instance) {
15
+ if (instance[INSTALLED]) return;
16
+ instance[INSTALLED] = true;
17
+ instance.plugin(function databasePolicy(schema) {
18
+ if (schema[COVERED]) return;
19
+ schema[COVERED] = true;
20
+ for (const method of QUERY_METHODS) {
21
+ schema.pre(method, { query: true, document: false }, function applyQueryBudget() {
22
+ if (this.model.db === instance.connection) this.setOptions({ maxTimeMS: boundedMaxTimeMS(this.getOptions().maxTimeMS) });
23
+ });
24
+ schema.post(method, { query: true, document: false }, function observeQueryError(error, result, next) {
25
+ observeOperationError(this.model.db, error);
26
+ next(error);
27
+ });
28
+ }
29
+ schema.pre("aggregate", function applyAggregateBudget() {
30
+ if (this.model().db === instance.connection) this.option({ maxTimeMS: boundedMaxTimeMS(this.options.maxTimeMS) });
31
+ });
32
+ schema.post("aggregate", function observeAggregateError(error, result, next) {
33
+ observeOperationError(this.model().db, error);
34
+ next(error);
35
+ });
36
+ });
37
+ }
38
+
39
+ install(mongoose);
40
+ module.exports = mongoose;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ // Versioned Core policy, deliberately not an app manifest/env extension point.
4
+ // Initial calibration profile: not a claim of safe shared-cluster capacity.
5
+ const DEFAULTS = Object.freeze({
6
+ maxPoolSize: 4,
7
+ minPoolSize: 0,
8
+ maxConnecting: 2,
9
+ waitQueueTimeoutMS: 2000,
10
+ maxTimeMS: 5000,
11
+ });
12
+
13
+ function configurationError() {
14
+ const error = new Error("Política de contenção MongoDB inválida.");
15
+ error.code = "DATABASE_POLICY_INVALID";
16
+ return error;
17
+ }
18
+
19
+ function validatePolicy(input = DEFAULTS) {
20
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw configurationError();
21
+ const policy = { ...DEFAULTS, ...input };
22
+ if (Object.keys(policy).some(key => !Object.hasOwn(DEFAULTS, key))) throw configurationError();
23
+ for (const [key, value] of Object.entries(policy)) {
24
+ if (!Number.isSafeInteger(value) || value < (key === "minPoolSize" ? 0 : 1)) throw configurationError();
25
+ }
26
+ if (policy.minPoolSize > policy.maxPoolSize || policy.maxConnecting > policy.maxPoolSize) throw configurationError();
27
+ return Object.freeze(policy);
28
+ }
29
+
30
+ function poolOptions(policy = DEFAULTS) {
31
+ const { maxTimeMS, ...pool } = validatePolicy(policy);
32
+ return pool;
33
+ }
34
+
35
+ function boundedMaxTimeMS(value, ceiling = DEFAULTS.maxTimeMS) {
36
+ if (value === undefined || value === 0) return ceiling;
37
+ if (!Number.isSafeInteger(value) || value < 0) throw configurationError();
38
+ return Math.min(value, ceiling);
39
+ }
40
+
41
+ module.exports = { DEFAULTS, validatePolicy, poolOptions, boundedMaxTimeMS };
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("./database/mongoose");
2
2
  const express = require("express");
3
3
  const registry = require("./registry");
4
4
  const ctx = require("./context");
@@ -212,3 +212,4 @@ module.exports = {
212
212
  definePipeline,
213
213
  defineRoutes,
214
214
  };
215
+
@@ -5,7 +5,7 @@
5
5
  * `__meta` é consumido pelo registry para gerar metadata/OpenAPI para o
6
6
  * front, sem vazar para o schema Mongoose final.
7
7
  */
8
- const mongoose = require("mongoose");
8
+ const mongoose = require("./database/mongoose");
9
9
 
10
10
  const withMeta = (mongooseDef, meta) => ({
11
11
  ...mongooseDef,
@@ -93,3 +93,4 @@ module.exports = {
93
93
  currencyCode,
94
94
  currencyConverted,
95
95
  };
96
+
@@ -1,3 +1,4 @@
1
+ const { databaseError } = require("../database/errors");
1
2
  const helpers = require("../utils/helpers");
2
3
  const GenericError = require("../errors/GenericError");
3
4
 
@@ -74,6 +75,16 @@ const errorMiddleware = (error, req, res, next) => {
74
75
  });
75
76
  }
76
77
 
78
+ const databaseFailure = databaseError(error);
79
+ if (databaseFailure) {
80
+ const { statusCode, ...details } = databaseFailure;
81
+ return helpers.sendErrorResponse({
82
+ res, statusCode,
83
+ error: { ...details, ...(requestId ? { requestId } : {}) },
84
+ message: details.message,
85
+ });
86
+ }
87
+
77
88
  if (error?.code === 11000 || error?.name === "MongoServerError" && error?.code === 11000) {
78
89
  const details = duplicateKeyDetails(error, requestId);
79
90
  return helpers.sendErrorResponse({
@@ -135,3 +146,4 @@ const errorMiddleware = (error, req, res, next) => {
135
146
  module.exports = errorMiddleware;
136
147
  module.exports.firstDuplicateField = firstDuplicateField;
137
148
  module.exports.duplicateKeyDetails = duplicateKeyDetails;
149
+
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../database/mongoose");
2
2
 
3
3
  /**
4
4
  * Trilha de auditoria de mutações. Diferente da versão do CST, no Core
@@ -26,3 +26,4 @@ const ControleAlteracao =
26
26
  module.exports = ControleAlteracao;
27
27
  module.exports.ACOES_PADRAO = ACOES_PADRAO;
28
28
  module.exports.ORIGENS_PADRAO = ORIGENS_PADRAO;
29
+
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../database/mongoose");
2
2
 
3
3
  const statuses = ["nao_ativada", "validando", "configurando", "ativa", "suspensa", "revogada", "erro"];
4
4
  const roles = ["root", "member"];
@@ -65,3 +65,4 @@ schema.index({ singletonKey: 1 }, { unique: true });
65
65
  module.exports = mongoose.models.InstanciaEcossistema || mongoose.model("InstanciaEcossistema", schema);
66
66
  module.exports.STATUSES = statuses;
67
67
  module.exports.ROLES = roles;
68
+
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../database/mongoose");
2
2
 
3
3
  /**
4
4
  * Log de requisições mutantes. Model interna do Core — registrada
@@ -20,3 +20,4 @@ const logSchema = new mongoose.Schema(
20
20
 
21
21
  module.exports =
22
22
  mongoose.models.Log || mongoose.model("Log", logSchema);
23
+
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../database/mongoose");
2
2
 
3
3
  /**
4
4
  * Configuração singleton da Central. Credenciais funcionais pertencem às
@@ -17,3 +17,4 @@ const sistemaSchema = new mongoose.Schema(
17
17
 
18
18
  module.exports =
19
19
  mongoose.models.Sistema || mongoose.model("Sistema", sistemaSchema);
20
+
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const crypto = require("node:crypto");
4
- const mongoose = require("mongoose");
4
+ const mongoose = require("../database/mongoose");
5
5
  const ctx = require("../context");
6
6
  const GenericError = require("../errors/GenericError");
7
7
  const { encrypt, decrypt } = require("../utils/instanceActivation");
@@ -405,3 +405,4 @@ module.exports = {
405
405
  activeAdminCount,
406
406
  assertNotLastAdmin,
407
407
  };
408
+
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const crypto = require("node:crypto");
4
- const mongoose = require("mongoose");
4
+ const mongoose = require("../database/mongoose");
5
5
  const ctx = require("../context");
6
6
  const GenericError = require("../errors/GenericError");
7
7
  const { policy, roleByCode, rolePermissions } = require("../rbac/policy");
@@ -338,3 +338,4 @@ module.exports = {
338
338
  clearCookie, assertLocalRequest, localRequestBoundary, localRuntimeSummary,
339
339
  resetLocalDevelopmentForTests,
340
340
  };
341
+
@@ -1,5 +1,5 @@
1
1
  const crypto = require("node:crypto");
2
- const mongoose = require("mongoose");
2
+ const mongoose = require("../database/mongoose");
3
3
  const axios = require("axios");
4
4
  const ctx = require("../context");
5
5
  const GenericError = require("../errors/GenericError");
@@ -641,3 +641,4 @@ module.exports = {
641
641
  event,
642
642
  };
643
643
 
644
+
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  require("dotenv").config();
2
+ require("./core/database/mongoose"); // Before any Core or consumer model compilation.
2
3
 
3
4
  const { start } = require("./boot/startServer");
4
5
  const { createApp } = require("./boot/createApp");
@@ -82,3 +83,4 @@ module.exports = {
82
83
  TransactionalEmailError: capabilities.TransactionalEmailError,
83
84
  GenericError, registry,
84
85
  };
86
+
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- const mongoose = require("mongoose");
3
+ const mongoose = require("../core/database/mongoose");
4
4
  const registry = require("../core/registry");
5
5
  const {
6
6
  DomainRuleError,
@@ -588,4 +588,4 @@ module.exports = {
588
588
  recalculateTarget,
589
589
  drainProcessJobs,
590
590
  toPlainRecord,
591
- };
591
+ };
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../../core/database/mongoose");
2
2
 
3
3
  /**
4
4
  * Módulo Assistentes IA (Seção 13) — catálogo de assistentes, prompts e
@@ -31,3 +31,4 @@ function mascarar(texto, segredos = []) {
31
31
  }
32
32
 
33
33
  module.exports = { Assistente, mascarar };
34
+
@@ -1,4 +1,4 @@
1
- const mongoose = require("mongoose");
1
+ const mongoose = require("../../core/database/mongoose");
2
2
 
3
3
  /**
4
4
  * Módulo Multi-moedas (Seção 14) — cadastro/cotação/conversão. As models
@@ -29,3 +29,4 @@ async function converter({ valor, de = "BRL", para = "BRL" }) {
29
29
  }
30
30
 
31
31
  module.exports = { Moeda, converter };
32
+