@nextage/nx-frame-be 1.0.44 → 1.0.46

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.
@@ -29,6 +29,36 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
29
29
  * Get default parameters
30
30
  */
31
31
  get modelParams(): any;
32
+ /**
33
+ * The tenant scoping in force for THIS model, or `null` when there is none.
34
+ *
35
+ * Read on every call rather than resolved once in the constructor: an application
36
+ * registers its provider during boot, which may well happen after the models have been
37
+ * built, and a value captured too early would be `null` forever.
38
+ */
39
+ private get tenancy();
40
+ /**
41
+ * Constrains a filter to what this context may read. No-op without tenancy.
42
+ *
43
+ * The field is ALWAYS overwritten, never read: whatever the caller put there is a
44
+ * request, and honouring it would let a client name another tenant simply by adding a
45
+ * field to a query.
46
+ *
47
+ * @param filter the filter, mutated in place
48
+ * @param context the request context
49
+ */
50
+ protected scopeFilter(filter: FilterParams, context: NxContext): FilterParams;
51
+ /**
52
+ * Asserts this context may write the document it just read. No-op without tenancy.
53
+ *
54
+ * Separate from the read filter, and it has to be: in a `shared` collection everybody
55
+ * reads the shared documents, so a scoped read says nothing about who may change them.
56
+ *
57
+ * @param doc the stored document
58
+ * @param context the request context
59
+ * @throws when the write is not allowed
60
+ */
61
+ protected assertWritable(doc: NxObject | null, context: NxContext): void;
32
62
  /**
33
63
  *
34
64
  */
@@ -96,12 +126,21 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
96
126
  */
97
127
  get(id: string, context?: NxContext): Promise<TDoc | null>;
98
128
  /**
129
+ * Decides whether a subscriber may see THIS event payload.
130
+ *
131
+ * Being authenticated is not being entitled: with tenancy in force the payload is a
132
+ * document, and a document has an owner. Left at "any authenticated user" - as it was -
133
+ * a single channel carried every tenant's documents to every subscriber.
99
134
  *
100
135
  * @param {*} item
101
136
  * @param {*} context
102
137
  */
103
138
  checkContextPermission(item: any, context?: NxContext): any;
104
139
  /**
140
+ * Decides whether a subscriber may listen on a channel carrying THIS payload.
141
+ *
142
+ * Note the parameter is the payload itself, not an id, whatever its name suggests:
143
+ * `getChannelSubscriber` hands over `payload[payloadId]`.
105
144
  *
106
145
  * @param {*} context
107
146
  */
@@ -127,18 +166,37 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
127
166
  */
128
167
  findOneNative(params: FilterParams): Promise<mongoose.mongo.WithId<mongoose.mongo.BSON.Document> | null>;
129
168
  /**
169
+ * Scopes a read by id. Overriding this REPLACES the tenant scoping, it does not add to it.
130
170
  *
131
- * @param id
171
+ * @param filter
132
172
  * @param context
133
- * @returns
173
+ * @returns the filter the query must use - the RETURNED one, which is what `get` uses
134
174
  */
135
175
  beforeGet(filter: FilterParams, context: NxContext): Promise<FilterParams>;
136
176
  /**
177
+ * Stamps the owner and the audit on a new document.
178
+ *
179
+ * The value in the payload is a REQUEST, never an instruction: the provider decides what
180
+ * is actually written, and refuses rather than downgrade when the context is not
181
+ * entitled to the scope it asked for.
182
+ *
183
+ * Declared `async` because that refusal is a THROW, and the signature promises a promise:
184
+ * left synchronous, a caller reaching for `.catch()` would never see it, while one using
185
+ * `await` would - the same split contract that made `beforeGet` silently unsafe.
186
+ *
187
+ * Overriding this to set other fields is expected - denormalised data, defaults - but
188
+ * call `super`, or both the scoping and the audit silently stop happening for this model
189
+ * alone.
137
190
  *
138
191
  * @param {*} item
139
192
  */
