@oondemand/oon-core-back 0.7.11 → 0.7.13
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/core/define.js +32 -4
- package/src/core/factories/crudController.js +2 -2
- package/src/index.js +1 -0
- package/src/manifest/appArchitecture.js +31 -0
- package/src/manifest/appManifest.js +9 -3
- package/src/manifest/processRuntime.js +30 -16
- package/src/manifest/referencePolicies.js +2 -1
package/package.json
CHANGED
package/src/core/define.js
CHANGED
|
@@ -75,14 +75,42 @@ function addDataScopeFields(schema, definition) {
|
|
|
75
75
|
return schema;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// Natural-key uniqueness follows the same data scope as CRUD. This only
|
|
79
|
+
// declares indexes for new schemas; it never drops or migrates existing indexes.
|
|
80
|
+
function buildScopedSchema(definition) {
|
|
81
|
+
const scope = dataScope(definition), prefix = {};
|
|
82
|
+
if (scope === "tenant" || scope === "user" && (ctx.config.tenancyModel || ctx.config.ecosystem?.tenancyModel) !== "none") prefix[tenantField(definition)] = 1;
|
|
83
|
+
if (scope === "user") prefix[userField(definition)] = 1;
|
|
84
|
+
if (!Object.keys(prefix).length) return buildSchema(definition.schema, definition.options);
|
|
85
|
+
const shape = {}, indexes = [];
|
|
86
|
+
for (const [field, descriptor] of Object.entries(definition.schema)) {
|
|
87
|
+
if (descriptor && typeof descriptor === "object" && (descriptor.unique === true || descriptor.index?.unique === true)) {
|
|
88
|
+
const { unique, index, sparse, ...rest } = descriptor;
|
|
89
|
+
// Retain partial/collation/sparse semantics on the compound index.
|
|
90
|
+
const options = { ...(index && typeof index === "object" ? index : {}), unique: true, ...(sparse !== undefined ? { sparse } : {}) };
|
|
91
|
+
// A field-level name may belong to an existing global index. Use the
|
|
92
|
+
// generated compound name so adoption can retain and review both indexes.
|
|
93
|
+
delete options.name;
|
|
94
|
+
if (options.sparse) {
|
|
95
|
+
delete options.sparse;
|
|
96
|
+
const exists = { [field]: { $exists: true } };
|
|
97
|
+
options.partialFilterExpression = options.partialFilterExpression
|
|
98
|
+
? { $and: [options.partialFilterExpression, exists] } : exists;
|
|
99
|
+
}
|
|
100
|
+
shape[field] = rest;
|
|
101
|
+
indexes.push([{ ...prefix, [field]: 1 }, options]);
|
|
102
|
+
} else shape[field] = descriptor;
|
|
103
|
+
}
|
|
104
|
+
const result = buildSchema(shape, definition.options);
|
|
105
|
+
for (const [keys, options] of indexes) result.schema.index(keys, options);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
function defineModel(definition) {
|
|
79
110
|
if (!definition?.name) throw new Error("defineModel requer `name`.");
|
|
80
111
|
if (!definition?.schema) throw new Error(`defineModel("${definition.name}") requer \`schema\`.`);
|
|
81
112
|
|
|
82
|
-
const { schema, metaFields, searchableFields } =
|
|
83
|
-
definition.schema,
|
|
84
|
-
definition.options
|
|
85
|
-
);
|
|
113
|
+
const { schema, metaFields, searchableFields } = buildScopedSchema(definition);
|
|
86
114
|
// Campos de isolamento são internos: fazem parte do schema e dos índices,
|
|
87
115
|
// mas não vazam para metadata/formulários declarativos.
|
|
88
116
|
addDataScopeFields(schema, definition);
|
|
@@ -127,7 +127,7 @@ function createCrudController(entry, service) {
|
|
|
127
127
|
|
|
128
128
|
async function createOne(data, method, session, accessContext) {
|
|
129
129
|
const prepared = await prepareCreate(data, method, session, accessContext);
|
|
130
|
-
await assertAtomicInvariants(entry, prepared.consolidated, null, session);
|
|
130
|
+
await assertAtomicInvariants(entry, prepared.consolidated, null, session, accessContext);
|
|
131
131
|
const record = await service.create(prepared.changes, { session, accessContext });
|
|
132
132
|
await recalculateDependents(entry, null, toPlainRecord(record), {
|
|
133
133
|
session,
|
|
@@ -139,7 +139,7 @@ function createCrudController(entry, service) {
|
|
|
139
139
|
|
|
140
140
|
async function updateOne(id, changes, method, session, accessContext) {
|
|
141
141
|
const prepared = await prepareUpdate(id, changes, method, session, accessContext);
|
|
142
|
-
await assertAtomicInvariants(entry, prepared.consolidated, prepared.current, session);
|
|
142
|
+
await assertAtomicInvariants(entry, prepared.consolidated, prepared.current, session, accessContext);
|
|
143
143
|
const record = await service.update(id, prepared.changes, { session, accessContext });
|
|
144
144
|
await recalculateDependents(entry, prepared.current, toPlainRecord(record), {
|
|
145
145
|
session,
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const localDevelopment = require("./core/runtime/localDevelopment");
|
|
|
23
23
|
const localExecutionPolicy = require("./core/runtime/localExecutionPolicy");
|
|
24
24
|
|
|
25
25
|
module.exports = {
|
|
26
|
+
appArchitecture: require("./manifest/appArchitecture"),
|
|
26
27
|
start, createApp, activate,
|
|
27
28
|
defineCentral: define.defineCentral, defineModel: define.defineModel,
|
|
28
29
|
defineCollection: define.defineCollection, defineDocument: define.defineDocument,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Canonical #290 contract. Deployment is a derived technical projection, never
|
|
4
|
+
// a second architectural authority. This module has no commercial policy.
|
|
5
|
+
const DEPLOYMENT_BY_TENANCY = Object.freeze({ none: "singleton", single_tenant: "dedicated", multi_tenant: "shared" });
|
|
6
|
+
const AVAILABILITY_TENANCY = Object.freeze({
|
|
7
|
+
system: Object.freeze(["none"]), common: Object.freeze(["none", "multi_tenant"]),
|
|
8
|
+
self: Object.freeze(["single_tenant"]), licensed: Object.freeze(["single_tenant", "multi_tenant"]),
|
|
9
|
+
});
|
|
10
|
+
function invalid(code, field, message) {
|
|
11
|
+
throw Object.assign(new Error(message), { code, statusCode: 422, details: { field } });
|
|
12
|
+
}
|
|
13
|
+
function deriveDeploymentMode(tenancyModel) {
|
|
14
|
+
if (!Object.hasOwn(DEPLOYMENT_BY_TENANCY, tenancyModel)) invalid("APP_TENANCY_INVALID", "tenancyModel", "Modelo de tenancy inválido.");
|
|
15
|
+
return DEPLOYMENT_BY_TENANCY[tenancyModel];
|
|
16
|
+
}
|
|
17
|
+
function resolveAppArchitecture(input) {
|
|
18
|
+
const { availability, tenancyModel } = input || {};
|
|
19
|
+
const deploymentMode = deriveDeploymentMode(tenancyModel);
|
|
20
|
+
if (!Object.hasOwn(AVAILABILITY_TENANCY, availability) || !AVAILABILITY_TENANCY[availability].includes(tenancyModel)) {
|
|
21
|
+
invalid("APP_ARCHITECTURE_INVALID", "availability", "Disponibilidade e tenancy fora da matriz suportada.");
|
|
22
|
+
}
|
|
23
|
+
if (input.ecosystemRole && (input.ecosystemRole === "root") !== (availability === "system")) {
|
|
24
|
+
invalid("APP_ARCHITECTURE_INVALID", "ecosystemRole", "Somente a Central root usa availability=system.");
|
|
25
|
+
}
|
|
26
|
+
if (input.deploymentMode !== undefined && input.deploymentMode !== deploymentMode) {
|
|
27
|
+
invalid(input.deploymentMode === "hybrid" ? "APP_HYBRID_UNSUPPORTED" : "APP_DEPLOYMENT_CONTRADICTION", "deploymentMode", "Implantação legada deve concordar com tenancyModel; hybrid não é suportado.");
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze({ availability, tenancyModel, deploymentMode });
|
|
30
|
+
}
|
|
31
|
+
module.exports = { DEPLOYMENT_BY_TENANCY, AVAILABILITY_TENANCY, deriveDeploymentMode, resolveAppArchitecture };
|
|
@@ -5,6 +5,7 @@ const path = require("node:path");
|
|
|
5
5
|
const ctx = require("../core/context");
|
|
6
6
|
const corePackage = require("../../package.json");
|
|
7
7
|
const { validateRbac } = require("./rbacManifest");
|
|
8
|
+
const { deriveDeploymentMode, resolveAppArchitecture } = require("./appArchitecture");
|
|
8
9
|
|
|
9
10
|
const APP_MANIFEST_FILENAME = "central.app.json";
|
|
10
11
|
const APP_MANIFEST_SCHEMA_VERSION = 2;
|
|
@@ -13,10 +14,10 @@ const SUPPORTED_MODULES = Object.freeze([
|
|
|
13
14
|
"collections", "documents", "pipelines", "assistants", "currencies",
|
|
14
15
|
]);
|
|
15
16
|
const SUPPORTED_TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
|
|
16
|
-
const SUPPORTED_AVAILABILITIES = Object.freeze(["system", "common", "licensed"]);
|
|
17
|
+
const SUPPORTED_AVAILABILITIES = Object.freeze(["system", "common", "self", "licensed"]);
|
|
17
18
|
const SUPPORTED_ACCESS_MODES = Object.freeze(["tenant_members", "explicit_users", "authenticated_users"]);
|
|
18
19
|
const SUPPORTED_AUTHORIZATION_MODES = Object.freeze(["none", "rbac"]);
|
|
19
|
-
const SUPPORTED_DEPLOYMENT_MODES = Object.freeze(["singleton", "shared", "dedicated"
|
|
20
|
+
const SUPPORTED_DEPLOYMENT_MODES = Object.freeze(["singleton", "shared", "dedicated"]);
|
|
20
21
|
const TRANSACTIONAL_EMAIL_CAPABILITY = "core.transactional-email";
|
|
21
22
|
const EMAIL_SCOPE_POLICIES = Object.freeze(["app_only", "tenant_required", "tenant_override"]);
|
|
22
23
|
const APP_KIND_ROLE = Object.freeze({
|
|
@@ -201,7 +202,7 @@ function dimensionsFor(manifest) {
|
|
|
201
202
|
availability: manifest.availability || defaults.availability,
|
|
202
203
|
accessMode: manifest.accessMode || defaults.accessMode,
|
|
203
204
|
authorizationMode: manifest.authorizationMode || defaults.authorizationMode,
|
|
204
|
-
deploymentMode: manifest.
|
|
205
|
+
deploymentMode: deriveDeploymentMode(SUPPORTED_TENANCY_MODELS.includes(manifest.tenancyModel) ? manifest.tenancyModel : defaults.tenancyModel),
|
|
205
206
|
};
|
|
206
207
|
}
|
|
207
208
|
|
|
@@ -238,6 +239,11 @@ function validateAppManifest(input, { coreVersion = corePackage.version } = {})
|
|
|
238
239
|
validateDimension(input.deploymentMode, "deploymentMode", SUPPORTED_DEPLOYMENT_MODES, issues);
|
|
239
240
|
const dimensions = dimensionsFor(input);
|
|
240
241
|
const role = APP_KIND_ROLE[input.appKind];
|
|
242
|
+
try {
|
|
243
|
+
resolveAppArchitecture({ ...dimensions, ecosystemRole: role, ...(input.deploymentMode !== undefined ? { deploymentMode: input.deploymentMode } : {}) });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
addIssue(issues, error.details?.field || "tenancyModel", `${error.code}: ${error.message}`);
|
|
246
|
+
}
|
|
241
247
|
if (role === "root" && (
|
|
242
248
|
dimensions.tenancyModel !== "none" ||
|
|
243
249
|
dimensions.availability !== "system" ||
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const mongoose = require("../core/database/mongoose");
|
|
4
4
|
const registry = require("../core/registry");
|
|
5
|
+
const { mergeScopedFilter, scopedIdFilter, scopeFilter } = require("../core/tenancy");
|
|
5
6
|
const {
|
|
6
7
|
DomainRuleError,
|
|
7
8
|
applyDomainMutation,
|
|
@@ -13,6 +14,7 @@ const {
|
|
|
13
14
|
const MAX_AUTOMATIC_TRANSITIONS = 8;
|
|
14
15
|
const MAX_TRANSACTION_ATTEMPTS = 3;
|
|
15
16
|
let asynchronousQueue = Promise.resolve();
|
|
17
|
+
const transactionJobs = new WeakMap();
|
|
16
18
|
|
|
17
19
|
function toPlainRecord(record) {
|
|
18
20
|
if (!record || typeof record !== "object") return {};
|
|
@@ -182,20 +184,20 @@ function bindingDefault(binding) {
|
|
|
182
184
|
return binding.kind === "aggregate" ? 0 : null;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
async function loadLookupValues(binding, states, session) {
|
|
187
|
+
async function loadLookupValues(binding, states, session, accessContext) {
|
|
186
188
|
const sourceEntry = registry.getModel(binding.sourceModel);
|
|
187
189
|
if (!sourceEntry) throw new Error(`Model ${binding.sourceModel} não registrada.`);
|
|
188
190
|
const ids = [...new Set(states.map((state) => idKey(state[binding.localField])).filter(Boolean))];
|
|
189
191
|
if (!ids.length) return new Map();
|
|
190
192
|
let query = sourceEntry.mongooseModel
|
|
191
|
-
.find({ _id: { $in: ids } })
|
|
193
|
+
.find(mergeScopedFilter(sourceEntry, { _id: { $in: ids } }, accessContext))
|
|
192
194
|
.select(`_id ${binding.sourceField}`);
|
|
193
195
|
query = queryWithSession(query, session);
|
|
194
196
|
const rows = await query.lean();
|
|
195
197
|
return new Map(rows.map((row) => [idKey(row._id), row[binding.sourceField]]));
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
async function loadAggregateValues(binding, states, session) {
|
|
200
|
+
async function loadAggregateValues(binding, states, session, accessContext) {
|
|
199
201
|
const sourceEntry = registry.getModel(binding.sourceModel);
|
|
200
202
|
if (!sourceEntry) throw new Error(`Model ${binding.sourceModel} não registrada.`);
|
|
201
203
|
const parentIds = [...new Map(
|
|
@@ -216,7 +218,7 @@ async function loadAggregateValues(binding, states, session) {
|
|
|
216
218
|
default: accumulator = { $sum: { $ifNull: [`$${binding.sourceField}`, 0] } }; break;
|
|
217
219
|
}
|
|
218
220
|
let aggregate = sourceEntry.mongooseModel.aggregate([
|
|
219
|
-
{ $match: match },
|
|
221
|
+
{ $match: mergeScopedFilter(sourceEntry, match, accessContext) },
|
|
220
222
|
{ $group: { _id: `$${binding.foreignField}`, value: accumulator } },
|
|
221
223
|
]);
|
|
222
224
|
if (session && typeof aggregate.session === "function") aggregate = aggregate.session(session);
|
|
@@ -231,7 +233,7 @@ async function resolveBindingsForRecords(entry, records, options = {}) {
|
|
|
231
233
|
|
|
232
234
|
for (const binding of bindings) {
|
|
233
235
|
if (binding.kind === "lookup") {
|
|
234
|
-
const values = await loadLookupValues(binding, states, options.session);
|
|
236
|
+
const values = await loadLookupValues(binding, states, options.session, options.accessContext);
|
|
235
237
|
states.forEach((state, index) => {
|
|
236
238
|
const value = values.has(idKey(state[binding.localField]))
|
|
237
239
|
? values.get(idKey(state[binding.localField]))
|
|
@@ -243,7 +245,7 @@ async function resolveBindingsForRecords(entry, records, options = {}) {
|
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
if (binding.kind === "aggregate") {
|
|
246
|
-
const values = await loadAggregateValues(binding, states, options.session);
|
|
248
|
+
const values = await loadAggregateValues(binding, states, options.session, options.accessContext);
|
|
247
249
|
states.forEach((state, index) => {
|
|
248
250
|
const value = values.has(idKey(state._id))
|
|
249
251
|
? values.get(idKey(state._id))
|
|
@@ -341,7 +343,7 @@ function invariantError(rule, total, limit) {
|
|
|
341
343
|
});
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
async function assertAtomicInvariants(entry, candidate, current, session) {
|
|
346
|
+
async function assertAtomicInvariants(entry, candidate, current, session, accessContext) {
|
|
345
347
|
for (const rule of entry.definition?.processRules?.atomicInvariants || []) {
|
|
346
348
|
if (rule.kind !== "relatedSumLteParentField") continue;
|
|
347
349
|
const parentId = candidate?.[rule.parentLocalField];
|
|
@@ -350,7 +352,7 @@ async function assertAtomicInvariants(entry, candidate, current, session) {
|
|
|
350
352
|
if (!parentEntry) throw new Error(`Model pai ${rule.parentModel} não registrada.`);
|
|
351
353
|
|
|
352
354
|
let parentQuery = parentEntry.mongooseModel.findOneAndUpdate(
|
|
353
|
-
|
|
355
|
+
scopedIdFilter(parentEntry, parentId, accessContext),
|
|
354
356
|
{ $inc: { __oonInvariantVersion: 1 } },
|
|
355
357
|
{ new: true, session },
|
|
356
358
|
);
|
|
@@ -372,7 +374,7 @@ async function assertAtomicInvariants(entry, candidate, current, session) {
|
|
|
372
374
|
};
|
|
373
375
|
if (current?._id) match._id = { $ne: current._id };
|
|
374
376
|
let aggregate = entry.mongooseModel.aggregate([
|
|
375
|
-
{ $match: match },
|
|
377
|
+
{ $match: mergeScopedFilter(entry, match, accessContext) },
|
|
376
378
|
{ $group: { _id: null, total: { $sum: { $ifNull: [`$${rule.sourceField}`, 0] } } } },
|
|
377
379
|
]);
|
|
378
380
|
if (session && typeof aggregate.session === "function") aggregate = aggregate.session(session);
|
|
@@ -400,10 +402,10 @@ async function assertDeleteAllowed(entry, id, options = {}) {
|
|
|
400
402
|
for (const rule of entry.definition?.processRules?.deleteProtection || []) {
|
|
401
403
|
const sourceEntry = registry.getModel(rule.sourceModel);
|
|
402
404
|
if (!sourceEntry) throw new Error(`Model ${rule.sourceModel} não registrada.`);
|
|
403
|
-
let query = sourceEntry.mongooseModel.exists({
|
|
405
|
+
let query = sourceEntry.mongooseModel.exists(mergeScopedFilter(sourceEntry, {
|
|
404
406
|
[rule.foreignField]: id,
|
|
405
407
|
...compileMongoMatch(rule.match),
|
|
406
|
-
});
|
|
408
|
+
}, options.accessContext));
|
|
407
409
|
query = queryWithSession(query, options.session);
|
|
408
410
|
if (await query) {
|
|
409
411
|
throw new DomainRuleError(rule.message, {
|
|
@@ -436,15 +438,18 @@ async function withProcessTransaction(entry, work) {
|
|
|
436
438
|
try {
|
|
437
439
|
let result;
|
|
438
440
|
await session.withTransaction(async () => {
|
|
441
|
+
transactionJobs.set(session, []);
|
|
439
442
|
result = await work(session);
|
|
440
443
|
}, {
|
|
441
444
|
readConcern: { level: "snapshot" },
|
|
442
445
|
writeConcern: { w: "majority" },
|
|
443
446
|
});
|
|
447
|
+
for (const job of transactionJobs.get(session) || []) enqueueAsynchronousRecalculation(job.target, job.options);
|
|
444
448
|
return result;
|
|
445
449
|
} catch (error) {
|
|
446
450
|
if (attempt === MAX_TRANSACTION_ATTEMPTS || !isTransientTransactionError(error)) throw error;
|
|
447
451
|
} finally {
|
|
452
|
+
transactionJobs.delete(session);
|
|
448
453
|
await session.endSession();
|
|
449
454
|
}
|
|
450
455
|
}
|
|
@@ -492,7 +497,7 @@ function collectDependentTargets(sourceEntry, before, after, changedFields) {
|
|
|
492
497
|
async function recalculateTarget(entry, selectors, options = {}) {
|
|
493
498
|
const clauses = selectors.map(({ field, value }) => ({ [field]: value }));
|
|
494
499
|
if (!clauses.length) return { matched: 0, modified: 0 };
|
|
495
|
-
let query = entry.mongooseModel.find(clauses.length === 1 ? clauses[0] : { $or: clauses });
|
|
500
|
+
let query = entry.mongooseModel.find(mergeScopedFilter(entry, clauses.length === 1 ? clauses[0] : { $or: clauses }, options.accessContext));
|
|
496
501
|
query = queryWithSession(query, options.session);
|
|
497
502
|
const documents = await query.lean();
|
|
498
503
|
if (!documents.length) return { matched: 0, modified: 0 };
|
|
@@ -513,7 +518,7 @@ async function recalculateTarget(entry, selectors, options = {}) {
|
|
|
513
518
|
if (Object.keys(changed).length) {
|
|
514
519
|
operations.push({
|
|
515
520
|
updateOne: {
|
|
516
|
-
filter:
|
|
521
|
+
filter: scopedIdFilter(entry, documents[index]._id, options.accessContext),
|
|
517
522
|
update: { $set: changed },
|
|
518
523
|
// Only server-derived, validated changes reach this recalculation path.
|
|
519
524
|
// readonly still rejects client writes in applyDomainMutation.
|
|
@@ -530,10 +535,14 @@ async function recalculateTarget(entry, selectors, options = {}) {
|
|
|
530
535
|
return { matched: documents.length, modified: result.modifiedCount || 0 };
|
|
531
536
|
}
|
|
532
537
|
|
|
533
|
-
function enqueueAsynchronousRecalculation(target) {
|
|
538
|
+
function enqueueAsynchronousRecalculation(target, options) {
|
|
539
|
+
// Capture a value, never a mutable request/session or the next user's context.
|
|
540
|
+
const accessContext = options.accessContext ? Object.freeze({ ...options.accessContext,
|
|
541
|
+
permissions: Object.freeze([...(options.accessContext.permissions || [])]) }) : undefined;
|
|
542
|
+
scopeFilter(target.entry, accessContext);
|
|
534
543
|
asynchronousQueue = asynchronousQueue
|
|
535
544
|
.then(() => new Promise((resolve) => setImmediate(resolve)))
|
|
536
|
-
.then(() => recalculateTarget(target.entry, target.selectors, { session: null }))
|
|
545
|
+
.then(() => recalculateTarget(target.entry, target.selectors, { session: null, accessContext }))
|
|
537
546
|
.catch((error) => {
|
|
538
547
|
// A fila não pode morrer silenciosamente nem bloquear novas tarefas.
|
|
539
548
|
console.error("OON_PROCESS_ASYNC_RECALCULATION_FAILED", {
|
|
@@ -556,7 +565,12 @@ async function recalculateDependents(entry, before, after, options = {}) {
|
|
|
556
565
|
const results = [];
|
|
557
566
|
for (const target of targets) {
|
|
558
567
|
if (target.mode === "async") {
|
|
559
|
-
|
|
568
|
+
if (options.session?.inTransaction()) {
|
|
569
|
+
const jobs = transactionJobs.get(options.session);
|
|
570
|
+
if (!jobs) throw new DomainRuleError("Fila assíncrona exige transação gerenciada.", { code: "OON_PROCESS_TRANSACTION_REQUIRED", statusCode: 409 });
|
|
571
|
+
scopeFilter(target.entry, options.accessContext);
|
|
572
|
+
jobs.push({ target, options: { accessContext: options.accessContext ? { ...options.accessContext } : undefined } });
|
|
573
|
+
} else enqueueAsynchronousRecalculation(target, options);
|
|
560
574
|
results.push({ model: target.entry.name, mode: "async" });
|
|
561
575
|
} else {
|
|
562
576
|
results.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const registry = require("../core/registry");
|
|
4
|
+
const { mergeScopedFilter } = require("../core/tenancy");
|
|
4
5
|
const { DomainRuleError, valuesEquivalent } = require("./domainRules");
|
|
5
6
|
|
|
6
7
|
function idKey(value) {
|
|
@@ -83,7 +84,7 @@ async function assertReferencePolicies(entry, candidate, options = {}) {
|
|
|
83
84
|
|
|
84
85
|
for (const group of grouped.values()) {
|
|
85
86
|
let query = group.entry.mongooseModel
|
|
86
|
-
.find({ _id: { $in: [...group.ids.values()] } })
|
|
87
|
+
.find(mergeScopedFilter(group.entry, { _id: { $in: [...group.ids.values()] } }, options.accessContext))
|
|
87
88
|
.select(requiredProjection(group.rules.map((item) => item.rule)));
|
|
88
89
|
query = queryWithSession(query, options.session);
|
|
89
90
|
const records = await query.lean();
|