@nextage/nx-frame-be 1.0.45 → 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.
- package/build/common/base/__test__/audit.test.d.ts +15 -0
- package/build/common/base/__test__/audit.test.js +126 -0
- package/build/common/base/__test__/tenancy-opt-out.test.d.ts +21 -0
- package/build/common/base/__test__/tenancy-opt-out.test.js +214 -0
- package/build/common/base/audit.d.ts +55 -0
- package/build/common/base/audit.js +77 -0
- package/build/common/base/base.model.d.ts +67 -2
- package/build/common/base/base.model.js +127 -7
- package/build/common/base/mongo.types.d.ts +17 -0
- package/build/common/base/tenancy.d.ts +101 -0
- package/build/common/base/tenancy.js +66 -0
- package/build/common/enums.d.ts +17 -0
- package/build/common/enums.js +19 -1
- package/build/common/index.d.ts +2 -0
- package/build/common/index.js +2 -0
- package/build/common/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*********************************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* AUDIT - the library keeps `createdBy`/`createdAt`/`updatedBy`/`updatedAt` for models that *
|
|
4
|
+
* declare them, and keeps out of the way of models that do not. *
|
|
5
|
+
* *
|
|
6
|
+
* The classes below are EMPTY on purpose. Whatever is stamped has to come from the library, *
|
|
7
|
+
* so a test that passed while a model helped would prove nothing about the models that *
|
|
8
|
+
* never do - which are precisely the ones that used to be left without an audit. *
|
|
9
|
+
* *
|
|
10
|
+
* Every assertion is on the payload the hooks return, not on stored documents: the point is *
|
|
11
|
+
* what the library puts there, and mongo would only add a round trip and a way to be wrong *
|
|
12
|
+
* about it. *
|
|
13
|
+
* *
|
|
14
|
+
*********************************************************************************************/
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*********************************************************************************************
|
|
3
|
+
* *
|
|
4
|
+
* AUDIT - the library keeps `createdBy`/`createdAt`/`updatedBy`/`updatedAt` for models that *
|
|
5
|
+
* declare them, and keeps out of the way of models that do not. *
|
|
6
|
+
* *
|
|
7
|
+
* The classes below are EMPTY on purpose. Whatever is stamped has to come from the library, *
|
|
8
|
+
* so a test that passed while a model helped would prove nothing about the models that *
|
|
9
|
+
* never do - which are precisely the ones that used to be left without an audit. *
|
|
10
|
+
* *
|
|
11
|
+
* Every assertion is on the payload the hooks return, not on stored documents: the point is *
|
|
12
|
+
* what the library puts there, and mongo would only add a round trip and a way to be wrong *
|
|
13
|
+
* about it. *
|
|
14
|
+
* *
|
|
15
|
+
*********************************************************************************************/
|
|
16
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
17
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const base_model_1 = require("../base.model");
|
|
27
|
+
const mongo_utils_1 = require("../mongo-utils");
|
|
28
|
+
/** Declares all four fields, as a model wanting an audit does. */
|
|
29
|
+
class Audited extends base_model_1.BaseModel {
|
|
30
|
+
}
|
|
31
|
+
/** Declares none of them, as a model wanting no audit does. */
|
|
32
|
+
class Plain extends base_model_1.BaseModel {
|
|
33
|
+
}
|
|
34
|
+
/** Declares them and keeps them itself - the reason `audit: false` exists. */
|
|
35
|
+
class OptedOut extends base_model_1.BaseModel {
|
|
36
|
+
}
|
|
37
|
+
const auditedSchema = {
|
|
38
|
+
code: { type: String, required: true },
|
|
39
|
+
createdBy: { type: String },
|
|
40
|
+
createdAt: { type: Date },
|
|
41
|
+
updatedBy: { type: String },
|
|
42
|
+
updatedAt: { type: Date }
|
|
43
|
+
};
|
|
44
|
+
const audited = (0, mongo_utils_1.createModel)({
|
|
45
|
+
name: 'AuditAudited',
|
|
46
|
+
modelName: 'AuditAudited',
|
|
47
|
+
collection: 'auditAudited',
|
|
48
|
+
classDef: Audited,
|
|
49
|
+
modelParams: {},
|
|
50
|
+
schema: auditedSchema
|
|
51
|
+
}, true);
|
|
52
|
+
const plain = (0, mongo_utils_1.createModel)({
|
|
53
|
+
name: 'AuditPlain',
|
|
54
|
+
modelName: 'AuditPlain',
|
|
55
|
+
collection: 'auditPlain',
|
|
56
|
+
classDef: Plain,
|
|
57
|
+
modelParams: {},
|
|
58
|
+
schema: { code: { type: String, required: true } }
|
|
59
|
+
}, true);
|
|
60
|
+
const optedOut = (0, mongo_utils_1.createModel)({
|
|
61
|
+
name: 'AuditOptedOut',
|
|
62
|
+
modelName: 'AuditOptedOut',
|
|
63
|
+
collection: 'auditOptedOut',
|
|
64
|
+
classDef: OptedOut,
|
|
65
|
+
modelParams: { audit: false },
|
|
66
|
+
schema: auditedSchema
|
|
67
|
+
}, true);
|
|
68
|
+
const caller = { user: { id: 'user-1' } };
|
|
69
|
+
const other = { user: { id: 'user-2' } };
|
|
70
|
+
const anonymous = {};
|
|
71
|
+
describe('audit kept by the library', () => {
|
|
72
|
+
describe('a model that declares the fields', () => {
|
|
73
|
+
it('stamps author and moment on creation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
const item = yield audited.beforeCreate({ code: 'a' }, caller);
|
|
75
|
+
expect(item.createdBy).toBe('user-1');
|
|
76
|
+
expect(item.createdAt).toBeInstanceOf(Date);
|
|
77
|
+
}));
|
|
78
|
+
it('stamps author and moment on update, and never the creation ones', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
const item = yield audited.beforeUpdate({ code: 'a' }, other);
|
|
80
|
+
expect(item.updatedBy).toBe('user-2');
|
|
81
|
+
expect(item.updatedAt).toBeInstanceOf(Date);
|
|
82
|
+
expect(item.createdBy).toBeUndefined();
|
|
83
|
+
expect(item.createdAt).toBeUndefined();
|
|
84
|
+
}));
|
|
85
|
+
// The payload is the client's, so an audit it could write would say whatever the client
|
|
86
|
+
// wanted it to say - which is the same as having none.
|
|
87
|
+
it('ignores an author named by the payload', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const created = yield audited.beforeCreate({ code: 'a', createdBy: 'forged' }, caller);
|
|
89
|
+
const updated = yield audited.beforeUpdate({ code: 'a', updatedBy: 'forged' }, caller);
|
|
90
|
+
expect(created.createdBy).toBe('user-1');
|
|
91
|
+
expect(updated.updatedBy).toBe('user-1');
|
|
92
|
+
}));
|
|
93
|
+
// A document is created once: an update carrying the creation fields is either noise or
|
|
94
|
+
// an attempt to rewrite history, and dropping them costs nothing either way.
|
|
95
|
+
it('drops the creation fields an update tries to carry', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
const item = yield audited.beforeUpdate({ code: 'a', createdBy: 'forged', createdAt: new Date(0) }, caller);
|
|
97
|
+
expect(item.createdBy).toBeUndefined();
|
|
98
|
+
expect(item.createdAt).toBeUndefined();
|
|
99
|
+
}));
|
|
100
|
+
// Nothing to attribute, so nothing is attributed - rather than a document claiming an
|
|
101
|
+
// author that does not exist.
|
|
102
|
+
it('leaves the author out when there is no user, and still records the moment', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
103
|
+
const item = yield audited.beforeCreate({ code: 'a' }, anonymous);
|
|
104
|
+
expect(item.createdBy).toBeUndefined();
|
|
105
|
+
expect(item.createdAt).toBeInstanceOf(Date);
|
|
106
|
+
}));
|
|
107
|
+
});
|
|
108
|
+
describe('a model that declares none of them', () => {
|
|
109
|
+
// What keeps this change safe for consumers that never asked for an audit: no field
|
|
110
|
+
// appears in a collection that did not declare it.
|
|
111
|
+
it('is left untouched', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
const created = yield plain.beforeCreate({ code: 'a' }, caller);
|
|
113
|
+
const updated = yield plain.beforeUpdate({ code: 'a' }, caller);
|
|
114
|
+
expect(created).toEqual({ code: 'a' });
|
|
115
|
+
expect(updated).toEqual({ code: 'a' });
|
|
116
|
+
}));
|
|
117
|
+
});
|
|
118
|
+
describe('a model that opted out', () => {
|
|
119
|
+
it('is left untouched even though it declares the fields', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
const created = yield optedOut.beforeCreate({ code: 'a' }, caller);
|
|
121
|
+
const updated = yield optedOut.beforeUpdate({ code: 'a' }, caller);
|
|
122
|
+
expect(created).toEqual({ code: 'a' });
|
|
123
|
+
expect(updated).toEqual({ code: 'a' });
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*********************************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* TENANT SCOPING IS OPT-OUT *
|
|
4
|
+
* *
|
|
5
|
+
* These tests state the behaviour of a model that overrides NOTHING. That is the whole *
|
|
6
|
+
* point: every leak this file guards against was born the same way - a class that declared *
|
|
7
|
+
* a tenant field and forgot one of the hooks that isolate it, silently answering every *
|
|
8
|
+
* tenant from then on. *
|
|
9
|
+
* *
|
|
10
|
+
* So none of the models below has a body. What they have is a schema with a tenant field *
|
|
11
|
+
* and, where it matters, a declared mode - and the isolation has to follow from that *
|
|
12
|
+
* alone. *
|
|
13
|
+
* *
|
|
14
|
+
* Two properties are asserted together, and they pull in opposite directions on purpose: *
|
|
15
|
+
* *
|
|
16
|
+
* 1. WITH a provider registered, an unaware model is scoped anyway. *
|
|
17
|
+
* 2. WITHOUT one, nothing changes at all - the library is used by consumers that have no *
|
|
18
|
+
* notion of a tenant, and for them this file must be a no-op. *
|
|
19
|
+
* *
|
|
20
|
+
*********************************************************************************************/
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*********************************************************************************************
|
|
3
|
+
* *
|
|
4
|
+
* TENANT SCOPING IS OPT-OUT *
|
|
5
|
+
* *
|
|
6
|
+
* These tests state the behaviour of a model that overrides NOTHING. That is the whole *
|
|
7
|
+
* point: every leak this file guards against was born the same way - a class that declared *
|
|
8
|
+
* a tenant field and forgot one of the hooks that isolate it, silently answering every *
|
|
9
|
+
* tenant from then on. *
|
|
10
|
+
* *
|
|
11
|
+
* So none of the models below has a body. What they have is a schema with a tenant field *
|
|
12
|
+
* and, where it matters, a declared mode - and the isolation has to follow from that *
|
|
13
|
+
* alone. *
|
|
14
|
+
* *
|
|
15
|
+
* Two properties are asserted together, and they pull in opposite directions on purpose: *
|
|
16
|
+
* *
|
|
17
|
+
* 1. WITH a provider registered, an unaware model is scoped anyway. *
|
|
18
|
+
* 2. WITHOUT one, nothing changes at all - the library is used by consumers that have no *
|
|
19
|
+
* notion of a tenant, and for them this file must be a no-op. *
|
|
20
|
+
* *
|
|
21
|
+
*********************************************************************************************/
|
|
22
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
23
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
24
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
25
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
26
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
27
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
28
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
const constants_1 = require("../../constants");
|
|
33
|
+
const base_model_1 = require("../base.model");
|
|
34
|
+
const mongo_utils_1 = require("../mongo-utils");
|
|
35
|
+
const enums_1 = require("../../enums");
|
|
36
|
+
/** Marks a document every tenant shares, as an application would mark it. */
|
|
37
|
+
const SHARED = '__GLOBAL__';
|
|
38
|
+
/**
|
|
39
|
+
* A provider written the way an application would write one: it knows the roles, the
|
|
40
|
+
* library does not. `systemTier` stands in for the role group entitled to the shared tier.
|
|
41
|
+
*/
|
|
42
|
+
const provider = {
|
|
43
|
+
field: 'tenantId',
|
|
44
|
+
read(mode, context) {
|
|
45
|
+
const tenant = context.tenantId;
|
|
46
|
+
return (mode === enums_1.TenancyMode.shared) ? { $in: [tenant, SHARED] } : tenant;
|
|
47
|
+
},
|
|
48
|
+
write(mode, requested, context) {
|
|
49
|
+
const ctx = context;
|
|
50
|
+
if (mode === enums_1.TenancyMode.shared && requested === SHARED) {
|
|
51
|
+
if (!ctx.systemTier)
|
|
52
|
+
throw new Error('denied');
|
|
53
|
+
return SHARED;
|
|
54
|
+
}
|
|
55
|
+
return ctx.tenantId;
|
|
56
|
+
},
|
|
57
|
+
canWrite(mode, current, context) {
|
|
58
|
+
const ctx = context;
|
|
59
|
+
if (current === SHARED)
|
|
60
|
+
return !!ctx.systemTier;
|
|
61
|
+
return current === ctx.tenantId;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const schema = {
|
|
65
|
+
code: { type: String, required: true },
|
|
66
|
+
tenantId: { type: String, required: true }
|
|
67
|
+
};
|
|
68
|
+
/** No body: whatever isolates it has to come from the library. */
|
|
69
|
+
class Unaware extends base_model_1.BaseModel {
|
|
70
|
+
}
|
|
71
|
+
/** Same, in a collection that mixes the shared tier with each tenant's own additions. */
|
|
72
|
+
class Mixed extends base_model_1.BaseModel {
|
|
73
|
+
}
|
|
74
|
+
/** Same, exempted on purpose - as a collection written by event listeners must be. */
|
|
75
|
+
class Exempt extends base_model_1.BaseModel {
|
|
76
|
+
}
|
|
77
|
+
const unaware = (0, mongo_utils_1.createModel)({
|
|
78
|
+
name: 'TenancyUnaware',
|
|
79
|
+
modelName: 'TenancyUnaware',
|
|
80
|
+
collection: 'tenancyUnaware',
|
|
81
|
+
classDef: Unaware,
|
|
82
|
+
modelParams: {},
|
|
83
|
+
schema
|
|
84
|
+
}, true);
|
|
85
|
+
const mixed = (0, mongo_utils_1.createModel)({
|
|
86
|
+
name: 'TenancyMixed',
|
|
87
|
+
modelName: 'TenancyMixed',
|
|
88
|
+
collection: 'tenancyMixed',
|
|
89
|
+
classDef: Mixed,
|
|
90
|
+
modelParams: { tenancy: enums_1.TenancyMode.shared },
|
|
91
|
+
schema
|
|
92
|
+
}, true);
|
|
93
|
+
const exempt = (0, mongo_utils_1.createModel)({
|
|
94
|
+
name: 'TenancyExempt',
|
|
95
|
+
modelName: 'TenancyExempt',
|
|
96
|
+
collection: 'tenancyExempt',
|
|
97
|
+
classDef: Exempt,
|
|
98
|
+
modelParams: { tenancy: enums_1.TenancyMode.none },
|
|
99
|
+
schema
|
|
100
|
+
}, true);
|
|
101
|
+
const owner = { user: { id: 'u1' }, tenantId: 'tenant-a' };
|
|
102
|
+
const outsider = { user: { id: 'u2' }, tenantId: 'tenant-b' };
|
|
103
|
+
const system = { user: { id: 'u3' }, tenantId: 'tenant-a', systemTier: true };
|
|
104
|
+
describe('a model that overrides nothing is scoped anyway', () => {
|
|
105
|
+
beforeEach(() => { constants_1.APP.tenancy = provider; });
|
|
106
|
+
afterEach(() => { delete constants_1.APP.tenancy; });
|
|
107
|
+
it('stamps the owner on create, ignoring what the payload asked for', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
const doc = yield unaware.create({ code: 'A', tenantId: 'tenant-b' }, owner);
|
|
109
|
+
expect(doc.tenantId).toBe('tenant-a');
|
|
110
|
+
}));
|
|
111
|
+
it('does not resolve another tenant document by id', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
const doc = yield unaware.create({ code: 'A' }, owner);
|
|
113
|
+
expect(yield unaware.get(doc.id, outsider)).toBeNull();
|
|
114
|
+
expect(yield unaware.get(doc.id, owner)).not.toBeNull();
|
|
115
|
+
}));
|
|
116
|
+
it('keeps another tenant out of list, count and findOne', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
yield unaware.create({ code: 'A' }, owner);
|
|
118
|
+
expect(yield unaware.list({}, {}, {}, outsider)).toHaveLength(0);
|
|
119
|
+
expect(yield unaware.count({}, outsider)).toBe(0);
|
|
120
|
+
expect(yield unaware.findOne({ code: 'A' }, outsider)).toBeNull();
|
|
121
|
+
expect(yield unaware.list({}, {}, {}, owner)).toHaveLength(1);
|
|
122
|
+
}));
|
|
123
|
+
it('refuses to delete another tenant document', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
const doc = yield unaware.create({ code: 'A' }, owner);
|
|
125
|
+
yield expect(unaware.remove(doc.id, outsider)).rejects.toBeDefined();
|
|
126
|
+
expect(yield unaware.mgModel.collection.countDocuments({})).toBe(1);
|
|
127
|
+
}));
|
|
128
|
+
it('refuses to update another tenant document', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const doc = yield unaware.create({ code: 'A' }, owner);
|
|
130
|
+
yield expect(unaware.update(doc.id, { code: 'B' }, outsider)).rejects.toBeDefined();
|
|
131
|
+
}));
|
|
132
|
+
it('will not let an update re-home a document into another tenant', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
+
const doc = yield unaware.create({ code: 'A' }, owner);
|
|
134
|
+
const updated = yield unaware.update(doc.id, { code: 'B', tenantId: 'tenant-b' }, owner);
|
|
135
|
+
expect(updated.tenantId).toBe('tenant-a');
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
describe('a shared collection reads across tiers and writes within one', () => {
|
|
139
|
+
beforeEach(() => { constants_1.APP.tenancy = provider; });
|
|
140
|
+
afterEach(() => { delete constants_1.APP.tenancy; });
|
|
141
|
+
it('shows a tenant both the shared documents and its own', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
142
|
+
yield mixed.create({ code: 'SYS', tenantId: SHARED }, system);
|
|
143
|
+
yield mixed.create({ code: 'OWN' }, owner);
|
|
144
|
+
const seen = yield mixed.list({}, {}, {}, owner);
|
|
145
|
+
expect(seen.map(d => d.code).sort()).toEqual(['OWN', 'SYS']);
|
|
146
|
+
}));
|
|
147
|
+
it('hides another tenant additions while still showing the shared ones', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
148
|
+
yield mixed.mgModel.collection.insertOne({ code: 'SYS', tenantId: SHARED });
|
|
149
|
+
yield mixed.create({ code: 'OWN' }, owner);
|
|
150
|
+
const seen = yield mixed.list({}, {}, {}, outsider);
|
|
151
|
+
expect(seen.map(d => d.code)).toEqual(['SYS']);
|
|
152
|
+
}));
|
|
153
|
+
it('lets the system tier create a shared document', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
const doc = yield mixed.create({ code: 'SYS', tenantId: SHARED }, system);
|
|
155
|
+
expect(doc.tenantId).toBe(SHARED);
|
|
156
|
+
}));
|
|
157
|
+
it('refuses a shared document to a tenant caller, rather than quietly downgrading it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
yield expect(mixed.create({ code: 'SYS', tenantId: SHARED }, owner)).rejects.toBeDefined();
|
|
159
|
+
}));
|
|
160
|
+
// The one a filter alone cannot express: the shared document IS readable by this caller,
|
|
161
|
+
// so scoping the read is not enough to stop the write.
|
|
162
|
+
it('refuses a tenant caller the update and the removal of a shared document', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
const inserted = yield mixed.mgModel.collection.insertOne({ code: 'SYS', tenantId: SHARED });
|
|
164
|
+
const id = inserted.insertedId.toString();
|
|
165
|
+
yield expect(mixed.update(id, { code: 'HACKED' }, owner)).rejects.toBeDefined();
|
|
166
|
+
yield expect(mixed.remove(id, owner)).rejects.toBeDefined();
|
|
167
|
+
}));
|
|
168
|
+
it('lets the system tier update a shared document', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
+
const inserted = yield mixed.mgModel.collection.insertOne({ code: 'SYS', tenantId: SHARED });
|
|
170
|
+
const updated = yield mixed.update(inserted.insertedId.toString(), { code: 'FIXED' }, system);
|
|
171
|
+
expect(updated.code).toBe('FIXED');
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
describe('subscription payloads are scoped too', () => {
|
|
175
|
+
beforeEach(() => { constants_1.APP.tenancy = provider; });
|
|
176
|
+
afterEach(() => { delete constants_1.APP.tenancy; });
|
|
177
|
+
// T58: both hooks defaulted to "any authenticated user", and nothing in the project
|
|
178
|
+
// overrode them - so an event carried a document to every subscriber on the channel.
|
|
179
|
+
it('withholds another tenant document from a subscriber', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
const doc = { code: 'A', tenantId: 'tenant-a' };
|
|
181
|
+
expect(yield unaware.checkContextPermission(doc, owner)).not.toBeNull();
|
|
182
|
+
expect(yield unaware.checkContextPermission(doc, outsider)).toBeNull();
|
|
183
|
+
}));
|
|
184
|
+
it('delivers a shared document to every tenant, and a private one to its owner alone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
185
|
+
expect(yield mixed.checkContextPermission({ code: 'S', tenantId: SHARED }, outsider)).not.toBeNull();
|
|
186
|
+
expect(yield mixed.checkContextPermission({ code: 'P', tenantId: 'tenant-a' }, outsider)).toBeNull();
|
|
187
|
+
}));
|
|
188
|
+
it('turns away a channel subscriber from another tenant', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
expect(yield unaware.checkChannelSubscriber({ tenantId: 'tenant-a' }, owner)).toBe(true);
|
|
190
|
+
expect(yield unaware.checkChannelSubscriber({ tenantId: 'tenant-a' }, outsider)).toBe(false);
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
describe('an exempt model is left alone', () => {
|
|
194
|
+
beforeEach(() => { constants_1.APP.tenancy = provider; });
|
|
195
|
+
afterEach(() => { delete constants_1.APP.tenancy; });
|
|
196
|
+
// Written by event listeners, which run with no user in context: scoping them
|
|
197
|
+
// automatically would break the very cascade they exist to perform.
|
|
198
|
+
it('neither stamps nor filters', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
yield exempt.create({ code: 'A', tenantId: 'tenant-b' }, owner);
|
|
200
|
+
expect(yield exempt.list({}, {}, {}, outsider)).toHaveLength(1);
|
|
201
|
+
expect(yield exempt.list({}, {}, {}, {})).toHaveLength(1);
|
|
202
|
+
}));
|
|
203
|
+
});
|
|
204
|
+
describe('with no provider registered nothing is scoped at all', () => {
|
|
205
|
+
// The library is used by projects that have no notion of a tenant. For them every
|
|
206
|
+
// assertion above must NOT hold, or upgrading would silently empty their queries.
|
|
207
|
+
it('behaves as it did before tenancy existed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
|
+
expect(constants_1.APP.tenancy).toBeUndefined();
|
|
209
|
+
const doc = yield unaware.create({ code: 'A', tenantId: 'tenant-b' }, owner);
|
|
210
|
+
expect(doc.tenantId).toBe('tenant-b');
|
|
211
|
+
expect(yield unaware.list({}, {}, {}, outsider)).toHaveLength(1);
|
|
212
|
+
expect(yield unaware.get(doc.id, outsider)).not.toBeNull();
|
|
213
|
+
}));
|
|
214
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*********************************************************************************************
|
|
2
|
+
* *
|
|
3
|
+
* AUDIT - who created a document and when, who last changed it and when. *
|
|
4
|
+
* *
|
|
5
|
+
* Written by `BaseModel` rather than by every model, for the same reason tenancy is: a *
|
|
6
|
+
* rule each author has to remember is a rule that gets forgotten, and forgetting this one *
|
|
7
|
+
* is SILENT - the document is simply stored without a trace of who made it. *
|
|
8
|
+
* *
|
|
9
|
+
* That is not hypothetical. Before this existed, some sixty assignments were spread over *
|
|
10
|
+
* twenty-one models in four spellings of the same line, two of them commented out, and one *
|
|
11
|
+
* model wrote fields its schema never declared - so mongoose dropped them and the audit it *
|
|
12
|
+
* appeared to keep was never kept at all. *
|
|
13
|
+
* *
|
|
14
|
+
* ONLY WHAT THE SCHEMA DECLARES *
|
|
15
|
+
* *
|
|
16
|
+
* A field is stamped only if the model's schema has it. That keeps the change INERT for *
|
|
17
|
+
* every consumer that never asked for an audit - nothing appears in collections that did *
|
|
18
|
+
* not declare it - which is the same property that made tenancy safe to invert. Wanting *
|
|
19
|
+
* the audit is therefore an act: declare the field. *
|
|
20
|
+
* *
|
|
21
|
+
* WHY NOT MONGOOSE `timestamps: true` *
|
|
22
|
+
* *
|
|
23
|
+
* Because it knows the WHEN and cannot know the WHO. `createdBy` needs the request context, *
|
|
24
|
+
* which mongoose has no access to, so the timestamps would come from the schema and the *
|
|
25
|
+
* actors from here - two mechanisms, in two places, that drift apart the moment a write *
|
|
26
|
+
* goes through only one of them. `updateArrayItem` and `addArrayItem` are exactly that *
|
|
27
|
+
* write: they call `findOneAndUpdate` directly, so mongoose would refresh `updatedAt` *
|
|
28
|
+
* while `updatedBy` kept naming whoever wrote last through `update()`. An audit that names *
|
|
29
|
+
* the wrong author is worse than one that names nobody, because it is believed. *
|
|
30
|
+
* *
|
|
31
|
+
*********************************************************************************************/
|
|
32
|
+
import { NxContext, NxObject } from '../interfaces';
|
|
33
|
+
/** The four fields this module knows how to keep, and the only ones it ever touches. */
|
|
34
|
+
export declare const AUDIT_FIELDS: {
|
|
35
|
+
createdBy: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
updatedBy: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Stamps the audit fields a schema declares, and leaves every other field alone.
|
|
42
|
+
*
|
|
43
|
+
* On UPDATE the creation fields are dropped from the payload instead of being checked: a
|
|
44
|
+
* document is created once, so an update carrying `createdBy` is either noise or an attempt
|
|
45
|
+
* to rewrite history, and neither is worth honouring. Same reasoning as the tenant field.
|
|
46
|
+
*
|
|
47
|
+
* The user is read from the context and never from the payload - a client naming its own
|
|
48
|
+
* author would make the whole record worthless.
|
|
49
|
+
*
|
|
50
|
+
* @param item the payload, mutated in place
|
|
51
|
+
* @param schema the model's schema definition, used to know which fields exist
|
|
52
|
+
* @param context the request context
|
|
53
|
+
* @param creating whether this is a creation or an update
|
|
54
|
+
*/
|
|
55
|
+
export declare function stampAudit(item: NxObject | undefined, schema: NxObject | undefined, context: NxContext | undefined, creating: boolean): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*********************************************************************************************
|
|
3
|
+
* *
|
|
4
|
+
* AUDIT - who created a document and when, who last changed it and when. *
|
|
5
|
+
* *
|
|
6
|
+
* Written by `BaseModel` rather than by every model, for the same reason tenancy is: a *
|
|
7
|
+
* rule each author has to remember is a rule that gets forgotten, and forgetting this one *
|
|
8
|
+
* is SILENT - the document is simply stored without a trace of who made it. *
|
|
9
|
+
* *
|
|
10
|
+
* That is not hypothetical. Before this existed, some sixty assignments were spread over *
|
|
11
|
+
* twenty-one models in four spellings of the same line, two of them commented out, and one *
|
|
12
|
+
* model wrote fields its schema never declared - so mongoose dropped them and the audit it *
|
|
13
|
+
* appeared to keep was never kept at all. *
|
|
14
|
+
* *
|
|
15
|
+
* ONLY WHAT THE SCHEMA DECLARES *
|
|
16
|
+
* *
|
|
17
|
+
* A field is stamped only if the model's schema has it. That keeps the change INERT for *
|
|
18
|
+
* every consumer that never asked for an audit - nothing appears in collections that did *
|
|
19
|
+
* not declare it - which is the same property that made tenancy safe to invert. Wanting *
|
|
20
|
+
* the audit is therefore an act: declare the field. *
|
|
21
|
+
* *
|
|
22
|
+
* WHY NOT MONGOOSE `timestamps: true` *
|
|
23
|
+
* *
|
|
24
|
+
* Because it knows the WHEN and cannot know the WHO. `createdBy` needs the request context, *
|
|
25
|
+
* which mongoose has no access to, so the timestamps would come from the schema and the *
|
|
26
|
+
* actors from here - two mechanisms, in two places, that drift apart the moment a write *
|
|
27
|
+
* goes through only one of them. `updateArrayItem` and `addArrayItem` are exactly that *
|
|
28
|
+
* write: they call `findOneAndUpdate` directly, so mongoose would refresh `updatedAt` *
|
|
29
|
+
* while `updatedBy` kept naming whoever wrote last through `update()`. An audit that names *
|
|
30
|
+
* the wrong author is worse than one that names nobody, because it is believed. *
|
|
31
|
+
* *
|
|
32
|
+
*********************************************************************************************/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.AUDIT_FIELDS = void 0;
|
|
35
|
+
exports.stampAudit = stampAudit;
|
|
36
|
+
/** The four fields this module knows how to keep, and the only ones it ever touches. */
|
|
37
|
+
exports.AUDIT_FIELDS = {
|
|
38
|
+
createdBy: 'createdBy',
|
|
39
|
+
createdAt: 'createdAt',
|
|
40
|
+
updatedBy: 'updatedBy',
|
|
41
|
+
updatedAt: 'updatedAt'
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Stamps the audit fields a schema declares, and leaves every other field alone.
|
|
45
|
+
*
|
|
46
|
+
* On UPDATE the creation fields are dropped from the payload instead of being checked: a
|
|
47
|
+
* document is created once, so an update carrying `createdBy` is either noise or an attempt
|
|
48
|
+
* to rewrite history, and neither is worth honouring. Same reasoning as the tenant field.
|
|
49
|
+
*
|
|
50
|
+
* The user is read from the context and never from the payload - a client naming its own
|
|
51
|
+
* author would make the whole record worthless.
|
|
52
|
+
*
|
|
53
|
+
* @param item the payload, mutated in place
|
|
54
|
+
* @param schema the model's schema definition, used to know which fields exist
|
|
55
|
+
* @param context the request context
|
|
56
|
+
* @param creating whether this is a creation or an update
|
|
57
|
+
*/
|
|
58
|
+
function stampAudit(item, schema, context, creating) {
|
|
59
|
+
var _a;
|
|
60
|
+
if (!item || !schema)
|
|
61
|
+
return;
|
|
62
|
+
const userId = (_a = context === null || context === void 0 ? void 0 : context.user) === null || _a === void 0 ? void 0 : _a.id;
|
|
63
|
+
const now = new Date();
|
|
64
|
+
if (creating) {
|
|
65
|
+
if (exports.AUDIT_FIELDS.createdAt in schema)
|
|
66
|
+
item[exports.AUDIT_FIELDS.createdAt] = now;
|
|
67
|
+
if (userId && exports.AUDIT_FIELDS.createdBy in schema)
|
|
68
|
+
item[exports.AUDIT_FIELDS.createdBy] = userId;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
delete item[exports.AUDIT_FIELDS.createdBy];
|
|
72
|
+
delete item[exports.AUDIT_FIELDS.createdAt];
|
|
73
|
+
if (exports.AUDIT_FIELDS.updatedAt in schema)
|
|
74
|
+
item[exports.AUDIT_FIELDS.updatedAt] = now;
|
|
75
|
+
if (userId && exports.AUDIT_FIELDS.updatedBy in schema)
|
|
76
|
+
item[exports.AUDIT_FIELDS.updatedBy] = userId;
|
|
77
|
+
}
|
|
@@ -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
|
|
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
|
*/
|
|
@@ -222,19 +268,41 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
222
268
|
});
|
|
223
269
|
}
|
|
224
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.
|
|
225
276
|
*
|
|
226
277
|
* @param {*} item
|
|
227
278
|
* @param {*} context
|
|
228
279
|
*/
|
|
229
280
|
checkContextPermission(item, context = {}) {
|
|
230
|
-
|
|
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);
|
|
231
288
|
}
|
|
232
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]`.
|
|
233
294
|
*
|
|
234
295
|
* @param {*} context
|
|
235
296
|
*/
|
|
236
297
|
checkChannelSubscriber(payloadId, context = {}) {
|
|
237
|
-
|
|
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));
|
|
238
306
|
}
|
|
239
307
|
/**
|
|
240
308
|
* Get document using Moongose wrapper method
|
|
@@ -268,28 +336,63 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
268
336
|
return this.mgModel.collection.findOne(params);
|
|
269
337
|
}
|
|
270
338
|
/**
|
|
339
|
+
* Scopes a read by id. Overriding this REPLACES the tenant scoping, it does not add to it.
|
|
271
340
|
*
|
|
272
|
-
* @param
|
|
341
|
+
* @param filter
|
|
273
342
|
* @param context
|
|
274
|
-
* @returns
|
|
343
|
+
* @returns the filter the query must use - the RETURNED one, which is what `get` uses
|
|
275
344
|
*/
|
|
276
345
|
beforeGet(filter, context) {
|
|
277
346
|
return __awaiter(this, void 0, void 0, function* () {
|
|
278
|
-
return filter;
|
|
347
|
+
return this.scopeFilter(filter, context);
|
|
279
348
|
});
|
|
280
349
|
}
|
|
281
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.
|
|
282
364
|
*
|
|
283
365
|
* @param {*} item
|
|
284
366
|
*/
|
|
285
367
|
beforeCreate(item, context) {
|
|
286
|
-
return
|
|
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
|
+
});
|
|
287
379
|
}
|
|
288
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.
|
|
289
386
|
*
|
|
290
387
|
* @param {*} item
|
|
291
388
|
*/
|
|
292
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);
|
|
293
396
|
return Promise.resolve(item);
|
|
294
397
|
}
|
|
295
398
|
/**
|
|
@@ -300,11 +403,23 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
300
403
|
return Promise.resolve(item);
|
|
301
404
|
}
|
|
302
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.
|
|
303
412
|
*
|
|
304
413
|
* @param {*} id
|
|
305
414
|
*/
|
|
306
415
|
beforeRemove(id, context) {
|
|
307
|
-
return
|
|
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
|
+
});
|
|
308
423
|
}
|
|
309
424
|
/**
|
|
310
425
|
*
|
|
@@ -348,6 +463,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
348
463
|
}
|
|
349
464
|
}
|
|
350
465
|
/**
|
|
466
|
+
* Scopes list / count / findOne / aggregate - the bulk of the read surface.
|
|
351
467
|
*
|
|
352
468
|
* @param {*} params
|
|
353
469
|
* @param {*} context
|
|
@@ -357,6 +473,7 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
357
473
|
var _a;
|
|
358
474
|
if (!params)
|
|
359
475
|
params = {};
|
|
476
|
+
this.scopeFilter(params, context);
|
|
360
477
|
this.manageFilterArrayParams(params);
|
|
361
478
|
(_a = params.rangeDate) !== null && _a !== void 0 ? _a : (params.rangeDate = {});
|
|
362
479
|
let and = [];
|
|
@@ -472,6 +589,9 @@ class BaseModel extends events_1.EventEmitter {
|
|
|
472
589
|
const itemDoc = yield this.get(id, context);
|
|
473
590
|
if (!itemDoc)
|
|
474
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);
|
|
475
595
|
let toApply = item;
|
|
476
596
|
// complete merge with the freshly read document
|
|
477
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
|
+
}
|
package/build/common/enums.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/build/common/enums.js
CHANGED
|
@@ -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 = {}));
|
package/build/common/index.d.ts
CHANGED
package/build/common/index.js
CHANGED
|
@@ -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';
|
package/build/common/types.d.ts
CHANGED
|
@@ -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;
|