140
193
  beforeCreate(item: TAttrs, context: NxContext): Promise<TAttrs>;
141
194
  /**
195
+ * Keeps an update from re-homing a document into another tenant, and stamps the audit.
196
+ *
197
+ * The owner is dropped from the payload rather than validated: an update never has a
198
+ * legitimate reason to move a document across tenants, so there is nothing to allow. The
199
+ * creation fields go the same way, for the same reason - a document is created once.
142
200
  *
143
201
  * @param {*} item
144
202
  */
@@ -149,6 +207,12 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
149
207
  */
150
208
  beforeCopy(item: TAttrs, context: NxContext): Promise<TAttrs>;
151
209
  /**
210
+ * Asserts the caller may delete this document, BEFORE it is deleted.
211
+ *
212
+ * `remove` deletes by `_id` with no filter whatsoever, and `bulkRemove` is a loop over
213
+ * it: without this check any caller deletes another tenant's documents knowing only an
214
+ * id. The document is re-read unscoped on purpose - the point is to judge its real
215
+ * owner, and a scoped read would hide the very case being guarded against.
152
216
  *
153
217
  * @param {*} id
154
218
  */
@@ -179,6 +243,7 @@ export declare class BaseModel<TAttrs, TDoc extends BaseMongoDoc, TMongoModel ex
179
243
  */
180
244
  manageFilterArrayParams(params: FilterParams): void;
