@zenstackhq/runtime 1.0.0-beta.2 → 1.0.0-beta.20
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/browser/index.d.mts +13 -0
- package/browser/index.d.ts +13 -0
- package/browser/index.js +70 -0
- package/browser/index.js.map +1 -0
- package/browser/index.mjs +32 -0
- package/browser/index.mjs.map +1 -0
- package/constants.d.ts +59 -2
- package/constants.js +60 -2
- package/constants.js.map +1 -1
- package/enhancements/enhance.d.ts +18 -0
- package/enhancements/enhance.js +42 -0
- package/enhancements/enhance.js.map +1 -0
- package/enhancements/index.d.ts +5 -0
- package/enhancements/index.js +5 -0
- package/enhancements/index.js.map +1 -1
- package/enhancements/model-data-visitor.d.ts +16 -0
- package/enhancements/model-data-visitor.js +41 -0
- package/enhancements/model-data-visitor.js.map +1 -0
- package/enhancements/model-meta.d.ts +6 -1
- package/enhancements/model-meta.js +23 -2
- package/enhancements/model-meta.js.map +1 -1
- package/enhancements/nested-write-vistor.d.ts +21 -16
- package/enhancements/nested-write-vistor.js +73 -34
- package/enhancements/nested-write-vistor.js.map +1 -1
- package/enhancements/omit.d.ts +1 -1
- package/enhancements/policy/handler.d.ts +25 -15
- package/enhancements/policy/handler.js +771 -133
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/index.d.ts +5 -1
- package/enhancements/policy/index.js +53 -3
- package/enhancements/policy/index.js.map +1 -1
- package/enhancements/policy/logger.js +1 -1
- package/enhancements/policy/logger.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +102 -46
- package/enhancements/policy/policy-utils.js +683 -550
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/preset.d.ts +3 -8
- package/enhancements/preset.js +2 -4
- package/enhancements/preset.js.map +1 -1
- package/enhancements/proxy.d.ts +3 -1
- package/enhancements/proxy.js +23 -12
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +25 -8
- package/enhancements/types.js +1 -0
- package/enhancements/types.js.map +1 -1
- package/enhancements/utils.d.ts +4 -0
- package/enhancements/utils.js +59 -8
- package/enhancements/utils.js.map +1 -1
- package/enhancements/where-visitor.d.ts +33 -0
- package/enhancements/where-visitor.js +87 -0
- package/enhancements/where-visitor.js.map +1 -0
- package/error.js +9 -3
- package/error.js.map +1 -1
- package/index.d.ts +3 -2
- package/index.js +3 -2
- package/index.js.map +1 -1
- package/package.json +33 -9
- package/types.d.ts +11 -2
- package/types.js +2 -0
- package/types.js.map +1 -1
- package/version.d.ts +5 -0
- package/version.js +34 -1
- package/version.js.map +1 -1
- package/zod/index.d.ts +2 -0
- package/zod/index.js +4 -0
- package/zod/input.d.ts +1 -0
- package/zod/input.js +8 -0
- package/zod/models.d.ts +1 -0
- package/zod/models.js +8 -0
- package/serialization-utils.d.ts +0 -1
- package/serialization-utils.js +0 -22
- package/serialization-utils.js.map +0 -1
- package/zod.d.ts +0 -10
- package/zod.js +0 -17
- package/zod.js.map +0 -1
|
@@ -14,130 +14,236 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
};
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.PolicyUtil = void 0;
|
|
17
|
-
const cuid2_1 = require("@paralleldrive/cuid2");
|
|
18
17
|
const deepcopy_1 = __importDefault(require("deepcopy"));
|
|
19
18
|
const lower_case_first_1 = require("lower-case-first");
|
|
20
|
-
const
|
|
19
|
+
const upper_case_first_1 = require("upper-case-first");
|
|
21
20
|
const zod_validation_error_1 = require("zod-validation-error");
|
|
22
21
|
const constants_1 = require("../../constants");
|
|
23
22
|
const version_1 = require("../../version");
|
|
24
23
|
const model_meta_1 = require("../model-meta");
|
|
25
|
-
const nested_write_vistor_1 = require("../nested-write-vistor");
|
|
26
24
|
const utils_1 = require("../utils");
|
|
27
25
|
const logger_1 = require("./logger");
|
|
28
26
|
/**
|
|
29
27
|
* Access policy enforcement utilities
|
|
30
28
|
*/
|
|
31
29
|
class PolicyUtil {
|
|
32
|
-
constructor(db, modelMeta, policy, user,
|
|
30
|
+
constructor(db, modelMeta, policy, zodSchemas, user, shouldLogQuery = false) {
|
|
33
31
|
this.db = db;
|
|
34
32
|
this.modelMeta = modelMeta;
|
|
35
33
|
this.policy = policy;
|
|
34
|
+
this.zodSchemas = zodSchemas;
|
|
36
35
|
this.user = user;
|
|
37
|
-
this.
|
|
36
|
+
this.shouldLogQuery = shouldLogQuery;
|
|
38
37
|
this.logger = new logger_1.Logger(db);
|
|
39
38
|
}
|
|
39
|
+
//#region Logical operators
|
|
40
40
|
/**
|
|
41
41
|
* Creates a conjunction of a list of query conditions.
|
|
42
42
|
*/
|
|
43
43
|
and(...conditions) {
|
|
44
|
-
|
|
45
|
-
// always false
|
|
46
|
-
return { [constants_1.GUARD_FIELD_NAME]: false };
|
|
47
|
-
}
|
|
48
|
-
const filtered = conditions.filter((c) => typeof c === 'object' && !!c && Object.keys(c).length > 0);
|
|
49
|
-
if (filtered.length === 0) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
else if (filtered.length === 1) {
|
|
53
|
-
return filtered[0];
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
return { AND: filtered };
|
|
57
|
-
}
|
|
44
|
+
return this.reduce({ AND: conditions });
|
|
58
45
|
}
|
|
59
46
|
/**
|
|
60
47
|
* Creates a disjunction of a list of query conditions.
|
|
61
48
|
*/
|
|
62
49
|
or(...conditions) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
return this.reduce({ OR: conditions });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a negation of a query condition.
|
|
54
|
+
*/
|
|
55
|
+
not(condition) {
|
|
56
|
+
if (condition === undefined) {
|
|
57
|
+
return this.makeTrue();
|
|
66
58
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return undefined;
|
|
59
|
+
else if (typeof condition === 'boolean') {
|
|
60
|
+
return this.reduce(!condition);
|
|
70
61
|
}
|
|
71
|
-
else
|
|
72
|
-
return
|
|
62
|
+
else {
|
|
63
|
+
return this.reduce({ NOT: condition });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Static True/False conditions
|
|
67
|
+
// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
|
|
68
|
+
isTrue(condition) {
|
|
69
|
+
if (condition === null || condition === undefined) {
|
|
70
|
+
return false;
|
|
73
71
|
}
|
|
74
72
|
else {
|
|
75
|
-
return
|
|
73
|
+
return ((typeof condition === 'object' && Object.keys(condition).length === 0) ||
|
|
74
|
+
('AND' in condition && Array.isArray(condition.AND) && condition.AND.length === 0));
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
not(condition) {
|
|
82
|
-
if (typeof condition === 'boolean') {
|
|
83
|
-
return !condition;
|
|
77
|
+
isFalse(condition) {
|
|
78
|
+
if (condition === null || condition === undefined) {
|
|
79
|
+
return false;
|
|
84
80
|
}
|
|
85
81
|
else {
|
|
86
|
-
return
|
|
82
|
+
return 'OR' in condition && Array.isArray(condition.OR) && condition.OR.length === 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
makeTrue() {
|
|
86
|
+
return { AND: [] };
|
|
87
|
+
}
|
|
88
|
+
makeFalse() {
|
|
89
|
+
return { OR: [] };
|
|
90
|
+
}
|
|
91
|
+
reduce(condition) {
|
|
92
|
+
if (condition === true || condition === undefined) {
|
|
93
|
+
return this.makeTrue();
|
|
94
|
+
}
|
|
95
|
+
if (condition === false) {
|
|
96
|
+
return this.makeFalse();
|
|
97
|
+
}
|
|
98
|
+
if ('AND' in condition && Array.isArray(condition.AND)) {
|
|
99
|
+
const children = condition.AND.map((c) => this.reduce(c)).filter((c) => c !== undefined && !this.isTrue(c));
|
|
100
|
+
if (children.length === 0) {
|
|
101
|
+
return this.makeTrue();
|
|
102
|
+
}
|
|
103
|
+
else if (children.some((c) => this.isFalse(c))) {
|
|
104
|
+
return this.makeFalse();
|
|
105
|
+
}
|
|
106
|
+
else if (children.length === 1) {
|
|
107
|
+
return children[0];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return { AND: children };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if ('OR' in condition && Array.isArray(condition.OR)) {
|
|
114
|
+
const children = condition.OR.map((c) => this.reduce(c)).filter((c) => c !== undefined && !this.isFalse(c));
|
|
115
|
+
if (children.length === 0) {
|
|
116
|
+
return this.makeFalse();
|
|
117
|
+
}
|
|
118
|
+
else if (children.some((c) => this.isTrue(c))) {
|
|
119
|
+
return this.makeTrue();
|
|
120
|
+
}
|
|
121
|
+
else if (children.length === 1) {
|
|
122
|
+
return children[0];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return { OR: children };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if ('NOT' in condition && condition.NOT !== null && typeof condition.NOT === 'object') {
|
|
129
|
+
const child = this.reduce(condition.NOT);
|
|
130
|
+
if (this.isTrue(child)) {
|
|
131
|
+
return this.makeFalse();
|
|
132
|
+
}
|
|
133
|
+
else if (this.isFalse(child)) {
|
|
134
|
+
return this.makeTrue();
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return { NOT: child };
|
|
138
|
+
}
|
|
87
139
|
}
|
|
140
|
+
return condition;
|
|
88
141
|
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//# Auth guard
|
|
89
144
|
/**
|
|
90
145
|
* Gets pregenerated authorization guard object for a given model and operation.
|
|
91
146
|
*
|
|
92
147
|
* @returns true if operation is unconditionally allowed, false if unconditionally denied,
|
|
93
148
|
* otherwise returns a guard object
|
|
94
149
|
*/
|
|
95
|
-
getAuthGuard(model, operation, preValue) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
});
|
|
150
|
+
getAuthGuard(db, model, operation, preValue) {
|
|
151
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
152
|
+
if (!guard) {
|
|
153
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
154
|
+
}
|
|
155
|
+
const provider = guard[operation];
|
|
156
|
+
if (typeof provider === 'boolean') {
|
|
157
|
+
return this.reduce(provider);
|
|
158
|
+
}
|
|
159
|
+
if (!provider) {
|
|
160
|
+
throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
|
|
161
|
+
}
|
|
162
|
+
const r = provider({ user: this.user, preValue }, db);
|
|
163
|
+
return this.reduce(r);
|
|
110
164
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
165
|
+
/**
|
|
166
|
+
* Get field-level auth guard
|
|
167
|
+
*/
|
|
168
|
+
getFieldUpdateAuthGuard(db, model, field) {
|
|
169
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
170
|
+
if (!guard) {
|
|
171
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
172
|
+
}
|
|
173
|
+
const provider = guard[`${constants_1.FIELD_LEVEL_UPDATE_GUARD_PREFIX}${field}`];
|
|
174
|
+
if (typeof provider === 'boolean') {
|
|
175
|
+
return this.reduce(provider);
|
|
176
|
+
}
|
|
177
|
+
if (!provider) {
|
|
178
|
+
return this.makeTrue();
|
|
179
|
+
}
|
|
180
|
+
const r = provider({ user: this.user }, db);
|
|
181
|
+
return this.reduce(r);
|
|
119
182
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Checks if the given model has a policy guard for the given operation.
|
|
185
|
+
*/
|
|
186
|
+
hasAuthGuard(model, operation) {
|
|
187
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
188
|
+
if (!guard) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const provider = guard[operation];
|
|
192
|
+
return typeof provider !== 'boolean' || provider !== true;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Checks model creation policy based on static analysis to the input args.
|
|
196
|
+
*
|
|
197
|
+
* @returns boolean if static analysis is enough to determine the result, undefined if not
|
|
198
|
+
*/
|
|
199
|
+
checkInputGuard(model, args, operation) {
|
|
200
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
201
|
+
if (!guard) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const provider = guard[`${operation}_input`];
|
|
205
|
+
if (typeof provider === 'boolean') {
|
|
206
|
+
return provider;
|
|
207
|
+
}
|
|
208
|
+
if (!provider) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
return provider(args, { user: this.user });
|
|
124
212
|
}
|
|
125
213
|
/**
|
|
126
214
|
* Injects model auth guard as where clause.
|
|
127
215
|
*/
|
|
128
|
-
injectAuthGuard(args, model, operation) {
|
|
216
|
+
injectAuthGuard(db, args, model, operation) {
|
|
129
217
|
return __awaiter(this, void 0, void 0, function* () {
|
|
218
|
+
let guard = this.getAuthGuard(db, model, operation);
|
|
219
|
+
if (this.isFalse(guard)) {
|
|
220
|
+
args.where = this.makeFalse();
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (operation === 'update' && args) {
|
|
224
|
+
// merge field-level policy guards
|
|
225
|
+
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
226
|
+
if (fieldUpdateGuard.rejectedByField) {
|
|
227
|
+
// rejected
|
|
228
|
+
args.where = this.makeFalse();
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
else if (fieldUpdateGuard.guard) {
|
|
232
|
+
// merge
|
|
233
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
130
236
|
if (args.where) {
|
|
131
237
|
// inject into relation fields:
|
|
132
238
|
// to-many: some/none/every
|
|
133
239
|
// to-one: direct-conditions/is/isNot
|
|
134
|
-
yield this.
|
|
240
|
+
yield this.injectGuardForRelationFields(db, model, args.where, operation);
|
|
135
241
|
}
|
|
136
|
-
const guard = yield this.getAuthGuard(model, operation);
|
|
137
242
|
args.where = this.and(args.where, guard);
|
|
243
|
+
return true;
|
|
138
244
|
});
|
|
139
245
|
}
|
|
140
|
-
|
|
246
|
+
injectGuardForRelationFields(db, model, payload, operation) {
|
|
141
247
|
return __awaiter(this, void 0, void 0, function* () {
|
|
142
248
|
for (const [field, subPayload] of Object.entries(payload)) {
|
|
143
249
|
if (!subPayload) {
|
|
@@ -148,24 +254,24 @@ class PolicyUtil {
|
|
|
148
254
|
continue;
|
|
149
255
|
}
|
|
150
256
|
if (fieldInfo.isArray) {
|
|
151
|
-
yield this.injectGuardForToManyField(fieldInfo, subPayload, operation);
|
|
257
|
+
yield this.injectGuardForToManyField(db, fieldInfo, subPayload, operation);
|
|
152
258
|
}
|
|
153
259
|
else {
|
|
154
|
-
yield this.injectGuardForToOneField(fieldInfo, subPayload, operation);
|
|
260
|
+
yield this.injectGuardForToOneField(db, fieldInfo, subPayload, operation);
|
|
155
261
|
}
|
|
156
262
|
}
|
|
157
263
|
});
|
|
158
264
|
}
|
|
159
|
-
injectGuardForToManyField(fieldInfo, payload, operation) {
|
|
265
|
+
injectGuardForToManyField(db, fieldInfo, payload, operation) {
|
|
160
266
|
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
-
const guard =
|
|
267
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, operation);
|
|
162
268
|
if (payload.some) {
|
|
163
|
-
yield this.
|
|
269
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.some, operation);
|
|
164
270
|
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
165
271
|
payload.some = this.and(payload.some, guard);
|
|
166
272
|
}
|
|
167
273
|
if (payload.none) {
|
|
168
|
-
yield this.
|
|
274
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.none, operation);
|
|
169
275
|
// turn none into: { none: { AND: [guard, payload.none] } }
|
|
170
276
|
payload.none = this.and(payload.none, guard);
|
|
171
277
|
}
|
|
@@ -173,7 +279,7 @@ class PolicyUtil {
|
|
|
173
279
|
typeof payload.every === 'object' &&
|
|
174
280
|
// ignore empty every clause
|
|
175
281
|
Object.keys(payload.every).length > 0) {
|
|
176
|
-
yield this.
|
|
282
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.every, operation);
|
|
177
283
|
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
|
|
178
284
|
if (!payload.none) {
|
|
179
285
|
payload.none = {};
|
|
@@ -183,24 +289,24 @@ class PolicyUtil {
|
|
|
183
289
|
}
|
|
184
290
|
});
|
|
185
291
|
}
|
|
186
|
-
injectGuardForToOneField(fieldInfo, payload, operation) {
|
|
292
|
+
injectGuardForToOneField(db, fieldInfo, payload, operation) {
|
|
187
293
|
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
-
const guard =
|
|
294
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, operation);
|
|
189
295
|
if (payload.is || payload.isNot) {
|
|
190
296
|
if (payload.is) {
|
|
191
|
-
yield this.
|
|
297
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.is, operation);
|
|
192
298
|
// turn "is" into: { is: { AND: [ originalIs, guard ] }
|
|
193
299
|
payload.is = this.and(payload.is, guard);
|
|
194
300
|
}
|
|
195
301
|
if (payload.isNot) {
|
|
196
|
-
yield this.
|
|
302
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation);
|
|
197
303
|
// turn "isNot" into: { isNot: { AND: [ originalIsNot, { NOT: guard } ] } }
|
|
198
304
|
payload.isNot = this.and(payload.isNot, this.not(guard));
|
|
199
305
|
delete payload.isNot;
|
|
200
306
|
}
|
|
201
307
|
}
|
|
202
308
|
else {
|
|
203
|
-
yield this.
|
|
309
|
+
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload, operation);
|
|
204
310
|
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
205
311
|
const combined = this.and((0, deepcopy_1.default)(payload), guard);
|
|
206
312
|
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
@@ -209,436 +315,405 @@ class PolicyUtil {
|
|
|
209
315
|
});
|
|
210
316
|
}
|
|
211
317
|
/**
|
|
212
|
-
*
|
|
213
|
-
* are guaranteed to fully satisfy 'read' policy rules recursively.
|
|
214
|
-
*
|
|
215
|
-
* For to-many relations involved, items not satisfying policy are
|
|
216
|
-
* silently trimmed. For to-one relation, if relation data fails policy
|
|
217
|
-
* an error is thrown.
|
|
318
|
+
* Injects auth guard for read operations.
|
|
218
319
|
*/
|
|
219
|
-
|
|
320
|
+
injectForRead(db, model, args) {
|
|
220
321
|
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
-
|
|
322
|
+
const injected = {};
|
|
323
|
+
if (!(yield this.injectAuthGuard(db, injected, model, 'read'))) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
222
326
|
if (args.where) {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
yield this.
|
|
227
|
-
}
|
|
228
|
-
yield this.injectAuthGuard(args, model, 'read');
|
|
229
|
-
// recursively inject read guard conditions into the query args
|
|
230
|
-
yield this.injectNestedReadConditions(model, args);
|
|
231
|
-
if (this.shouldLogQuery) {
|
|
232
|
-
this.logger.info(`[withPolicy] \`findMany\`:\n${(0, utils_1.formatObject)(args)}`);
|
|
327
|
+
// inject into relation fields:
|
|
328
|
+
// to-many: some/none/every
|
|
329
|
+
// to-one: direct-conditions/is/isNot
|
|
330
|
+
yield this.injectGuardForRelationFields(db, model, args.where, 'read');
|
|
233
331
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
332
|
+
if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
|
|
333
|
+
if (!args.where) {
|
|
334
|
+
args.where = injected.where;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.mergeWhereClause(args.where, injected.where);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// recursively inject read guard conditions into nested select, include, and _count
|
|
341
|
+
const hoistedConditions = yield this.injectNestedReadConditions(db, model, args);
|
|
342
|
+
// the injection process may generate conditions that need to be hoisted to the toplevel,
|
|
343
|
+
// if so, merge it with the existing where
|
|
344
|
+
if (hoistedConditions.length > 0) {
|
|
345
|
+
if (!args.where) {
|
|
346
|
+
args.where = this.and(...hoistedConditions);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
this.mergeWhereClause(args.where, this.and(...hoistedConditions));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
237
353
|
});
|
|
238
354
|
}
|
|
239
355
|
// flatten unique constraint filters
|
|
240
356
|
flattenGeneratedUniqueField(model, args) {
|
|
241
357
|
var _a;
|
|
358
|
+
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
359
|
+
const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
360
|
+
if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) {
|
|
361
|
+
for (const [field, value] of Object.entries(args)) {
|
|
362
|
+
if (uniqueConstraints[field] &&
|
|
363
|
+
uniqueConstraints[field].fields.length > 1 &&
|
|
364
|
+
typeof value === 'object') {
|
|
365
|
+
// multi-field unique constraint, flatten it
|
|
366
|
+
delete args[field];
|
|
367
|
+
for (const [f, v] of Object.entries(value)) {
|
|
368
|
+
args[f] = v;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Gets unique constraints for the given model.
|
|
376
|
+
*/
|
|
377
|
+
getUniqueConstraints(model) {
|
|
378
|
+
var _a, _b;
|
|
379
|
+
return (_b = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)]) !== null && _b !== void 0 ? _b : {};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Builds a reversed query for the given nested path.
|
|
383
|
+
*/
|
|
384
|
+
buildReversedQuery(context) {
|
|
242
385
|
return __awaiter(this, void 0, void 0, function* () {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
386
|
+
let result, currQuery;
|
|
387
|
+
let currField;
|
|
388
|
+
for (let i = context.nestingPath.length - 1; i >= 0; i--) {
|
|
389
|
+
const { field, model, where } = context.nestingPath[i];
|
|
390
|
+
// never modify the original where because it's shared in the structure
|
|
391
|
+
const visitWhere = Object.assign({}, where);
|
|
392
|
+
if (model && where) {
|
|
393
|
+
// make sure composite unique condition is flattened
|
|
394
|
+
this.flattenGeneratedUniqueField(model, visitWhere);
|
|
395
|
+
}
|
|
396
|
+
if (!result) {
|
|
397
|
+
// first segment (bottom), just use its where clause
|
|
398
|
+
result = currQuery = Object.assign({}, visitWhere);
|
|
399
|
+
currField = field;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
if (!currField) {
|
|
403
|
+
throw this.unknownError(`missing field in nested path`);
|
|
404
|
+
}
|
|
405
|
+
if (!currField.backLink) {
|
|
406
|
+
throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
|
|
407
|
+
}
|
|
408
|
+
const backLinkField = this.getModelField(currField.type, currField.backLink);
|
|
409
|
+
if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isArray) {
|
|
410
|
+
// many-side of relationship, wrap with "some" query
|
|
411
|
+
currQuery[currField.backLink] = { some: Object.assign({}, visitWhere) };
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
if (where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping) {
|
|
415
|
+
for (const [r, fk] of Object.entries(backLinkField.foreignKeyMapping)) {
|
|
416
|
+
currQuery[fk] = visitWhere[r];
|
|
417
|
+
}
|
|
418
|
+
if (i > 0) {
|
|
419
|
+
currQuery[currField.backLink] = {};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
currQuery[currField.backLink] = Object.assign({}, visitWhere);
|
|
251
424
|
}
|
|
252
|
-
delete args[field];
|
|
253
|
-
flattened = true;
|
|
254
425
|
}
|
|
426
|
+
currQuery = currQuery[currField.backLink];
|
|
427
|
+
currField = field;
|
|
255
428
|
}
|
|
256
429
|
}
|
|
257
|
-
|
|
258
|
-
// DEBUG
|
|
259
|
-
// this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
|
|
260
|
-
}
|
|
430
|
+
return result;
|
|
261
431
|
});
|
|
262
432
|
}
|
|
263
|
-
injectNestedReadConditions(model, args) {
|
|
264
|
-
var _a
|
|
433
|
+
injectNestedReadConditions(db, model, args) {
|
|
434
|
+
var _a;
|
|
265
435
|
return __awaiter(this, void 0, void 0, function* () {
|
|
266
436
|
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
267
437
|
if (!injectTarget) {
|
|
268
|
-
return;
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
if (injectTarget._count !== undefined) {
|
|
441
|
+
// _count needs to respect read policies of related models
|
|
442
|
+
if (injectTarget._count === true) {
|
|
443
|
+
// include count for all relations, expand to all fields
|
|
444
|
+
// so that we can inject guard conditions for each of them
|
|
445
|
+
injectTarget._count = { select: {} };
|
|
446
|
+
const modelFields = (0, model_meta_1.getFields)(this.modelMeta, model);
|
|
447
|
+
if (modelFields) {
|
|
448
|
+
for (const [k, v] of Object.entries(modelFields)) {
|
|
449
|
+
if (v.isDataModel && v.isArray) {
|
|
450
|
+
// create an entry for to-many relation
|
|
451
|
+
injectTarget._count.select[k] = {};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// inject conditions for each relation
|
|
457
|
+
for (const field of Object.keys(injectTarget._count.select)) {
|
|
458
|
+
if (typeof injectTarget._count.select[field] !== 'object') {
|
|
459
|
+
injectTarget._count.select[field] = {};
|
|
460
|
+
}
|
|
461
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
462
|
+
if (!fieldInfo) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
// inject into the "where" clause inside select
|
|
466
|
+
yield this.injectAuthGuard(db, injectTarget._count.select[field], fieldInfo.type, 'read');
|
|
467
|
+
}
|
|
269
468
|
}
|
|
270
|
-
|
|
469
|
+
// collect filter conditions that should be hoisted to the toplevel
|
|
470
|
+
const hoistedConditions = [];
|
|
271
471
|
for (const field of (0, utils_1.getModelFields)(injectTarget)) {
|
|
272
472
|
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
273
473
|
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
274
474
|
// only care about relation fields
|
|
275
475
|
continue;
|
|
276
476
|
}
|
|
277
|
-
|
|
477
|
+
let hoisted;
|
|
478
|
+
if (fieldInfo.isArray ||
|
|
479
|
+
// Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0
|
|
480
|
+
// https://github.com/prisma/prisma/discussions/20350
|
|
481
|
+
fieldInfo.isOptional) {
|
|
278
482
|
if (typeof injectTarget[field] !== 'object') {
|
|
279
483
|
injectTarget[field] = {};
|
|
280
484
|
}
|
|
281
|
-
// inject extra condition for to-many relation
|
|
282
|
-
yield this.injectAuthGuard(injectTarget[field], fieldInfo.type, 'read');
|
|
485
|
+
// inject extra condition for to-many or nullable to-one relation
|
|
486
|
+
yield this.injectAuthGuard(db, injectTarget[field], fieldInfo.type, 'read');
|
|
487
|
+
// recurse
|
|
488
|
+
const subHoisted = yield this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
489
|
+
if (subHoisted.length > 0) {
|
|
490
|
+
// we can convert it to a where at this level
|
|
491
|
+
injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted);
|
|
492
|
+
}
|
|
283
493
|
}
|
|
284
494
|
else {
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
if (injectTarget[field].select[idField.name] !== true) {
|
|
292
|
-
injectTarget[field].select[idField.name] = true;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
495
|
+
// hoist non-nullable to-one filter to the parent level
|
|
496
|
+
hoisted = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
497
|
+
// recurse
|
|
498
|
+
const subHoisted = yield this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
499
|
+
if (subHoisted.length > 0) {
|
|
500
|
+
hoisted = this.and(hoisted, ...subHoisted);
|
|
295
501
|
}
|
|
296
502
|
}
|
|
297
|
-
|
|
298
|
-
|
|
503
|
+
if (hoisted && !this.isTrue(hoisted)) {
|
|
504
|
+
hoistedConditions.push({ [field]: hoisted });
|
|
505
|
+
}
|
|
299
506
|
}
|
|
507
|
+
return hoistedConditions;
|
|
300
508
|
});
|
|
301
509
|
}
|
|
302
510
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* omitted.
|
|
511
|
+
* Given a model and a unique filter, checks the operation is allowed by policies and field validations.
|
|
512
|
+
* Rejects with an error if not allowed.
|
|
306
513
|
*/
|
|
307
|
-
|
|
308
|
-
var _a;
|
|
514
|
+
checkPolicyForUnique(model, uniqueFilter, operation, db, args, preValue) {
|
|
309
515
|
return __awaiter(this, void 0, void 0, function* () {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
516
|
+
let guard = this.getAuthGuard(db, model, operation, preValue);
|
|
517
|
+
if (this.isFalse(guard)) {
|
|
518
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`);
|
|
519
|
+
}
|
|
520
|
+
if (operation === 'update' && args) {
|
|
521
|
+
// merge field-level policy guards
|
|
522
|
+
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
523
|
+
if (fieldUpdateGuard.rejectedByField) {
|
|
524
|
+
// rejected
|
|
525
|
+
throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"`);
|
|
319
526
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
527
|
+
else if (fieldUpdateGuard.guard) {
|
|
528
|
+
// merge
|
|
529
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
323
530
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
531
|
+
}
|
|
532
|
+
// Zod schema is to be checked for "create" and "postUpdate"
|
|
533
|
+
const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined;
|
|
534
|
+
if (this.isTrue(guard) && !schema) {
|
|
535
|
+
// unconditionally allowed
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const select = schema
|
|
539
|
+
? // need to validate against schema, need to fetch all fields
|
|
540
|
+
undefined
|
|
541
|
+
: // only fetch id fields
|
|
542
|
+
this.makeIdSelection(model);
|
|
543
|
+
let where = this.clone(uniqueFilter);
|
|
544
|
+
// query args may have be of combined-id form, need to flatten it to call findFirst
|
|
545
|
+
this.flattenGeneratedUniqueField(model, where);
|
|
546
|
+
// query with policy guard
|
|
547
|
+
where = this.and(where, guard);
|
|
548
|
+
const query = { select, where };
|
|
549
|
+
if (this.shouldLogQuery) {
|
|
550
|
+
this.logger.info(`[policy] checking ${model} for ${operation}, \`findFirst\`:\n${(0, utils_1.formatObject)(query)}`);
|
|
551
|
+
}
|
|
552
|
+
const result = yield db[model].findFirst(query);
|
|
553
|
+
if (!result) {
|
|
554
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`);
|
|
555
|
+
}
|
|
556
|
+
if (schema) {
|
|
557
|
+
// TODO: push down schema check to the database
|
|
558
|
+
const parseResult = schema.safeParse(result);
|
|
559
|
+
if (!parseResult.success) {
|
|
560
|
+
const error = (0, zod_validation_error_1.fromZodError)(parseResult.error);
|
|
561
|
+
if (this.logger.enabled('info')) {
|
|
562
|
+
this.logger.info(`entity ${model} failed validation for operation ${operation}: ${error}`);
|
|
347
563
|
}
|
|
564
|
+
throw this.deniedByPolicy(model, operation, `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
|
|
348
565
|
}
|
|
349
566
|
}
|
|
350
567
|
});
|
|
351
568
|
}
|
|
569
|
+
getFieldUpdateGuards(db, model, args) {
|
|
570
|
+
var _a;
|
|
571
|
+
const allFieldGuards = [];
|
|
572
|
+
for (const [k, v] of Object.entries((_a = args.data) !== null && _a !== void 0 ? _a : args)) {
|
|
573
|
+
if (typeof v === 'undefined') {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, k);
|
|
577
|
+
if (this.isFalse(fieldGuard)) {
|
|
578
|
+
return { guard: allFieldGuards, rejectedByField: k };
|
|
579
|
+
}
|
|
580
|
+
allFieldGuards.push(fieldGuard);
|
|
581
|
+
}
|
|
582
|
+
return { guard: this.and(...allFieldGuards), rejectedByField: undefined };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Tries rejecting a request based on static "false" policy.
|
|
586
|
+
*/
|
|
587
|
+
tryReject(db, model, operation) {
|
|
588
|
+
const guard = this.getAuthGuard(db, model, operation);
|
|
589
|
+
if (this.isFalse(guard)) {
|
|
590
|
+
throw this.deniedByPolicy(model, operation);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
352
593
|
/**
|
|
353
|
-
*
|
|
594
|
+
* Checks if a model exists given a unique filter.
|
|
354
595
|
*/
|
|
355
|
-
|
|
596
|
+
checkExistence(db, model, uniqueFilter, throwIfNotFound = false) {
|
|
356
597
|
return __awaiter(this, void 0, void 0, function* () {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// values before update, so we can post-check if they satisfy
|
|
362
|
-
// model => { ids, entity value }
|
|
363
|
-
const updatedModels = new Map();
|
|
364
|
-
function addUpdatedEntity(model, ids, entity) {
|
|
365
|
-
let modelEntities = updatedModels.get(model);
|
|
366
|
-
if (!modelEntities) {
|
|
367
|
-
modelEntities = [];
|
|
368
|
-
updatedModels.set(model, modelEntities);
|
|
369
|
-
}
|
|
370
|
-
modelEntities.push({ ids, value: entity });
|
|
371
|
-
}
|
|
372
|
-
const idFields = this.getIdFields(model);
|
|
373
|
-
if (args.select) {
|
|
374
|
-
// make sure id fields are selected, we need it to
|
|
375
|
-
// read back the updated entity
|
|
376
|
-
for (const idField of idFields) {
|
|
377
|
-
if (!args.select[idField.name]) {
|
|
378
|
-
args.select[idField.name] = true;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
598
|
+
uniqueFilter = this.clone(uniqueFilter);
|
|
599
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
600
|
+
if (this.shouldLogQuery) {
|
|
601
|
+
this.logger.info(`[policy] checking ${model} existence, \`findFirst\`:\n${(0, utils_1.formatObject)(uniqueFilter)}`);
|
|
381
602
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// args processor for create
|
|
386
|
-
const processCreate = (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
387
|
-
const guard = yield this.getAuthGuard(model, 'create');
|
|
388
|
-
const schema = yield this.getModelSchema(model);
|
|
389
|
-
if (guard === false) {
|
|
390
|
-
throw this.deniedByPolicy(model, 'create');
|
|
391
|
-
}
|
|
392
|
-
else if (guard !== true || schema) {
|
|
393
|
-
// mark the create with a transaction tag so we can check them later
|
|
394
|
-
args[constants_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
|
|
395
|
-
createdModels.add(model);
|
|
396
|
-
}
|
|
603
|
+
const existing = yield db[model].findFirst({
|
|
604
|
+
where: uniqueFilter,
|
|
605
|
+
select: this.makeIdSelection(model),
|
|
397
606
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
if (!where) {
|
|
471
|
-
throw this.unknownError(`Missing 'where' parameter`);
|
|
472
|
-
}
|
|
473
|
-
yield this.checkPolicyForFilter(model, where, 'update', this.db);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
yield preparePostUpdateCheck(model, context);
|
|
477
|
-
});
|
|
478
|
-
// args processor for updateMany
|
|
479
|
-
const processUpdateMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
480
|
-
const guard = yield this.getAuthGuard(model, 'update');
|
|
481
|
-
if (guard === false) {
|
|
482
|
-
throw this.deniedByPolicy(model, 'update');
|
|
483
|
-
}
|
|
484
|
-
else if (guard !== true) {
|
|
485
|
-
// inject policy filter
|
|
486
|
-
yield this.injectAuthGuard(args, model, 'update');
|
|
487
|
-
}
|
|
488
|
-
yield preparePostUpdateCheck(model, context);
|
|
489
|
-
});
|
|
490
|
-
// for models with post-update rules, we need to read and store
|
|
491
|
-
// entity values before the update for post-update check
|
|
492
|
-
const preparePostUpdateCheck = (model, context) => __awaiter(this, void 0, void 0, function* () {
|
|
493
|
-
const postGuard = yield this.getAuthGuard(model, 'postUpdate');
|
|
494
|
-
const schema = yield this.getModelSchema(model);
|
|
495
|
-
// post-update check is needed if there's post-update rule or validation schema
|
|
496
|
-
if (postGuard !== true || schema) {
|
|
497
|
-
// fetch preValue selection (analyzed from the post-update rules)
|
|
498
|
-
const preValueSelect = yield this.getPreValueSelect(model);
|
|
499
|
-
const filter = yield buildReversedQuery(context);
|
|
500
|
-
// query args will be used with findMany, so we need to
|
|
501
|
-
// translate unique constraint filters into a flat filter
|
|
502
|
-
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
503
|
-
yield this.flattenGeneratedUniqueField(model, filter);
|
|
504
|
-
const idFields = this.getIdFields(model);
|
|
505
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
506
|
-
const select = Object.assign({}, preValueSelect);
|
|
507
|
-
for (const idField of idFields) {
|
|
508
|
-
select[idField.name] = true;
|
|
509
|
-
}
|
|
510
|
-
const query = { where: filter, select };
|
|
511
|
-
if (this.shouldLogQuery) {
|
|
512
|
-
this.logger.info(`[withPolicy] \`findMany\` for fetching pre-update entities:\n${(0, utils_1.formatObject)(args)}`);
|
|
513
|
-
}
|
|
514
|
-
const entities = yield this.db[model].findMany(query);
|
|
515
|
-
entities.forEach((entity) => {
|
|
516
|
-
addUpdatedEntity(model, this.getEntityIds(model, entity), entity);
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
// args processor for delete
|
|
521
|
-
const processDelete = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
522
|
-
const guard = yield this.getAuthGuard(model, 'delete');
|
|
523
|
-
if (guard === false) {
|
|
524
|
-
throw this.deniedByPolicy(model, 'delete');
|
|
525
|
-
}
|
|
526
|
-
else if (guard !== true) {
|
|
527
|
-
if (this.isToOneRelation(context.field)) {
|
|
528
|
-
// see comments in processUpdate
|
|
529
|
-
const subQuery = yield buildReversedQuery(context);
|
|
530
|
-
yield this.checkPolicyForFilter(model, subQuery, 'delete', this.db);
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
yield this.checkPolicyForFilter(model, args, 'delete', this.db);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
// process relation updates: connect, connectOrCreate, and disconnect
|
|
538
|
-
const processRelationUpdate = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
539
|
-
var _a;
|
|
540
|
-
if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
|
|
541
|
-
// fetch the backlink field of the model being connected
|
|
542
|
-
const backLinkField = (0, model_meta_1.resolveField)(this.modelMeta, model, context.field.backLink);
|
|
543
|
-
if (backLinkField.isRelationOwner) {
|
|
544
|
-
// the target side of relation owns the relation,
|
|
545
|
-
// mark it as updated
|
|
546
|
-
yield processUpdate(model, args, context);
|
|
607
|
+
if (!existing && throwIfNotFound) {
|
|
608
|
+
throw this.notFound(model);
|
|
609
|
+
}
|
|
610
|
+
return existing;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Returns an entity given a unique filter with read policy checked. Reject if not readable.
|
|
615
|
+
*/
|
|
616
|
+
readBack(db, model, operation, selectInclude, uniqueFilter) {
|
|
617
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
618
|
+
uniqueFilter = this.clone(uniqueFilter);
|
|
619
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
620
|
+
const readArgs = { select: selectInclude.select, include: selectInclude.include, where: uniqueFilter };
|
|
621
|
+
const error = this.deniedByPolicy(model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
|
|
622
|
+
const injectResult = yield this.injectForRead(db, model, readArgs);
|
|
623
|
+
if (!injectResult) {
|
|
624
|
+
return { error, result: undefined };
|
|
625
|
+
}
|
|
626
|
+
// inject select needed for field-level read checks
|
|
627
|
+
this.injectReadCheckSelect(model, readArgs);
|
|
628
|
+
if (this.shouldLogQuery) {
|
|
629
|
+
this.logger.info(`[policy] checking read-back, \`findFirst\` ${model}:\n${(0, utils_1.formatObject)(readArgs)}`);
|
|
630
|
+
}
|
|
631
|
+
const result = yield db[model].findFirst(readArgs);
|
|
632
|
+
if (!result) {
|
|
633
|
+
return { error, result: undefined };
|
|
634
|
+
}
|
|
635
|
+
this.postProcessForRead(result, model, selectInclude);
|
|
636
|
+
return { result, error: undefined };
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Injects field selection needed for checking field-level read policy into query args.
|
|
641
|
+
* @returns
|
|
642
|
+
*/
|
|
643
|
+
injectReadCheckSelect(model, args) {
|
|
644
|
+
if (!this.hasFieldLevelPolicy(model)) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const readFieldSelect = this.getReadFieldSelect(model);
|
|
648
|
+
if (!readFieldSelect) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.doInjectReadCheckSelect(model, args, { select: readFieldSelect });
|
|
652
|
+
}
|
|
653
|
+
doInjectReadCheckSelect(model, args, input) {
|
|
654
|
+
if (!(input === null || input === void 0 ? void 0 : input.select)) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
let target; // injection target
|
|
658
|
+
let isInclude = false; // if the target is include or select
|
|
659
|
+
if (args.select) {
|
|
660
|
+
target = args.select;
|
|
661
|
+
isInclude = false;
|
|
662
|
+
}
|
|
663
|
+
else if (args.include) {
|
|
664
|
+
target = args.include;
|
|
665
|
+
isInclude = true;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
target = args.select = this.makeAllScalarFieldSelect(model);
|
|
669
|
+
isInclude = false;
|
|
670
|
+
}
|
|
671
|
+
if (!isInclude) {
|
|
672
|
+
// merge selects
|
|
673
|
+
for (const [k, v] of Object.entries(input.select)) {
|
|
674
|
+
if (v === true) {
|
|
675
|
+
if (!target[k]) {
|
|
676
|
+
target[k] = true;
|
|
547
677
|
}
|
|
548
678
|
}
|
|
549
|
-
});
|
|
550
|
-
// use a visitor to process args before conducting the write action
|
|
551
|
-
const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
|
|
552
|
-
create: (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
553
|
-
yield processCreate(model, args);
|
|
554
|
-
}),
|
|
555
|
-
connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
556
|
-
if (args.create) {
|
|
557
|
-
yield processCreate(model, args.create);
|
|
558
|
-
}
|
|
559
|
-
if (args.where) {
|
|
560
|
-
yield processRelationUpdate(model, args.where, context);
|
|
561
|
-
}
|
|
562
|
-
}),
|
|
563
|
-
connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
564
|
-
yield processRelationUpdate(model, args, context);
|
|
565
|
-
}),
|
|
566
|
-
disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
567
|
-
yield processRelationUpdate(model, args, context);
|
|
568
|
-
}),
|
|
569
|
-
update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
570
|
-
yield processUpdate(model, args.where, context);
|
|
571
|
-
}),
|
|
572
|
-
updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
573
|
-
yield processUpdateMany(model, args, context);
|
|
574
|
-
}),
|
|
575
|
-
upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
576
|
-
if (args.create) {
|
|
577
|
-
yield processCreate(model, args.create);
|
|
578
|
-
}
|
|
579
|
-
if (args.update) {
|
|
580
|
-
yield processUpdate(model, args.where, context);
|
|
581
|
-
}
|
|
582
|
-
}),
|
|
583
|
-
delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
584
|
-
yield processDelete(model, args, context);
|
|
585
|
-
}),
|
|
586
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
587
|
-
deleteMany: (model, args, _context) => __awaiter(this, void 0, void 0, function* () {
|
|
588
|
-
const guard = yield this.getAuthGuard(model, 'delete');
|
|
589
|
-
if (guard === false) {
|
|
590
|
-
throw this.deniedByPolicy(model, 'delete');
|
|
591
|
-
}
|
|
592
|
-
else if (guard !== true) {
|
|
593
|
-
if (args.where) {
|
|
594
|
-
args.where = this.and(args.where, guard);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
const copy = (0, deepcopy_1.default)(args);
|
|
598
|
-
for (const key of Object.keys(args)) {
|
|
599
|
-
delete args[key];
|
|
600
|
-
}
|
|
601
|
-
const combined = this.and(copy, guard);
|
|
602
|
-
Object.assign(args, combined);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}),
|
|
606
|
-
});
|
|
607
|
-
yield visitor.visit(model, action, args);
|
|
608
|
-
if (createdModels.size === 0 && updatedModels.size === 0) {
|
|
609
|
-
// no post-check needed, we can proceed with the write without transaction
|
|
610
|
-
return yield writeAction(this.db[model], args);
|
|
611
679
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
680
|
+
}
|
|
681
|
+
// recurse into nested selects (relation fields)
|
|
682
|
+
for (const [k, v] of Object.entries(input.select)) {
|
|
683
|
+
if (typeof v === 'object' && (v === null || v === void 0 ? void 0 : v.select)) {
|
|
684
|
+
const field = (0, model_meta_1.resolveField)(this.modelMeta, model, k);
|
|
685
|
+
if (field === null || field === void 0 ? void 0 : field.isDataModel) {
|
|
686
|
+
// recurse into relation
|
|
687
|
+
if (isInclude && target[k] === true) {
|
|
688
|
+
// select all fields for the relation
|
|
689
|
+
target[k] = { select: this.makeAllScalarFieldSelect(field.type) };
|
|
619
690
|
}
|
|
620
|
-
if (
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
.map(([model, modelEntities]) => modelEntities.map(({ ids, value: preValue }) => __awaiter(this, void 0, void 0, function* () { return this.checkPostUpdate(model, ids, tx, preValue); })))
|
|
624
|
-
.flat());
|
|
691
|
+
else if (!target[k]) {
|
|
692
|
+
// ensure an empty select clause
|
|
693
|
+
target[k] = { select: {} };
|
|
625
694
|
}
|
|
626
|
-
|
|
627
|
-
|
|
695
|
+
// recurse
|
|
696
|
+
this.doInjectReadCheckSelect(field.type, target[k], v);
|
|
697
|
+
}
|
|
628
698
|
}
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
transaction(db, action) {
|
|
632
|
-
if (db.__zenstack_tx) {
|
|
633
|
-
// already in transaction, don't nest
|
|
634
|
-
return action(db);
|
|
635
699
|
}
|
|
636
|
-
|
|
637
|
-
|
|
700
|
+
}
|
|
701
|
+
makeAllScalarFieldSelect(model) {
|
|
702
|
+
const fields = this.modelMeta.fields[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
703
|
+
const result = {};
|
|
704
|
+
if (fields) {
|
|
705
|
+
Object.entries(fields).forEach(([k, v]) => {
|
|
706
|
+
if (!v.isDataModel) {
|
|
707
|
+
result[k] = true;
|
|
708
|
+
}
|
|
709
|
+
});
|
|
638
710
|
}
|
|
711
|
+
return result;
|
|
639
712
|
}
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region Errors
|
|
640
715
|
deniedByPolicy(model, operation, extra, reason) {
|
|
641
|
-
return (0, utils_1.prismaClientKnownRequestError)(this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code:
|
|
716
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINED_FAILED, meta: { reason } });
|
|
642
717
|
}
|
|
643
718
|
notFound(model) {
|
|
644
719
|
return (0, utils_1.prismaClientKnownRequestError)(this.db, `entity not found for model ${model}`, {
|
|
@@ -646,122 +721,151 @@ class PolicyUtil {
|
|
|
646
721
|
code: 'P2025',
|
|
647
722
|
});
|
|
648
723
|
}
|
|
724
|
+
validationError(message) {
|
|
725
|
+
return (0, utils_1.prismaClientValidationError)(this.db, message, {
|
|
726
|
+
clientVersion: (0, version_1.getVersion)(),
|
|
727
|
+
});
|
|
728
|
+
}
|
|
649
729
|
unknownError(message) {
|
|
650
730
|
return (0, utils_1.prismaClientUnknownRequestError)(this.db, message, {
|
|
651
731
|
clientVersion: (0, version_1.getVersion)(),
|
|
652
732
|
});
|
|
653
733
|
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region Misc
|
|
654
736
|
/**
|
|
655
|
-
*
|
|
656
|
-
* in data being trimmed, and if so, throw an error.
|
|
737
|
+
* Gets field selection for fetching pre-update entity values for the given model.
|
|
657
738
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
739
|
+
getPreValueSelect(model) {
|
|
740
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
741
|
+
if (!guard) {
|
|
742
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
743
|
+
}
|
|
744
|
+
return guard[constants_1.PRE_UPDATE_VALUE_SELECTOR];
|
|
745
|
+
}
|
|
746
|
+
getReadFieldSelect(model) {
|
|
747
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
748
|
+
if (!guard) {
|
|
749
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
750
|
+
}
|
|
751
|
+
return guard[constants_1.FIELD_LEVEL_READ_CHECKER_SELECTOR];
|
|
752
|
+
}
|
|
753
|
+
checkReadField(model, field, entity) {
|
|
754
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
755
|
+
if (!guard) {
|
|
756
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
757
|
+
}
|
|
758
|
+
const func = guard[`${constants_1.FIELD_LEVEL_READ_CHECKER_PREFIX}${field}`];
|
|
759
|
+
if (!func) {
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
return func(entity, { user: this.user });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
hasFieldValidation(model) {
|
|
767
|
+
var _a, _b;
|
|
768
|
+
return ((_b = (_a = this.policy.validation) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _b === void 0 ? void 0 : _b.hasValidation) === true;
|
|
769
|
+
}
|
|
770
|
+
hasFieldLevelPolicy(model) {
|
|
771
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
772
|
+
if (!guard) {
|
|
773
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
774
|
+
}
|
|
775
|
+
return !!guard[constants_1.HAS_FIELD_LEVEL_POLICY_FLAG];
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Gets Zod schema for the given model and access kind.
|
|
779
|
+
*
|
|
780
|
+
* @param kind If undefined, returns the full schema.
|
|
781
|
+
*/
|
|
782
|
+
getZodSchema(model, kind = undefined) {
|
|
783
|
+
var _a, _b;
|
|
784
|
+
if (!this.hasFieldValidation(model)) {
|
|
785
|
+
return undefined;
|
|
786
|
+
}
|
|
787
|
+
const schemaKey = `${(0, upper_case_first_1.upperCaseFirst)(model)}${kind ? (0, upper_case_first_1.upperCaseFirst)(kind) : ''}Schema`;
|
|
788
|
+
return (_b = (_a = this.zodSchemas) === null || _a === void 0 ? void 0 : _a.models) === null || _b === void 0 ? void 0 : _b[schemaKey];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Post processing checks and clean-up for read model entities.
|
|
792
|
+
*/
|
|
793
|
+
postProcessForRead(data, model, queryArgs) {
|
|
794
|
+
// preserve the original data as it may be needed for checking field-level readability,
|
|
795
|
+
// while the "data" will be manipulated during traversal (deleting unreadable fields)
|
|
796
|
+
const origData = this.clone(data);
|
|
797
|
+
this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
|
|
798
|
+
}
|
|
799
|
+
doPostProcessForRead(data, model, fullData, queryArgs, hasFieldLevelPolicy, path = '') {
|
|
800
|
+
var _a, _b;
|
|
801
|
+
if (data === null || data === undefined) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
for (const [entityData, entityFullData] of (0, utils_1.zip)(data, fullData)) {
|
|
805
|
+
if (typeof entityData !== 'object' || !entityData) {
|
|
683
806
|
return;
|
|
684
807
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
808
|
+
// strip auxiliary fields
|
|
809
|
+
for (const auxField of constants_1.AUXILIARY_FIELDS) {
|
|
810
|
+
if (auxField in entityData) {
|
|
811
|
+
delete entityData[auxField];
|
|
812
|
+
}
|
|
688
813
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
// we've got schemas, so have to fetch entities and validate them
|
|
693
|
-
// if (this.shouldLogQuery) {
|
|
694
|
-
// this.logger.info(
|
|
695
|
-
// `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}`
|
|
696
|
-
// );
|
|
697
|
-
// }
|
|
698
|
-
const entities = yield db[model].findMany(guardedQuery);
|
|
699
|
-
if (entities.length < count) {
|
|
700
|
-
if (this.logger.enabled('info')) {
|
|
701
|
-
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
702
|
-
}
|
|
703
|
-
throw this.deniedByPolicy(model, operation, `${count - entities.length} ${(0, pluralize_1.default)('entity', count - entities.length)} failed policy check`);
|
|
814
|
+
for (const [field, fieldData] of Object.entries(entityData)) {
|
|
815
|
+
if (fieldData === undefined) {
|
|
816
|
+
continue;
|
|
704
817
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (this.logger.enabled('info')) {
|
|
710
|
-
this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
|
|
711
|
-
}
|
|
712
|
-
throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`);
|
|
818
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
819
|
+
if (!fieldInfo) {
|
|
820
|
+
// could be _count, etc.
|
|
821
|
+
continue;
|
|
713
822
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
823
|
+
if (hasFieldLevelPolicy) {
|
|
824
|
+
// 1. remove fields selected for checking field-level policies but not selected by the original query args
|
|
825
|
+
// 2. evaluate field-level policies and remove fields that are not readable
|
|
826
|
+
if (!fieldInfo.isDataModel) {
|
|
827
|
+
// scalar field, delete unselected ones
|
|
828
|
+
const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
|
|
829
|
+
if (select && typeof select === 'object' && select[field] !== true) {
|
|
830
|
+
// there's a select clause but this field is not included
|
|
831
|
+
delete entityData[field];
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
726
834
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (this.logger.enabled('info')) {
|
|
745
|
-
this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
|
|
746
|
-
}
|
|
747
|
-
throw this.deniedByPolicy(model, 'postUpdate');
|
|
748
|
-
}
|
|
749
|
-
// TODO: push down schema check to the database
|
|
750
|
-
const schema = yield this.getModelSchema(model);
|
|
751
|
-
if (schema) {
|
|
752
|
-
const schemaCheckResult = schema.safeParse(entity);
|
|
753
|
-
if (!schemaCheckResult.success) {
|
|
754
|
-
const error = (0, zod_validation_error_1.fromZodError)(schemaCheckResult.error).message;
|
|
755
|
-
if (this.logger.enabled('info')) {
|
|
756
|
-
this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
|
|
835
|
+
else {
|
|
836
|
+
// relation field, delete if not selected or included
|
|
837
|
+
const include = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include;
|
|
838
|
+
const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
|
|
839
|
+
if (!(include === null || include === void 0 ? void 0 : include[field]) && !(select === null || select === void 0 ? void 0 : select[field])) {
|
|
840
|
+
// relation field not included or selected
|
|
841
|
+
delete entityData[field];
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// delete unreadable fields
|
|
846
|
+
if (!this.checkReadField(model, field, entityFullData)) {
|
|
847
|
+
if (this.shouldLogQuery) {
|
|
848
|
+
this.logger.info(`[policy] dropping unreadable field ${path ? path + '.' : ''}${field}`);
|
|
849
|
+
}
|
|
850
|
+
delete entityData[field];
|
|
851
|
+
continue;
|
|
757
852
|
}
|
|
758
|
-
|
|
853
|
+
}
|
|
854
|
+
if (fieldInfo.isDataModel) {
|
|
855
|
+
// recurse into nested fields
|
|
856
|
+
const nextArgs = (_b = ((_a = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select) !== null && _a !== void 0 ? _a : queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include)) === null || _b === void 0 ? void 0 : _b[field];
|
|
857
|
+
this.doPostProcessForRead(fieldData, fieldInfo.type, entityFullData[field], nextArgs, hasFieldLevelPolicy, path ? path + '.' + field : field);
|
|
759
858
|
}
|
|
760
859
|
}
|
|
761
|
-
}
|
|
860
|
+
}
|
|
762
861
|
}
|
|
763
|
-
|
|
764
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Gets information for a specific model field.
|
|
864
|
+
*/
|
|
865
|
+
getModelField(model, field) {
|
|
866
|
+
var _a;
|
|
867
|
+
model = (0, lower_case_first_1.lowerCaseFirst)(model);
|
|
868
|
+
return (_a = this.modelMeta.fields[model]) === null || _a === void 0 ? void 0 : _a[field];
|
|
765
869
|
}
|
|
766
870
|
/**
|
|
767
871
|
* Clones an object and makes sure it's not empty.
|
|
@@ -770,13 +874,13 @@ class PolicyUtil {
|
|
|
770
874
|
return value ? (0, deepcopy_1.default)(value) : {};
|
|
771
875
|
}
|
|
772
876
|
/**
|
|
773
|
-
* Gets "id"
|
|
877
|
+
* Gets "id" fields for a given model.
|
|
774
878
|
*/
|
|
775
879
|
getIdFields(model) {
|
|
776
880
|
return (0, utils_1.getIdFields)(this.modelMeta, model, true);
|
|
777
881
|
}
|
|
778
882
|
/**
|
|
779
|
-
* Gets id field
|
|
883
|
+
* Gets id field values from an entity.
|
|
780
884
|
*/
|
|
781
885
|
getEntityIds(model, entityData) {
|
|
782
886
|
const idFields = this.getIdFields(model);
|
|
@@ -786,8 +890,37 @@ class PolicyUtil {
|
|
|
786
890
|
}
|
|
787
891
|
return result;
|
|
788
892
|
}
|
|
789
|
-
|
|
790
|
-
|
|
893
|
+
/**
|
|
894
|
+
* Creates a selection object for id fields for the given model.
|
|
895
|
+
*/
|
|
896
|
+
makeIdSelection(model) {
|
|
897
|
+
const idFields = this.getIdFields(model);
|
|
898
|
+
return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true })));
|
|
899
|
+
}
|
|
900
|
+
mergeWhereClause(where, extra) {
|
|
901
|
+
var _a;
|
|
902
|
+
if (!where) {
|
|
903
|
+
throw new Error('invalid where clause');
|
|
904
|
+
}
|
|
905
|
+
extra = this.reduce(extra);
|
|
906
|
+
if (this.isTrue(extra)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// instead of simply wrapping with AND, we preserve the structure
|
|
910
|
+
// of the original where clause and merge `extra` into it so that
|
|
911
|
+
// unique query can continue working
|
|
912
|
+
if (where.AND) {
|
|
913
|
+
// merge into existing AND clause
|
|
914
|
+
const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND];
|
|
915
|
+
conditions.push(extra);
|
|
916
|
+
const combined = this.and(...conditions);
|
|
917
|
+
// make sure the merging always goes under AND
|
|
918
|
+
where.AND = (_a = combined.AND) !== null && _a !== void 0 ? _a : combined;
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
// insert an AND clause
|
|
922
|
+
where.AND = [extra];
|
|
923
|
+
}
|
|
791
924
|
}
|
|
792
925
|
}
|
|
793
926
|
exports.PolicyUtil = PolicyUtil;
|