@oondemand/oon-core-back 0.3.74 → 0.3.76

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.
@@ -1,5 +1,6 @@
1
1
  const Helpers = require("../utils/helpers");
2
2
  const registry = require("../registry");
3
+ const { scopeMutation } = require("../tenancy");
3
4
  const {
4
5
  prepareProcessMutation,
5
6
  assertAtomicInvariants,
@@ -38,13 +39,27 @@ function createCrudController(entry, service) {
38
39
  if (validate) await validate(data, context);
39
40
  };
40
41
 
41
- const prepareCreate = async (data, method = "post", session = null) => {
42
- const prepared = await prepareProcessMutation(entry, {}, data, {
42
+ const prepareCreate = async (
43
+ data,
44
+ method = "post",
45
+ session = null,
46
+ accessContext = undefined,
47
+ ) => {
48
+ const scopedData = scopeMutation(entry, data, accessContext);
49
+ const prepared = await prepareProcessMutation(entry, {}, scopedData, {
43
50
  op: "create",
44
51
  session,
52
+ accessContext,
45
53
  });
46
54
 
47
- await assertReferencePolicies(entry, prepared.consolidated, { session });
55
+ // Reaplica o escopo depois das regras para impedir que fórmula/hook de
56
+ // domínio substitua tenantId/usuarioId confiável. O controller usa o
57
+ // contrato público de tenancy e não exige helpers extras em services
58
+ // customizados/legados.
59
+ prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
60
+ prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
61
+
62
+ await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
48
63
  await runCustomValidation(prepared.consolidated, {
49
64
  op: "create",
50
65
  method,
@@ -54,14 +69,24 @@ function createCrudController(entry, service) {
54
69
  consolidated: prepared.consolidated,
55
70
  changedFields: prepared.changedFields,
56
71
  session,
72
+ accessContext,
57
73
  });
58
74
  return prepared;
59
75
  };
60
76
 
61
- const prepareUpdate = async (id, changes, method, session = null) => {
77
+ const prepareUpdate = async (
78
+ id,
79
+ changes,
80
+ method,
81
+ session = null,
82
+ accessContext = undefined,
83
+ ) => {
84
+ const scopedChanges = scopeMutation(entry, changes, accessContext);
62
85
  const validate = registry.getValidation(entry.name);
63
86
  if (!validate && !hasDomainRules(entry) && !hasProcessRules(entry)) {
64
- const normalizedChanges = changes && typeof changes === "object" ? { ...changes } : {};
87
+ const normalizedChanges = scopedChanges && typeof scopedChanges === "object"
88
+ ? { ...scopedChanges }
89
+ : {};
65
90
  return {
66
91
  current: null,
67
92
  requestedChanges: normalizedChanges,
@@ -71,14 +96,17 @@ function createCrudController(entry, service) {
71
96
  };
72
97
  }
73
98
 
74
- const currentRecord = await service.getById(id, { session });
99
+ const currentRecord = await service.getById(id, { session, accessContext });
75
100
  const current = toPlainRecord(currentRecord);
76
- const prepared = await prepareProcessMutation(entry, current, changes, {
101
+ const prepared = await prepareProcessMutation(entry, current, scopedChanges, {
77
102
  op: "update",
78
103
  session,
104
+ accessContext,
79
105
  });
106
+ prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
107
+ prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
80
108
 
81
- await assertReferencePolicies(entry, prepared.consolidated, { session });
109
+ await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
82
110
  if (validate) {
83
111
  await validate(prepared.consolidated, {
84
112
  op: "update",
@@ -90,30 +118,33 @@ function createCrudController(entry, service) {
90
118
  consolidated: prepared.consolidated,
91
119
  changedFields: prepared.changedFields,
92
120
  session,
121
+ accessContext,
93
122
  });
94
123
  }
95
124
 
96
125
  return { current, ...prepared };
97
126
  };
98
127
 
99
- async function createOne(data, method, session) {
100
- const prepared = await prepareCreate(data, method, session);
128
+ async function createOne(data, method, session, accessContext) {
129
+ const prepared = await prepareCreate(data, method, session, accessContext);
101
130
  await assertAtomicInvariants(entry, prepared.consolidated, null, session);
102
- const record = await service.create(prepared.changes, { session });
131
+ const record = await service.create(prepared.changes, { session, accessContext });
103
132
  await recalculateDependents(entry, null, toPlainRecord(record), {
104
133
  session,
105
134
  changedFields: prepared.changedFields,
135
+ accessContext,
106
136
  });
107
137
  return record;
108
138
  }
109
139
 
110
- async function updateOne(id, changes, method, session) {
111
- const prepared = await prepareUpdate(id, changes, method, session);
140
+ async function updateOne(id, changes, method, session, accessContext) {
141
+ const prepared = await prepareUpdate(id, changes, method, session, accessContext);
112
142
  await assertAtomicInvariants(entry, prepared.consolidated, prepared.current, session);
113
- const record = await service.update(id, prepared.changes, { session });
143
+ const record = await service.update(id, prepared.changes, { session, accessContext });
114
144
  await recalculateDependents(entry, prepared.current, toPlainRecord(record), {
115
145
  session,
116
146
  changedFields: prepared.changedFields,
147
+ accessContext,
117
148
  });
118
149
  return record;
119
150
  }
@@ -127,19 +158,27 @@ function createCrudController(entry, service) {
127
158
  searchTerm,
128
159
  sort,
129
160
  filtros,
161
+ accessContext: req.accessContext,
130
162
  });
131
163
  Helpers.sendPaginatedResponse({ res, statusCode: 200, results, pagination });
132
164
  },
133
165
 
134
166
  async getById(req, res) {
135
- const registro = await service.getById(req.params.id);
167
+ const registro = await service.getById(req.params.id, {
168
+ accessContext: req.accessContext,
169
+ });
136
170
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
137
171
  },
138
172
 
139
173
  async create(req, res) {
140
174
  const registro = await withProcessTransaction(
141
175
  entry,
142
- (session) => createOne(req.body ?? {}, "post", session),
176
+ (session) => createOne(
177
+ req.body ?? {},
178
+ "post",
179
+ session,
180
+ req.accessContext,
181
+ ),
143
182
  );
144
183
  Helpers.sendResponse({ res, statusCode: 201, [entry.singular]: registro });
145
184
  },
@@ -147,7 +186,13 @@ function createCrudController(entry, service) {
147
186
  async update(req, res) {
148
187
  const registro = await withProcessTransaction(
149
188
  entry,
150
- (session) => updateOne(req.params.id, req.body ?? {}, "put", session),
189
+ (session) => updateOne(
190
+ req.params.id,
191
+ req.body ?? {},
192
+ "put",
193
+ session,
194
+ req.accessContext,
195
+ ),
151
196
  );
152
197
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
153
198
  },
@@ -155,20 +200,36 @@ function createCrudController(entry, service) {
155
200
  async patch(req, res) {
156
201
  const registro = await withProcessTransaction(
157
202
  entry,
158
- (session) => updateOne(req.params.id, req.body ?? {}, "patch", session),
203
+ (session) => updateOne(
204
+ req.params.id,
205
+ req.body ?? {},
206
+ "patch",
207
+ session,
208
+ req.accessContext,
209
+ ),
159
210
  );
160
211
  Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
161
212
  },
162
213
 
163
214
  async remove(req, res) {
164
215
  const registro = await withProcessTransaction(entry, async (session) => {
165
- const currentRecord = await service.getById(req.params.id, { session });
216
+ const currentRecord = await service.getById(req.params.id, {
217
+ session,
218
+ accessContext: req.accessContext,
219
+ });
166
220
  const current = toPlainRecord(currentRecord);
167
- await assertDeleteAllowed(entry, req.params.id, { session });
168
- const removed = await service.remove(req.params.id, { session });
221
+ await assertDeleteAllowed(entry, req.params.id, {
222
+ session,
223
+ accessContext: req.accessContext,
224
+ });
225
+ const removed = await service.remove(req.params.id, {
226
+ session,
227
+ accessContext: req.accessContext,
228
+ });
169
229
  await recalculateDependents(entry, current, null, {
170
230
  session,
171
231
  changedFields: Object.keys(current),
232
+ accessContext: req.accessContext,
172
233
  });
173
234
  return removed;
174
235
  });
@@ -178,16 +239,22 @@ function createCrudController(entry, service) {
178
239
  async importMany(req, res) {
179
240
  const rows = req.body?.rows || req.body;
180
241
  if (!Array.isArray(rows)) {
181
- const result = await service.importMany(rows);
242
+ const result = await service.importMany(rows, {
243
+ accessContext: req.accessContext,
244
+ });
182
245
  Helpers.sendResponse({ res, statusCode: 201, ...result });
183
246
  return;
184
247
  }
185
248
 
186
249
  if (!hasProcessRules(entry)) {
187
250
  const preparedRows = await Promise.all(
188
- rows.map(async (row) => (await prepareCreate(row, "import")).changes),
251
+ rows.map(async (row) => (
252
+ await prepareCreate(row, "import", null, req.accessContext)
253
+ ).changes),
189
254
  );
190
- const result = await service.importMany(preparedRows);
255
+ const result = await service.importMany(preparedRows, {
256
+ accessContext: req.accessContext,
257
+ });
191
258
  Helpers.sendResponse({ res, statusCode: 201, ...result });
192
259
  return;
193
260
  }
@@ -195,7 +262,7 @@ function createCrudController(entry, service) {
195
262
  let inserted = 0;
196
263
  for (const row of rows) {
197
264
  await withProcessTransaction(entry, async (session) => {
198
- await createOne(row, "import", session);
265
+ await createOne(row, "import", session, req.accessContext);
199
266
  inserted += 1;
200
267
  });
201
268
  }
@@ -203,7 +270,9 @@ function createCrudController(entry, service) {
203
270
  },
204
271
 
205
272
  async exportAll(req, res) {
206
- const results = await service.exportAll(req.query);
273
+ const results = await service.exportAll(req.query, {
274
+ accessContext: req.accessContext,
275
+ });
207
276
  Helpers.sendResponse({ res, statusCode: 200, results });
208
277
  },
209
278
  };
@@ -214,4 +283,4 @@ module.exports = {
214
283
  toPlainRecord,
215
284
  hasDomainRules,
216
285
  hasProcessRules,
217
- };
286
+ };
@@ -1,5 +1,12 @@
1
1
  const GenericError = require("../errors/GenericError");
2
2
  const registry = require("../registry");
3
+ const {
4
+ dataScope,
5
+ scopeFilter,
6
+ mergeScopedFilter,
7
+ scopeMutation,
8
+ scopedIdFilter,
9
+ } = require("../tenancy");
3
10
 
4
11
  const COMMON_REFERENCE_LABEL_FIELDS = [
5
12
  "nome",
@@ -15,7 +22,7 @@ const COMMON_REFERENCE_LABEL_FIELDS = [
15
22
  "sigla",
16
23
  ];
17
24
 
18
- function referencePopulateSpecs(entry) {
25
+ function referencePopulateSpecs(entry, accessContext) {
19
26
  const setting = entry.definition?.crud?.populateRefs;
20
27
  if (!setting) return [];
21
28
 
@@ -35,10 +42,15 @@ function referencePopulateSpecs(entry) {
35
42
  ...COMMON_REFERENCE_LABEL_FIELDS,
36
43
  ...(referencedEntry?.searchableFields || []),
37
44
  ];
45
+ let match;
46
+ if (referencedEntry && dataScope(referencedEntry) !== "legacy") {
47
+ match = scopeFilter(referencedEntry, accessContext);
48
+ }
38
49
 
39
50
  return {
40
51
  path: field.name,
41
52
  select: [...new Set(select)].join(" "),
53
+ ...(match && Object.keys(match).length ? { match } : {}),
42
54
  };
43
55
  });
44
56
  }
@@ -62,21 +74,47 @@ function withSession(query, session) {
62
74
  return session && query && typeof query.session === "function" ? query.session(session) : query;
63
75
  }
64
76
 
77
+ function scopeDenied() {
78
+ return new GenericError("Registro fora do contexto de acesso atual.", {
79
+ statusCode: 403,
80
+ code: "TENANT_SCOPE_DENIED",
81
+ });
82
+ }
83
+
84
+ function notFound() {
85
+ return new GenericError("Registro não encontrado.", { statusCode: 404 });
86
+ }
87
+
65
88
  function createCrudService(entry) {
66
89
  const { mongooseModel: Model, searchableFields } = entry;
67
- const populateSpecs = referencePopulateSpecs(entry);
68
90
 
69
- async function listPaginated({ pageIndex = 0, pageSize = 20, searchTerm, sort, filtros = {} } = {}) {
91
+ async function assertScopedMiss(id, accessContext, session) {
92
+ const scope = dataScope(entry);
93
+ if (scope === "legacy" || scope === "system") throw notFound();
94
+ const exists = await withSession(Model.exists({ _id: id }), session);
95
+ if (exists) throw scopeDenied();
96
+ throw notFound();
97
+ }
98
+
99
+ async function listPaginated({
100
+ pageIndex = 0,
101
+ pageSize = 20,
102
+ searchTerm,
103
+ sort,
104
+ filtros = {},
105
+ accessContext,
106
+ } = {}) {
70
107
  const page = Math.max(0, Number(pageIndex) || 0);
71
108
  const limit = Math.min(200, Math.max(1, Number(pageSize) || 20));
72
109
 
73
- const query = { ...cleanFilters(filtros) };
110
+ const query = mergeScopedFilter(entry, cleanFilters(filtros), accessContext);
74
111
  if (searchTerm && searchableFields.length) {
75
112
  const rx = new RegExp(escapeRegex(searchTerm), "i");
76
113
  query.$or = searchableFields.map((f) => ({ [f]: rx }));
77
114
  }
78
115
 
79
116
  const sortSpec = parseSort(sort);
117
+ const populateSpecs = referencePopulateSpecs(entry, accessContext);
80
118
  const listQuery = populateQuery(
81
119
  Model.find(query).sort(sortSpec).skip(page * limit).limit(limit),
82
120
  populateSpecs
@@ -98,32 +136,48 @@ function createCrudService(entry) {
98
136
  }
99
137
 
100
138
  async function getById(id, options = {}) {
101
- const query = withSession(Model.findById(id), options.session);
139
+ const populateSpecs = referencePopulateSpecs(entry, options.accessContext);
140
+ const query = withSession(
141
+ Model.findOne(scopedIdFilter(entry, id, options.accessContext)),
142
+ options.session,
143
+ );
102
144
  const doc = await resolvePopulated(query, populateSpecs);
103
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
145
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
104
146
  return doc;
105
147
  }
106
148
 
107
149
  async function create(data, options = {}) {
108
- const doc = new Model(data);
150
+ const scopedData = scopeMutation(entry, data, options.accessContext);
151
+ const doc = new Model(scopedData);
109
152
  await doc.save({ session: options.session || undefined });
110
- return resolvePopulated(doc, populateSpecs);
153
+ return resolvePopulated(doc, referencePopulateSpecs(entry, options.accessContext));
111
154
  }
112
155
 
113
156
  async function update(id, data, options = {}) {
114
- const query = Model.findByIdAndUpdate(id, data, {
115
- new: true,
116
- runValidators: true,
117
- session: options.session || undefined,
118
- });
119
- const doc = await resolvePopulated(query, populateSpecs);
120
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
157
+ const scopedData = scopeMutation(entry, data, options.accessContext);
158
+ const query = Model.findOneAndUpdate(
159
+ scopedIdFilter(entry, id, options.accessContext),
160
+ scopedData,
161
+ {
162
+ new: true,
163
+ runValidators: true,
164
+ session: options.session || undefined,
165
+ },
166
+ );
167
+ const doc = await resolvePopulated(
168
+ query,
169
+ referencePopulateSpecs(entry, options.accessContext),
170
+ );
171
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
121
172
  return doc;
122
173
  }
123
174
 
124
175
  async function remove(id, options = {}) {
125
- const doc = await withSession(Model.findByIdAndDelete(id), options.session);
126
- if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
176
+ const doc = await withSession(
177
+ Model.findOneAndDelete(scopedIdFilter(entry, id, options.accessContext)),
178
+ options.session,
179
+ );
180
+ if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
127
181
  return doc;
128
182
  }
129
183
 
@@ -131,18 +185,38 @@ function createCrudService(entry) {
131
185
  if (!Array.isArray(rows)) {
132
186
  throw new GenericError("Esperado um array de registros.", { statusCode: 400 });
133
187
  }
134
- const docs = await Model.insertMany(rows, {
188
+ const scopedRows = rows.map((row) => scopeMutation(entry, row, options.accessContext));
189
+ const docs = await Model.insertMany(scopedRows, {
135
190
  ordered: false,
136
191
  session: options.session || undefined,
137
192
  });
138
193
  return { inserted: docs.length };
139
194
  }
140
195
 
141
- async function exportAll(filtros = {}) {
142
- return Model.find(cleanFilters(filtros)).lean();
196
+ async function exportAll(filtros = {}, options = {}) {
197
+ const query = mergeScopedFilter(entry, cleanFilters(filtros), options.accessContext);
198
+ return Model.find(query).lean();
199
+ }
200
+
201
+ function scopeQuery(filter = {}, accessContext) {
202
+ return mergeScopedFilter(entry, filter, accessContext);
203
+ }
204
+
205
+ function scopedData(data = {}, accessContext) {
206
+ return scopeMutation(entry, data, accessContext);
143
207
  }
144
208
 
145
- return { listPaginated, getById, create, update, remove, importMany, exportAll };
209
+ return {
210
+ listPaginated,
211
+ getById,
212
+ create,
213
+ update,
214
+ remove,
215
+ importMany,
216
+ exportAll,
217
+ scopeQuery,
218
+ scopedData,
219
+ };
146
220
  }
147
221
 
148
222
  function cleanFilters(filtros) {
@@ -163,4 +237,9 @@ function escapeRegex(str) {
163
237
  return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
164
238
  }
165
239
 
166
- module.exports = { createCrudService, referencePopulateSpecs, resolvePopulated };
240
+ module.exports = {
241
+ createCrudService,
242
+ referencePopulateSpecs,
243
+ resolvePopulated,
244
+ cleanFilters,
245
+ };
@@ -2,16 +2,14 @@ const express = require("express");
2
2
  const { asyncHandler } = require("../utils/helpers");
3
3
  const auth = require("../middlewares/auth");
4
4
  const { rbac } = require("../middlewares/rbac");
5
+ const { requirePermission } = require("../middlewares/permissions");
5
6
  const { mutationAudit } = require("../middlewares/mutationAudit");
6
7
  const { createCrudService } = require("./crudService");
7
8
  const { createCrudController } = require("./crudController");
8
9
 
9
10
  /**
10
- * Monta o router de CRUD de uma model. Todas as rotas são privadas
11
- * (auth obrigatório). Mutações passam por RBAC + mutationAudit.
12
- *
13
- * Endpoints (Seção 2.9): GET list, GET :id, POST, PUT :id, PATCH :id,
14
- * DELETE :id, POST /import, GET /export.
11
+ * Monta o router de CRUD de uma model. `crud.permissions` é o contrato
12
+ * canônico; `crud.roles` permanece como fallback para Centrais legadas.
15
13
  */
16
14
  function buildModelRouter(entry) {
17
15
  const router = express.Router();
@@ -20,43 +18,48 @@ function buildModelRouter(entry) {
20
18
 
21
19
  const crud = entry.definition.crud || {};
22
20
  const roles = crud.roles || {};
23
- const readRoles = roles.read || [];
24
- const writeRoles = roles.write || [];
21
+ const permissions = crud.permissions || {};
22
+ const readGuard = permissions.read
23
+ ? requirePermission(permissions.read)
24
+ : rbac(roles.read || []);
25
+ const writeGuard = permissions.write
26
+ ? requirePermission(permissions.write)
27
+ : rbac(roles.write || []);
25
28
  const entidade = entry.name;
26
29
 
27
30
  router.use(auth);
28
31
 
29
- router.get("/", rbac(readRoles), asyncHandler(controller.list));
30
- router.get("/export", rbac(readRoles), asyncHandler(controller.exportAll));
31
- router.get("/:id", rbac(readRoles), asyncHandler(controller.getById));
32
+ router.get("/", readGuard, asyncHandler(controller.list));
33
+ router.get("/export", readGuard, asyncHandler(controller.exportAll));
34
+ router.get("/:id", readGuard, asyncHandler(controller.getById));
32
35
 
33
36
  router.post(
34
37
  "/import",
35
- rbac(writeRoles),
38
+ writeGuard,
36
39
  mutationAudit({ entidade, acao: "adicionado" }),
37
40
  asyncHandler(controller.importMany)
38
41
  );
39
42
  router.post(
40
43
  "/",
41
- rbac(writeRoles),
44
+ writeGuard,
42
45
  mutationAudit({ entidade, acao: "adicionado" }),
43
46
  asyncHandler(controller.create)
44
47
  );
45
48
  router.put(
46
49
  "/:id",
47
- rbac(writeRoles),
50
+ writeGuard,
48
51
  mutationAudit({ entidade, acao: "alterado" }),
49
52
  asyncHandler(controller.update)
50
53
  );
51
54
  router.patch(
52
55
  "/:id",
53
- rbac(writeRoles),
56
+ writeGuard,
54
57
  mutationAudit({ entidade, acao: "alterado" }),
55
58
  asyncHandler(controller.patch)
56
59
  );
57
60
  router.delete(
58
61
  "/:id",
59
- rbac(writeRoles),
62
+ writeGuard,
60
63
  mutationAudit({ entidade, acao: "excluido" }),
61
64
  asyncHandler(controller.remove)
62
65
  );