181
245
  /**
246
+ * Scopes list / count / findOne / aggregate - the bulk of the read surface.
182
247
  *
183
248
  * @param {*} params
184
249
  * @param {*} context
@@ -30,6 +30,8 @@ const utils_1 = require("../utils");
30
30
  const constants_1 = require("../constants");
31
31
  const enums_1 = require("../enums");
32
32
  const errors_1 = require("../errors");
33
+ const tenancy_1 = require("./tenancy");
34
+ const audit_1 = require("./audit");
33
35
  const mongo_utils_1 = require("./mongo-utils");
34
36
  class BaseModel extends events_1.EventEmitter {
35
37
  constructor({ name, model, params, logger }) {
@@ -73,6 +75,50 @@ class BaseModel extends events_1.EventEmitter {
73
75
  return null;
74
76
  return (0, utils_1.clone)(this.defaultParams);
75
77
  }
78
+ /**
79
+ * The tenant scoping in force for THIS model, or `null` when there is none.
80
+ *
81
+ * Read on every call rather than resolved once in the constructor: an application
82
+ * registers its provider during boot, which may well happen after the models have been
83
+ * built, and a value captured too early would be `null` forever.
84
+ */
85
+ get tenancy() {
86
+ var _a;
87
+ return (0, tenancy_1.resolveTenancy)(constants_1.APP.tenancy, this.schema, (_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.tenancy);
88
+ }
89
+ /**
90
+ * Constrains a filter to what this context may read. No-op without tenancy.
91
+ *
92
+ * The field is ALWAYS overwritten, never read: whatever the caller put there is a
93
+ * request, and honouring it would let a client name another tenant simply by adding a
94
+ * field to a query.
95
+ *
96
+ * @param filter the filter, mutated in place
97
+ * @param context the request context
98
+ */
99
+ scopeFilter(filter, context) {
100
+ const tenancy = this.tenancy;
101
+ if (tenancy && filter)
102
+ filter[tenancy.provider.field] = tenancy.provider.read(tenancy.mode, context);
103
+ return filter;
104
+ }
105
+ /**
106
+ * Asserts this context may write the document it just read. No-op without tenancy.
107
+ *
108
+ * Separate from the read filter, and it has to be: in a `shared` collection everybody
109
+ * reads the shared documents, so a scoped read says nothing about who may change them.
110
+ *
111
+ * @param doc the stored document
112
+ * @param context the request context
113
+ * @throws when the write is not allowed
114
+ */
115
+ assertWritable(doc, context) {
116
+ const tenancy = this.tenancy;
117
+ if (!tenancy || !doc)
118
+ return;
119
+ if (!tenancy.provider.canWrite(tenancy.mode, doc[tenancy.provider.field], context))
120
+ throw new errors_1.ForbiddenError();
121
+ }
76
122
  /**
77
123
  *
78
124
  */
@@ -213,25 +259,50 @@ class BaseModel extends events_1.EventEmitter {
213
259
  return __awaiter(this, arguments, void 0, function* (id, context = {}) {
214
260
  if (!id)
215
261
  return null;
216
- const filter = { _id: (0, mongo_utils_1.toObjectId)(id) };
217
- yield this.beforeGet(filter, context);
262
+ // The RETURNED filter is the one that counts. This used to discard it and query with
263
+ // the object built here, which worked only because every override mutates its argument:
264
+ // one written in pure style - `return { ...filter, tenantId }`, the shape this very
265
+ // signature invites - compiled, read across every tenant, and said nothing.
266
+ const filter = yield this.beforeGet({ _id: (0, mongo_utils_1.toObjectId)(id) }, context);
218
267
  return this.mgModel.findOne(filter, null, { session: context === null || context === void 0 ? void 0 : context.session });
219
268
  });
220
269
  }
221
270
  /**
271
+ * Decides whether a subscriber may see THIS event payload.
272
+ *
273
+ * Being authenticated is not being entitled: with tenancy in force the payload is a
274
+ * document, and a document has an owner. Left at "any authenticated user" - as it was -
275
+ * a single channel carried every tenant's documents to every subscriber.
222
276
  *
223
277
  * @param {*} item
224
278
  * @param {*} context
225
279
  */
226
280
  checkContextPermission(item, context = {}) {
227
- return Promise.resolve((context.user) ? item : null);
281
+ if (!context.user)
282
+ return Promise.resolve(null);
283
+ const tenancy = this.tenancy;
284
+ if (!tenancy || !item)
285
+ return Promise.resolve(item);
286
+ const allowed = tenancy.provider.read(tenancy.mode, context);
287
+ return Promise.resolve((0, tenancy_1.matchesTenancy)(item[tenancy.provider.field], allowed) ? item : null);
228
288
  }
229
289
  /**
290
+ * Decides whether a subscriber may listen on a channel carrying THIS payload.
291
+ *
292
+ * Note the parameter is the payload itself, not an id, whatever its name suggests:
293
+ * `getChannelSubscriber` hands over `payload[payloadId]`.
230
294
  *
231
295
  * @param {*} context
232
296
  */
233
297
  checkChannelSubscriber(payloadId, context = {}) {
234
- return Promise.resolve(!!(context.user));
298
+ if (!context.user)
299
+ return Promise.resolve(false);
300
+ const tenancy = this.tenancy;
301
+ const payload = payloadId;
302
+ if (!tenancy || !payload || typeof payload !== 'object')
303
+ return Promise.resolve(true);
304
+ const allowed = tenancy.provider.read(tenancy.mode, context);
305
+ return Promise.resolve((0, tenancy_1.matchesTenancy)(payload[tenancy.provider.field], allowed));
235
306
  }
236
307
  /**
237
308
  * Get document using Moongose wrapper method
@@ -265,28 +336,63 @@ class BaseModel extends events_1.EventEmitter {
265
336
  return this.mgModel.collection.findOne(params);
266
337
  }
267
338
  /**
339
+ * Scopes a read by id. Overriding this REPLACES the tenant scoping, it does not add to it.
268
340
  *
269
- * @param id
341
+ * @param filter
270
342
  * @param context
271
- * @returns
343
+ * @returns the filter the query must use - the RETURNED one, which is what `get` uses
272
344
  */
273
345
  beforeGet(filter, context) {
274
346
  return __awaiter(this, void 0, void 0, function* () {
275
- return filter;
347
+ return this.scopeFilter(filter, context);
276
348
  });
277
349
  }
278
350
  /**
351
+ * Stamps the owner and the audit on a new document.
352
+ *
353
+ * The value in the payload is a REQUEST, never an instruction: the provider decides what
354
+ * is actually written, and refuses rather than downgrade when the context is not
355
+ * entitled to the scope it asked for.
356
+ *
357
+ * Declared `async` because that refusal is a THROW, and the signature promises a promise:
358
+ * left synchronous, a caller reaching for `.catch()` would never see it, while one using
359
+ * `await` would - the same split contract that made `beforeGet` silently unsafe.
360
+ *
361
+ * Overriding this to set other fields is expected - denormalised data, defaults - but
362
+ * call `super`, or both the scoping and the audit silently stop happening for this model
363
+ * alone.
279
364
  *
280
365
  * @param {*} item
281
366
  */
282
367
  beforeCreate(item, context) {
283
- return Promise.resolve(item);
368
+ return __awaiter(this, void 0, void 0, function* () {
369
+ var _a;
370
+ const tenancy = this.tenancy;
371
+ if (tenancy && item) {
372
+ const field = tenancy.provider.field;
373
+ item[field] = tenancy.provider.write(tenancy.mode, item[field], context);
374
+ }
375
+ if (((_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.audit) !== false)
376
+ (0, audit_1.stampAudit)(item, this.schema, context, true);
377
+ return item;
378
+ });
284
379
  }
285
380
  /**
381
+ * Keeps an update from re-homing a document into another tenant, and stamps the audit.
382
+ *
383
+ * The owner is dropped from the payload rather than validated: an update never has a
384
+ * legitimate reason to move a document across tenants, so there is nothing to allow. The
385
+ * creation fields go the same way, for the same reason - a document is created once.
286
386
  *
287
387
  * @param {*} item
288
388
  */
289
389
  beforeUpdate(item, context) {
390
+ var _a;
391
+ const tenancy = this.tenancy;
392
+ if (tenancy && item)
393
+ delete item[tenancy.provider.field];
394
+ if (((_a = this.defaultParams) === null || _a === void 0 ? void 0 : _a.audit) !== false)
395
+ (0, audit_1.stampAudit)(item, this.schema, context, false);
290
396
  return Promise.resolve(item);
291
397
  }
292
398
  /**
@@ -297,11 +403,23 @@ class BaseModel extends events_1.EventEmitter {
297
403
  return Promise.resolve(item);
298
404
  }
299
405
  /**
406
+ * Asserts the caller may delete this document, BEFORE it is deleted.
407
+ *
408
+ * `remove` deletes by `_id` with no filter whatsoever, and `bulkRemove` is a loop over
409
+ * it: without this check any caller deletes another tenant's documents knowing only an
410
+ * id. The document is re-read unscoped on purpose - the point is to judge its real
411
+ * owner, and a scoped read would hide the very case being guarded against.
300
412
  *
301
413
  * @param {*} id
302
414
  */
303
415
  beforeRemove(id, context) {
304
- return Promise.resolve(id);
416
+ return __awaiter(this, void 0, void 0, function* () {
417
+ if (this.tenancy && id) {
418
+ const doc = yield this.mgModel.findOne({ _id: (0, mongo_utils_1.toObjectId)(id) });
419
+ this.assertWritable(doc, context);
420
+ }
421
+ return id;
422
+ });
305
423
  }
306
424
  /**
307
425
  *
@@ -345,6 +463,7 @@ class BaseModel extends events_1.EventEmitter {
345
463
  }
346
464
  }
347
465
  /**
466
+ * Scopes list / count / findOne / aggregate - the bulk of the read surface.
348
467
  *
349
468
  * @param {*} params
350
469
  * @param {*} context
@@ -354,6 +473,7 @@ class BaseModel extends events_1.EventEmitter {
354
473
  var _a;
355
474
  if (!params)
356
475
  params = {};
476
+ this.scopeFilter(params, context);
357
477
  this.manageFilterArrayParams(params);
358
478
  (_a = params.rangeDate) !== null && _a !== void 0 ? _a : (params.rangeDate = {});
359
479
  let and = [];
@@ -469,6 +589,9 @@ class BaseModel extends events_1.EventEmitter {
469
589
  const itemDoc = yield this.get(id, context);
470
590
  if (!itemDoc)
471
591
  throw new errors_1.AppError('messages.notFound');
592
+ // A scoped READ is not a permission to WRITE: in a shared collection the document
593
+ // just read may well be one everybody sees and only the application tier may change.
594
+ this.assertWritable(itemDoc, context);
472
595
  let toApply = item;
473
596
  // complete merge with the freshly read document
474
597
  if (mergeItem) {
@@ -1,4 +1,5 @@
1
1
  import { NxObject } from '../interfaces';
2
+ import { TenancyMode } from '../enums';
2
3
  export type ModelDef<TDoc, TMongoModel, TNxModel> = {
3
4
  classDef: new (params: ModelData<TDoc, TMongoModel>) => TNxModel;
4
5
  name: string;
@@ -18,4 +19,20 @@ export type ModelData<TDoc, TMongoModel> = {
18
19
  export type ModelDefParams = {
19
20
  sort?: NxObject;
20
21
  CRUDEvents?: boolean;
22
+ /**
23
+ * How this collection relates to tenants.
24
+ *
25
+ * Omitted, a schema declaring the tenant field is scoped to ONE tenant: the scoping is
26
+ * opt-OUT, so silence means isolated. Say `none` to exempt a collection, and say WHY
27
+ * next to it - an unexplained exemption cannot be told apart from an oversight.
28
+ */
29
+ tenancy?: TenancyMode;
30
+ /**
31
+ * Whether `createdBy`/`createdAt`/`updatedBy`/`updatedAt` are kept automatically.
32
+ *
33
+ * Omitted, they are - but only for the fields the schema actually declares, so a model
34
+ * that never wanted an audit gets none either way. Set `false` for a collection that
35
+ * declares the fields and fills them itself, which today is nothing.
36
+ */
37
+ audit?: boolean;
21
38
  };
@@ -0,0 +1,101 @@
1
+ /*********************************************************************************************
2
+ * *
3
+ * TENANCY - the contract that lets `BaseModel` scope by tenant WITHOUT knowing what a *
4
+ * tenant is. *
5
+ * *
6
+ * Isolation used to be opt-IN: `BaseModel` scoped nothing, so a model owning a tenant *
7
+ * field was isolated only for as long as somebody remembered to override five hooks in *
8
+ * every single class. Forgetting one is silent - the collection simply answers every *
9
+ * tenant - and it is a mistake that has already been made three times over. *
10
+ * *
11
+ * Here it is opt-OUT: a model whose schema declares the tenant field is scoped by *
12
+ * default, and a model that must NOT be scoped has to say so. *
13
+ * *
14
+ * WHY A PROVIDER, AND NOT THE RULES THEMSELVES *
15
+ * *
16
+ * "Which tenant is this request" is an application question: it depends on roles, on *
17
+ * memberships, on grants - none of which this library knows, nor should. So the library *
18
+ * asks, and the application answers by registering a provider on `APP.tenancy`. *
19
+ * *
20
+ * IMPORTANT: with NO provider registered, every model behaves exactly as it did before *
21
+ * this file existed. The scoping is inert until an application opts into it, which is *
22
+ * what makes the change safe for consumers that know nothing about tenants. *
23
+ * *
24
+ *********************************************************************************************/
25
+ import { NxContext, NxObject } from '../interfaces';
26
+ import { TenancyMode } from '../enums';
27
+ /**
28
+ * What the application must answer for the library to scope on its behalf.
29
+ *
30
+ * Every method takes the mode, so one provider serves every model: the rules of a `shared`
31
+ * collection differ from those of an `own` one, and the difference belongs to the
32
+ * application, not to a second provider.
33
+ */
34
+ export interface TenancyProvider {
35
+ /** Schema field carrying the owner. */
36
+ field: string;
37
+ /**
38
+ * The value a READ must be constrained to - a plain id for `own`, typically an `$in`
39
+ * over the tenant and the shared marker for `shared`.
40
+ *
41
+ * @param mode the model's declared mode
42
+ * @param context the request context
43
+ * @returns whatever mongo should match the field against
44
+ */
45
+ read(mode: TenancyMode, context: NxContext): unknown;
46
+ /**
47
+ * The value a NEW document must carry, and the place where a write is authorised.
48
+ *
49
+ * `requested` is whatever the payload carried under the tenant field: it is a REQUEST and
50
+ * never an instruction - honour it only for a context entitled to it, and refuse
51
+ * otherwise rather than silently downgrading, which would let a client discover the rule
52
+ * by trial.
53
+ *
54
+ * @param mode the model's declared mode
55
+ * @param requested the value found in the payload, when any
56
+ * @param context the request context
57
+ * @returns the value to stamp on the document
58
+ * @throws when the context may not write the requested scope
59
+ */
60
+ write(mode: TenancyMode, requested: unknown, context: NxContext): unknown;
61
+ /**
62
+ * Whether an EXISTING document may be modified or deleted by this context.
63
+ *
64
+ * Separate from `read` on purpose: in a `shared` collection everybody reads the shared
65
+ * documents and only the application tier writes them, so a filter alone - which is a
66
+ * read - cannot express the rule.
67
+ *
68
+ * @param mode the model's declared mode
69
+ * @param current the value the stored document carries
70
+ * @param context the request context
71
+ */
72
+ canWrite(mode: TenancyMode, current: unknown, context: NxContext): boolean;
73
+ }
74
+ /**
75
+ * Reads the tenancy in force for a model, or `null` when there is none.
76
+ *
77
+ * Three conditions, all necessary: an application must have registered a provider, the
78
+ * model must not have opted out, and the schema must actually declare the field - a model
79
+ * without it has no owner to scope by.
80
+ *
81
+ * @param provider the registered provider, when any
82
+ * @param schema the model's mongoose schema definition
83
+ * @param declared the mode declared in `ModelDefParams`, when any
84
+ */
85
+ export declare function resolveTenancy(provider: TenancyProvider | undefined, schema: NxObject | undefined, declared?: TenancyMode): {
86
+ provider: TenancyProvider;
87
+ mode: TenancyMode;
88
+ } | null;
89
+ /**
90
+ * Whether a document's owner satisfies what a read allows.
91
+ *
92
+ * The allowed value is whatever `TenancyProvider.read` returned, so it is either a plain
93
+ * value or a mongo operator. Only `$in` is understood, and deliberately so: this is used
94
+ * where no query runs - filtering an event payload in memory - and quietly accepting an
95
+ * operator it cannot evaluate would let a document through unchecked. Anything else is
96
+ * refused, which fails closed.
97
+ *
98
+ * @param current the value carried by the document
99
+ * @param allowed what `read` returned for this context
100
+ */
101
+ export declare function matchesTenancy(current: unknown, allowed: unknown): boolean;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /*********************************************************************************************
3
+ * *
4
+ * TENANCY - the contract that lets `BaseModel` scope by tenant WITHOUT knowing what a *
5
+ * tenant is. *
6
+ * *
7
+ * Isolation used to be opt-IN: `BaseModel` scoped nothing, so a model owning a tenant *
8
+ * field was isolated only for as long as somebody remembered to override five hooks in *
9
+ * every single class. Forgetting one is silent - the collection simply answers every *
10
+ * tenant - and it is a mistake that has already been made three times over. *
11
+ * *
12
+ * Here it is opt-OUT: a model whose schema declares the tenant field is scoped by *
13
+ * default, and a model that must NOT be scoped has to say so. *
14
+ * *
15
+ * WHY A PROVIDER, AND NOT THE RULES THEMSELVES *
16
+ * *
17
+ * "Which tenant is this request" is an application question: it depends on roles, on *
18
+ * memberships, on grants - none of which this library knows, nor should. So the library *
19
+ * asks, and the application answers by registering a provider on `APP.tenancy`. *
20
+ * *
21
+ * IMPORTANT: with NO provider registered, every model behaves exactly as it did before *
22
+ * this file existed. The scoping is inert until an application opts into it, which is *
23
+ * what makes the change safe for consumers that know nothing about tenants. *
24
+ * *
25
+ *********************************************************************************************/
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.resolveTenancy = resolveTenancy;
28
+ exports.matchesTenancy = matchesTenancy;
29
+ const enums_1 = require("../enums");
30
+ /**
31
+ * Reads the tenancy in force for a model, or `null` when there is none.
32
+ *
33
+ * Three conditions, all necessary: an application must have registered a provider, the
34
+ * model must not have opted out, and the schema must actually declare the field - a model
35
+ * without it has no owner to scope by.
36
+ *
37
+ * @param provider the registered provider, when any
38
+ * @param schema the model's mongoose schema definition
39
+ * @param declared the mode declared in `ModelDefParams`, when any
40
+ */
41
+ function resolveTenancy(provider, schema, declared) {
42
+ if (!provider || declared === enums_1.TenancyMode.none)
43
+ return null;
44
+ if (!schema || !(provider.field in schema))
45
+ return null;
46
+ return { provider, mode: declared !== null && declared !== void 0 ? declared : enums_1.TenancyMode.own };
47
+ }
48
+ /**
49
+ * Whether a document's owner satisfies what a read allows.
50
+ *
51
+ * The allowed value is whatever `TenancyProvider.read` returned, so it is either a plain
52
+ * value or a mongo operator. Only `$in` is understood, and deliberately so: this is used
53
+ * where no query runs - filtering an event payload in memory - and quietly accepting an
54
+ * operator it cannot evaluate would let a document through unchecked. Anything else is
55
+ * refused, which fails closed.
56
+ *
57
+ * @param current the value carried by the document
58
+ * @param allowed what `read` returned for this context
59
+ */
60
+ function matchesTenancy(current, allowed) {
61
+ if (allowed && typeof allowed === 'object') {
62
+ const values = allowed.$in;
63
+ return Array.isArray(values) ? values.includes(current) : false;
64
+ }
65
+ return current === allowed;
66
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './crypto.interfaces';
2
+ export * from './crypto.constants';
2
3
  export * from './crypto.classes';
3
4
  export * from './crypto.utils';
4
5
  export * from './crypto-legacy';
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./crypto.interfaces"), exports);
18
+ __exportStar(require("./crypto.constants"), exports);
18
19
  __exportStar(require("./crypto.classes"), exports);
19
20
  __exportStar(require("./crypto.utils"), exports);
20
21
  __exportStar(require("./crypto-legacy"), exports);
@@ -59,3 +59,20 @@ export declare enum AuthIssuer {
59
59
  USER = "user",
60
60
  DEVICE = "device"
61
61
  }
62
+ /**
63
+ * How a collection relates to tenants. Declared per model in `ModelDefParams.tenancy`.
64
+ *
65
+ * - `own` every document belongs to exactly ONE tenant. The default whenever the schema
66
+ * declares the tenant field, because it is the safe reading of a tenant field:
67
+ * assuming otherwise is what leaks.
68
+ * - `shared` the collection holds BOTH documents shared by all tenants and documents owned
69
+ * by one. Reads span the two, writes do not: see `TenancyProvider.write`.
70
+ * - `none` the collection carries the field but is NOT request-scoped. Always a decision,
71
+ * never a default - state the reason next to it, because an unexplained `none`
72
+ * is indistinguishable from an oversight.
73
+ */
74
+ export declare enum TenancyMode {
75
+ own = "own",
76
+ shared = "shared",
77
+ none = "none"
78
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AuthIssuer = exports.ValueType = exports.DatePeriod = exports.ModelCrudEventType = exports.AppStatus = exports.AppMode = void 0;
3
+ exports.TenancyMode = exports.AuthIssuer = exports.ValueType = exports.DatePeriod = exports.ModelCrudEventType = exports.AppStatus = exports.AppMode = void 0;
4
4
  var AppMode;
5
5
  (function (AppMode) {
6
6
  AppMode["DEV"] = "development";
@@ -73,3 +73,21 @@ var AuthIssuer;
73
73
  AuthIssuer["USER"] = "user";
74
74
  AuthIssuer["DEVICE"] = "device";
75
75
  })(AuthIssuer || (exports.AuthIssuer = AuthIssuer = {}));
76
+ /**
77
+ * How a collection relates to tenants. Declared per model in `ModelDefParams.tenancy`.
78
+ *
79
+ * - `own` every document belongs to exactly ONE tenant. The default whenever the schema
80
+ * declares the tenant field, because it is the safe reading of a tenant field:
81
+ * assuming otherwise is what leaks.
82
+ * - `shared` the collection holds BOTH documents shared by all tenants and documents owned
83
+ * by one. Reads span the two, writes do not: see `TenancyProvider.write`.
84
+ * - `none` the collection carries the field but is NOT request-scoped. Always a decision,
85
+ * never a default - state the reason next to it, because an unexplained `none`
86
+ * is indistinguishable from an oversight.
87
+ */
88
+ var TenancyMode;
89
+ (function (TenancyMode) {
90
+ TenancyMode["own"] = "own";
91
+ TenancyMode["shared"] = "shared";
92
+ TenancyMode["none"] = "none";
93
+ })(TenancyMode || (exports.TenancyMode = TenancyMode = {}));
@@ -6,5 +6,7 @@ export * from './interfaces';
6
6
  export * from './types';
7
7
  export * from './manager';
8
8
  export * from './utils';
9
+ export * from './base/tenancy';
10
+ export * from './base/audit';
9
11
  export * from './express';
10
12
  export * from './crypto';
@@ -22,6 +22,8 @@ __exportStar(require("./interfaces"), exports);
22
22
  __exportStar(require("./types"), exports);
23
23
  __exportStar(require("./manager"), exports);
24
24
  __exportStar(require("./utils"), exports);
25
+ __exportStar(require("./base/tenancy"), exports);
26
+ __exportStar(require("./base/audit"), exports);
25
27
  // export * from './models';
26
28
  __exportStar(require("./express"), exports);
27
29
  // export * from './nx-disk-storage';
@@ -9,6 +9,7 @@ import { NxObject, NxTypeObject } from './interfaces';
9
9
  import { PubSubManager } from './manager/pubsub-manager';
10
10
  import { ShutdownManager } from './manager/shutdown-manager';
11
11
  import { CryptoManager } from './manager/crypto-manager';
12
+ import { TenancyProvider } from './base/tenancy';
12
13
  export type NxDateRage = {
13
14
  from?: Date;
14
15
  to?: Date;
@@ -32,6 +33,14 @@ export type AppData = {
32
33
  cryptoMng?: CryptoManager;
33
34
  streams: ChangeStreamData[];
34
35
  config: AppConfig;
36
+ /**
37
+ * Tenant scoping, registered by the application at boot.
38
+ *
39
+ * Left unset - as every consumer that knows nothing about tenants leaves it - the models
40
+ * behave exactly as they did before tenancy existed: the scoping is inert until somebody
41
+ * opts into it.
42
+ */
43
+ tenancy?: TenancyProvider;
35
44
  };
36
45
  export type AppConfig = {
37
46
  endpoint: AppEndpoint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextage/nx-frame-be",
3
- "version": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",