@zenstackhq/runtime 1.0.0-beta.20 → 1.0.0-beta.22
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/constants.d.ts +9 -13
- package/constants.js +10 -14
- package/constants.js.map +1 -1
- package/enhancements/enhance.js +2 -2
- package/enhancements/enhance.js.map +1 -1
- package/enhancements/index.d.ts +1 -1
- package/enhancements/index.js +1 -1
- package/enhancements/index.js.map +1 -1
- package/enhancements/model-meta.d.ts +0 -4
- package/enhancements/model-meta.js +3 -28
- package/enhancements/model-meta.js.map +1 -1
- package/enhancements/{nested-write-vistor.js → nested-write-visitor.js} +1 -1
- package/enhancements/nested-write-visitor.js.map +1 -0
- package/enhancements/omit.d.ts +3 -3
- package/enhancements/omit.js +2 -1
- package/enhancements/omit.js.map +1 -1
- package/enhancements/password.d.ts +4 -4
- package/enhancements/password.js +4 -4
- package/enhancements/password.js.map +1 -1
- package/enhancements/policy/handler.d.ts +13 -7
- package/enhancements/policy/handler.js +134 -107
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/index.d.ts +4 -4
- package/enhancements/policy/index.js +4 -40
- package/enhancements/policy/index.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +10 -5
- package/enhancements/policy/policy-utils.js +306 -276
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/policy/promise.d.ts +5 -0
- package/enhancements/policy/promise.js +42 -0
- package/enhancements/policy/promise.js.map +1 -0
- package/enhancements/proxy.js +27 -21
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +9 -0
- package/enhancements/utils.d.ts +1 -1
- package/enhancements/utils.js +3 -4
- package/enhancements/utils.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/loader.d.ts +22 -0
- package/loader.js +86 -0
- package/loader.js.map +1 -0
- package/package.json +1 -1
- package/types.d.ts +14 -13
- package/types.js.map +1 -1
- package/validation.d.ts +5 -0
- package/validation.js +13 -1
- package/validation.js.map +1 -1
- package/zod/index.d.ts +1 -0
- package/zod/index.js +1 -0
- package/zod/objects.d.ts +1 -0
- package/zod/objects.js +8 -0
- package/enhancements/nested-write-vistor.js.map +0 -1
- /package/enhancements/{nested-write-vistor.d.ts → nested-write-visitor.d.ts} +0 -0
|
@@ -41,13 +41,31 @@ class PolicyUtil {
|
|
|
41
41
|
* Creates a conjunction of a list of query conditions.
|
|
42
42
|
*/
|
|
43
43
|
and(...conditions) {
|
|
44
|
-
|
|
44
|
+
const filtered = conditions.filter((c) => c !== undefined);
|
|
45
|
+
if (filtered.length === 0) {
|
|
46
|
+
return this.makeTrue();
|
|
47
|
+
}
|
|
48
|
+
else if (filtered.length === 1) {
|
|
49
|
+
return this.reduce(filtered[0]);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
return this.reduce({ AND: filtered });
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
56
|
* Creates a disjunction of a list of query conditions.
|
|
48
57
|
*/
|
|
49
58
|
or(...conditions) {
|
|
50
|
-
|
|
59
|
+
const filtered = conditions.filter((c) => c !== undefined);
|
|
60
|
+
if (filtered.length === 0) {
|
|
61
|
+
return this.makeFalse();
|
|
62
|
+
}
|
|
63
|
+
else if (filtered.length === 1) {
|
|
64
|
+
return this.reduce(filtered[0]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
return this.reduce({ OR: filtered });
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
/**
|
|
53
71
|
* Creates a negation of a query condition.
|
|
@@ -95,49 +113,72 @@ class PolicyUtil {
|
|
|
95
113
|
if (condition === false) {
|
|
96
114
|
return this.makeFalse();
|
|
97
115
|
}
|
|
98
|
-
if (
|
|
99
|
-
|
|
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
|
-
}
|
|
116
|
+
if (condition === null) {
|
|
117
|
+
return condition;
|
|
112
118
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
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();
|
|
119
|
+
const result = {};
|
|
120
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
121
|
+
if (value === null || value === undefined) {
|
|
122
|
+
result[key] = value;
|
|
123
|
+
continue;
|
|
135
124
|
}
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
switch (key) {
|
|
126
|
+
case 'AND': {
|
|
127
|
+
const children = (0, utils_1.enumerate)(value)
|
|
128
|
+
.map((c) => this.reduce(c))
|
|
129
|
+
.filter((c) => c !== undefined && !this.isTrue(c));
|
|
130
|
+
if (children.length === 0) {
|
|
131
|
+
result[key] = []; // true
|
|
132
|
+
}
|
|
133
|
+
else if (children.some((c) => this.isFalse(c))) {
|
|
134
|
+
result['OR'] = []; // false
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (!this.isTrue({ AND: result[key] })) {
|
|
138
|
+
// use AND only if it's not already true
|
|
139
|
+
result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'OR': {
|
|
145
|
+
const children = (0, utils_1.enumerate)(value)
|
|
146
|
+
.map((c) => this.reduce(c))
|
|
147
|
+
.filter((c) => c !== undefined && !this.isFalse(c));
|
|
148
|
+
if (children.length === 0) {
|
|
149
|
+
result[key] = []; // false
|
|
150
|
+
}
|
|
151
|
+
else if (children.some((c) => this.isTrue(c))) {
|
|
152
|
+
result['AND'] = []; // true
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!this.isFalse({ OR: result[key] })) {
|
|
156
|
+
// use OR only if it's not already false
|
|
157
|
+
result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'NOT': {
|
|
163
|
+
result[key] = this.reduce(value);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
default: {
|
|
167
|
+
const booleanKeys = ['AND', 'OR', 'NOT', 'is', 'isNot', 'none', 'every', 'some'];
|
|
168
|
+
if (typeof value === 'object' &&
|
|
169
|
+
value &&
|
|
170
|
+
// recurse only if the value has at least one boolean key
|
|
171
|
+
Object.keys(value).some((k) => booleanKeys.includes(k))) {
|
|
172
|
+
result[key] = this.reduce(value);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
result[key] = value;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
138
179
|
}
|
|
139
180
|
}
|
|
140
|
-
return
|
|
181
|
+
return result;
|
|
141
182
|
}
|
|
142
183
|
//#endregion
|
|
143
184
|
//# Auth guard
|
|
@@ -214,143 +255,131 @@ class PolicyUtil {
|
|
|
214
255
|
* Injects model auth guard as where clause.
|
|
215
256
|
*/
|
|
216
257
|
injectAuthGuard(db, args, model, operation) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
258
|
+
let guard = this.getAuthGuard(db, model, operation);
|
|
259
|
+
if (this.isFalse(guard)) {
|
|
260
|
+
args.where = this.makeFalse();
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
if (operation === 'update' && args) {
|
|
264
|
+
// merge field-level policy guards
|
|
265
|
+
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
266
|
+
if (fieldUpdateGuard.rejectedByField) {
|
|
267
|
+
// rejected
|
|
220
268
|
args.where = this.makeFalse();
|
|
221
269
|
return false;
|
|
222
270
|
}
|
|
223
|
-
if (
|
|
224
|
-
// merge
|
|
225
|
-
|
|
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
|
-
}
|
|
236
|
-
if (args.where) {
|
|
237
|
-
// inject into relation fields:
|
|
238
|
-
// to-many: some/none/every
|
|
239
|
-
// to-one: direct-conditions/is/isNot
|
|
240
|
-
yield this.injectGuardForRelationFields(db, model, args.where, operation);
|
|
271
|
+
else if (fieldUpdateGuard.guard) {
|
|
272
|
+
// merge
|
|
273
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
241
274
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
275
|
+
}
|
|
276
|
+
if (args.where) {
|
|
277
|
+
// inject into relation fields:
|
|
278
|
+
// to-many: some/none/every
|
|
279
|
+
// to-one: direct-conditions/is/isNot
|
|
280
|
+
this.injectGuardForRelationFields(db, model, args.where, operation);
|
|
281
|
+
}
|
|
282
|
+
args.where = this.and(args.where, guard);
|
|
283
|
+
return true;
|
|
245
284
|
}
|
|
246
285
|
injectGuardForRelationFields(db, model, payload, operation) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const fieldInfo = yield (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
253
|
-
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (fieldInfo.isArray) {
|
|
257
|
-
yield this.injectGuardForToManyField(db, fieldInfo, subPayload, operation);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
yield this.injectGuardForToOneField(db, fieldInfo, subPayload, operation);
|
|
261
|
-
}
|
|
286
|
+
for (const [field, subPayload] of Object.entries(payload)) {
|
|
287
|
+
if (!subPayload) {
|
|
288
|
+
continue;
|
|
262
289
|
}
|
|
263
|
-
|
|
290
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
291
|
+
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (fieldInfo.isArray) {
|
|
295
|
+
this.injectGuardForToManyField(db, fieldInfo, subPayload, operation);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.injectGuardForToOneField(db, fieldInfo, subPayload, operation);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
264
301
|
}
|
|
265
302
|
injectGuardForToManyField(db, fieldInfo, payload, operation) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
});
|
|
303
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, operation);
|
|
304
|
+
if (payload.some) {
|
|
305
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload.some, operation);
|
|
306
|
+
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
307
|
+
payload.some = this.and(payload.some, guard);
|
|
308
|
+
}
|
|
309
|
+
if (payload.none) {
|
|
310
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload.none, operation);
|
|
311
|
+
// turn none into: { none: { AND: [guard, payload.none] } }
|
|
312
|
+
payload.none = this.and(payload.none, guard);
|
|
313
|
+
}
|
|
314
|
+
if (payload.every &&
|
|
315
|
+
typeof payload.every === 'object' &&
|
|
316
|
+
// ignore empty every clause
|
|
317
|
+
Object.keys(payload.every).length > 0) {
|
|
318
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload.every, operation);
|
|
319
|
+
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
|
|
320
|
+
if (!payload.none) {
|
|
321
|
+
payload.none = {};
|
|
322
|
+
}
|
|
323
|
+
payload.none = this.and(payload.none, guard, this.not(payload.every));
|
|
324
|
+
delete payload.every;
|
|
325
|
+
}
|
|
291
326
|
}
|
|
292
327
|
injectGuardForToOneField(db, fieldInfo, payload, operation) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// turn "is" into: { is: { AND: [ originalIs, guard ] }
|
|
299
|
-
payload.is = this.and(payload.is, guard);
|
|
300
|
-
}
|
|
301
|
-
if (payload.isNot) {
|
|
302
|
-
yield this.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation);
|
|
303
|
-
// turn "isNot" into: { isNot: { AND: [ originalIsNot, { NOT: guard } ] } }
|
|
304
|
-
payload.isNot = this.and(payload.isNot, this.not(guard));
|
|
305
|
-
delete payload.isNot;
|
|
306
|
-
}
|
|
328
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, operation);
|
|
329
|
+
// is|isNot and flat fields conditions are mutually exclusive
|
|
330
|
+
if (payload.is || payload.isNot) {
|
|
331
|
+
if (payload.is) {
|
|
332
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload.is, operation);
|
|
307
333
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
311
|
-
const combined = this.and((0, deepcopy_1.default)(payload), guard);
|
|
312
|
-
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
313
|
-
payload.is = combined;
|
|
334
|
+
if (payload.isNot) {
|
|
335
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload.isNot, operation);
|
|
314
336
|
}
|
|
315
|
-
|
|
337
|
+
// merge guard with existing "is": { is: [originalIs, guard] }
|
|
338
|
+
payload.is = this.and(payload.is, guard);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.injectGuardForRelationFields(db, fieldInfo.type, payload, operation);
|
|
342
|
+
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
343
|
+
const combined = this.and((0, deepcopy_1.default)(payload), guard);
|
|
344
|
+
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
345
|
+
payload.is = combined;
|
|
346
|
+
}
|
|
316
347
|
}
|
|
317
348
|
/**
|
|
318
349
|
* Injects auth guard for read operations.
|
|
319
350
|
*/
|
|
320
351
|
injectForRead(db, model, args) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
352
|
+
const injected = {};
|
|
353
|
+
if (!this.injectAuthGuard(db, injected, model, 'read')) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (args.where) {
|
|
357
|
+
// inject into relation fields:
|
|
358
|
+
// to-many: some/none/every
|
|
359
|
+
// to-one: direct-conditions/is/isNot
|
|
360
|
+
this.injectGuardForRelationFields(db, model, args.where, 'read');
|
|
361
|
+
}
|
|
362
|
+
if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
|
|
363
|
+
if (!args.where) {
|
|
364
|
+
args.where = injected.where;
|
|
325
365
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// to-many: some/none/every
|
|
329
|
-
// to-one: direct-conditions/is/isNot
|
|
330
|
-
yield this.injectGuardForRelationFields(db, model, args.where, 'read');
|
|
366
|
+
else {
|
|
367
|
+
this.mergeWhereClause(args.where, injected.where);
|
|
331
368
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
369
|
+
}
|
|
370
|
+
// recursively inject read guard conditions into nested select, include, and _count
|
|
371
|
+
const hoistedConditions = this.injectNestedReadConditions(db, model, args);
|
|
372
|
+
// the injection process may generate conditions that need to be hoisted to the toplevel,
|
|
373
|
+
// if so, merge it with the existing where
|
|
374
|
+
if (hoistedConditions.length > 0) {
|
|
375
|
+
if (!args.where) {
|
|
376
|
+
args.where = this.and(...hoistedConditions);
|
|
339
377
|
}
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
}
|
|
378
|
+
else {
|
|
379
|
+
this.mergeWhereClause(args.where, this.and(...hoistedConditions));
|
|
351
380
|
}
|
|
352
|
-
|
|
353
|
-
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
354
383
|
}
|
|
355
384
|
// flatten unique constraint filters
|
|
356
385
|
flattenGeneratedUniqueField(model, args) {
|
|
@@ -382,130 +411,126 @@ class PolicyUtil {
|
|
|
382
411
|
* Builds a reversed query for the given nested path.
|
|
383
412
|
*/
|
|
384
413
|
buildReversedQuery(context) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
414
|
+
let result, currQuery;
|
|
415
|
+
let currField;
|
|
416
|
+
for (let i = context.nestingPath.length - 1; i >= 0; i--) {
|
|
417
|
+
const { field, model, where } = context.nestingPath[i];
|
|
418
|
+
// never modify the original where because it's shared in the structure
|
|
419
|
+
const visitWhere = Object.assign({}, where);
|
|
420
|
+
if (model && where) {
|
|
421
|
+
// make sure composite unique condition is flattened
|
|
422
|
+
this.flattenGeneratedUniqueField(model, visitWhere);
|
|
423
|
+
}
|
|
424
|
+
if (!result) {
|
|
425
|
+
// first segment (bottom), just use its where clause
|
|
426
|
+
result = currQuery = Object.assign({}, visitWhere);
|
|
427
|
+
currField = field;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
if (!currField) {
|
|
431
|
+
throw this.unknownError(`missing field in nested path`);
|
|
395
432
|
}
|
|
396
|
-
if (!
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
433
|
+
if (!currField.backLink) {
|
|
434
|
+
throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
|
|
435
|
+
}
|
|
436
|
+
const backLinkField = this.getModelField(currField.type, currField.backLink);
|
|
437
|
+
if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isArray) {
|
|
438
|
+
// many-side of relationship, wrap with "some" query
|
|
439
|
+
currQuery[currField.backLink] = { some: Object.assign({}, visitWhere) };
|
|
400
440
|
}
|
|
401
441
|
else {
|
|
402
|
-
if (
|
|
403
|
-
|
|
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
|
-
}
|
|
442
|
+
if (where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping) {
|
|
443
|
+
for (const [r, fk] of Object.entries(backLinkField.foreignKeyMapping)) {
|
|
444
|
+
currQuery[fk] = visitWhere[r];
|
|
421
445
|
}
|
|
422
|
-
|
|
423
|
-
currQuery[currField.backLink] =
|
|
446
|
+
if (i > 0) {
|
|
447
|
+
currQuery[currField.backLink] = {};
|
|
424
448
|
}
|
|
425
449
|
}
|
|
426
|
-
|
|
427
|
-
|
|
450
|
+
else {
|
|
451
|
+
currQuery[currField.backLink] = Object.assign({}, visitWhere);
|
|
452
|
+
}
|
|
428
453
|
}
|
|
454
|
+
currQuery = currQuery[currField.backLink];
|
|
455
|
+
currField = field;
|
|
429
456
|
}
|
|
430
|
-
|
|
431
|
-
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
432
459
|
}
|
|
433
460
|
injectNestedReadConditions(db, model, args) {
|
|
434
461
|
var _a;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
injectTarget._count.select[k] = {};
|
|
452
|
-
}
|
|
462
|
+
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
463
|
+
if (!injectTarget) {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
if (injectTarget._count !== undefined) {
|
|
467
|
+
// _count needs to respect read policies of related models
|
|
468
|
+
if (injectTarget._count === true) {
|
|
469
|
+
// include count for all relations, expand to all fields
|
|
470
|
+
// so that we can inject guard conditions for each of them
|
|
471
|
+
injectTarget._count = { select: {} };
|
|
472
|
+
const modelFields = (0, model_meta_1.getFields)(this.modelMeta, model);
|
|
473
|
+
if (modelFields) {
|
|
474
|
+
for (const [k, v] of Object.entries(modelFields)) {
|
|
475
|
+
if (v.isDataModel && v.isArray) {
|
|
476
|
+
// create an entry for to-many relation
|
|
477
|
+
injectTarget._count.select[k] = {};
|
|
453
478
|
}
|
|
454
479
|
}
|
|
455
480
|
}
|
|
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
|
-
}
|
|
468
481
|
}
|
|
469
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
482
|
+
// inject conditions for each relation
|
|
483
|
+
for (const field of Object.keys(injectTarget._count.select)) {
|
|
484
|
+
if (typeof injectTarget._count.select[field] !== 'object') {
|
|
485
|
+
injectTarget._count.select[field] = {};
|
|
486
|
+
}
|
|
472
487
|
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
473
|
-
if (!fieldInfo
|
|
474
|
-
// only care about relation fields
|
|
488
|
+
if (!fieldInfo) {
|
|
475
489
|
continue;
|
|
476
490
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
// inject into the "where" clause inside select
|
|
492
|
+
this.injectAuthGuard(db, injectTarget._count.select[field], fieldInfo.type, 'read');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// collect filter conditions that should be hoisted to the toplevel
|
|
496
|
+
const hoistedConditions = [];
|
|
497
|
+
for (const field of (0, utils_1.getModelFields)(injectTarget)) {
|
|
498
|
+
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
499
|
+
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
500
|
+
// only care about relation fields
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
let hoisted;
|
|
504
|
+
if (fieldInfo.isArray ||
|
|
505
|
+
// Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0
|
|
506
|
+
// https://github.com/prisma/prisma/discussions/20350
|
|
507
|
+
fieldInfo.isOptional) {
|
|
508
|
+
if (typeof injectTarget[field] !== 'object') {
|
|
509
|
+
injectTarget[field] = {};
|
|
493
510
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
511
|
+
// inject extra condition for to-many or nullable to-one relation
|
|
512
|
+
this.injectAuthGuard(db, injectTarget[field], fieldInfo.type, 'read');
|
|
513
|
+
// recurse
|
|
514
|
+
const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
515
|
+
if (subHoisted.length > 0) {
|
|
516
|
+
// we can convert it to a where at this level
|
|
517
|
+
injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted);
|
|
502
518
|
}
|
|
503
|
-
|
|
504
|
-
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// hoist non-nullable to-one filter to the parent level
|
|
522
|
+
hoisted = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
523
|
+
// recurse
|
|
524
|
+
const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
525
|
+
if (subHoisted.length > 0) {
|
|
526
|
+
hoisted = this.and(hoisted, ...subHoisted);
|
|
505
527
|
}
|
|
506
528
|
}
|
|
507
|
-
|
|
508
|
-
|
|
529
|
+
if (hoisted && !this.isTrue(hoisted)) {
|
|
530
|
+
hoistedConditions.push({ [field]: hoisted });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return hoistedConditions;
|
|
509
534
|
}
|
|
510
535
|
/**
|
|
511
536
|
* Given a model and a unique filter, checks the operation is allowed by policies and field validations.
|
|
@@ -515,14 +540,14 @@ class PolicyUtil {
|
|
|
515
540
|
return __awaiter(this, void 0, void 0, function* () {
|
|
516
541
|
let guard = this.getAuthGuard(db, model, operation, preValue);
|
|
517
542
|
if (this.isFalse(guard)) {
|
|
518
|
-
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check
|
|
543
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
519
544
|
}
|
|
520
545
|
if (operation === 'update' && args) {
|
|
521
546
|
// merge field-level policy guards
|
|
522
547
|
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
523
548
|
if (fieldUpdateGuard.rejectedByField) {
|
|
524
549
|
// rejected
|
|
525
|
-
throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"
|
|
550
|
+
throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
526
551
|
}
|
|
527
552
|
else if (fieldUpdateGuard.guard) {
|
|
528
553
|
// merge
|
|
@@ -551,7 +576,7 @@ class PolicyUtil {
|
|
|
551
576
|
}
|
|
552
577
|
const result = yield db[model].findFirst(query);
|
|
553
578
|
if (!result) {
|
|
554
|
-
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check
|
|
579
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
555
580
|
}
|
|
556
581
|
if (schema) {
|
|
557
582
|
// TODO: push down schema check to the database
|
|
@@ -561,7 +586,7 @@ class PolicyUtil {
|
|
|
561
586
|
if (this.logger.enabled('info')) {
|
|
562
587
|
this.logger.info(`entity ${model} failed validation for operation ${operation}: ${error}`);
|
|
563
588
|
}
|
|
564
|
-
throw this.deniedByPolicy(model, operation, `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
|
|
589
|
+
throw this.deniedByPolicy(model, operation, `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, parseResult.error);
|
|
565
590
|
}
|
|
566
591
|
}
|
|
567
592
|
});
|
|
@@ -587,7 +612,7 @@ class PolicyUtil {
|
|
|
587
612
|
tryReject(db, model, operation) {
|
|
588
613
|
const guard = this.getAuthGuard(db, model, operation);
|
|
589
614
|
if (this.isFalse(guard)) {
|
|
590
|
-
throw this.deniedByPolicy(model, operation);
|
|
615
|
+
throw this.deniedByPolicy(model, operation, undefined, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
591
616
|
}
|
|
592
617
|
}
|
|
593
618
|
/**
|
|
@@ -619,7 +644,7 @@ class PolicyUtil {
|
|
|
619
644
|
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
620
645
|
const readArgs = { select: selectInclude.select, include: selectInclude.include, where: uniqueFilter };
|
|
621
646
|
const error = this.deniedByPolicy(model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
|
|
622
|
-
const injectResult =
|
|
647
|
+
const injectResult = this.injectForRead(db, model, readArgs);
|
|
623
648
|
if (!injectResult) {
|
|
624
649
|
return { error, result: undefined };
|
|
625
650
|
}
|
|
@@ -712,8 +737,15 @@ class PolicyUtil {
|
|
|
712
737
|
}
|
|
713
738
|
//#endregion
|
|
714
739
|
//#region Errors
|
|
715
|
-
deniedByPolicy(model, operation, extra, reason) {
|
|
716
|
-
|
|
740
|
+
deniedByPolicy(model, operation, extra, reason, zodErrors) {
|
|
741
|
+
const args = { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINED_FAILED, meta: {} };
|
|
742
|
+
if (reason) {
|
|
743
|
+
args.meta.reason = reason;
|
|
744
|
+
}
|
|
745
|
+
if (zodErrors) {
|
|
746
|
+
args.meta.zodErrors = zodErrors;
|
|
747
|
+
}
|
|
748
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, args);
|
|
717
749
|
}
|
|
718
750
|
notFound(model) {
|
|
719
751
|
return (0, utils_1.prismaClientKnownRequestError)(this.db, `entity not found for model ${model}`, {
|
|
@@ -722,9 +754,7 @@ class PolicyUtil {
|
|
|
722
754
|
});
|
|
723
755
|
}
|
|
724
756
|
validationError(message) {
|
|
725
|
-
return (0, utils_1.prismaClientValidationError)(this.db, message
|
|
726
|
-
clientVersion: (0, version_1.getVersion)(),
|
|
727
|
-
});
|
|
757
|
+
return (0, utils_1.prismaClientValidationError)(this.db, message);
|
|
728
758
|
}
|
|
729
759
|
unknownError(message) {
|
|
730
760
|
return (0, utils_1.prismaClientUnknownRequestError)(this.db, message, {
|
|
@@ -805,12 +835,6 @@ class PolicyUtil {
|
|
|
805
835
|
if (typeof entityData !== 'object' || !entityData) {
|
|
806
836
|
return;
|
|
807
837
|
}
|
|
808
|
-
// strip auxiliary fields
|
|
809
|
-
for (const auxField of constants_1.AUXILIARY_FIELDS) {
|
|
810
|
-
if (auxField in entityData) {
|
|
811
|
-
delete entityData[auxField];
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
838
|
for (const [field, fieldData] of Object.entries(entityData)) {
|
|
815
839
|
if (fieldData === undefined) {
|
|
816
840
|
continue;
|
|
@@ -859,6 +883,13 @@ class PolicyUtil {
|
|
|
859
883
|
}
|
|
860
884
|
}
|
|
861
885
|
}
|
|
886
|
+
/**
|
|
887
|
+
* Gets information for all fields of a model.
|
|
888
|
+
*/
|
|
889
|
+
getModelFields(model) {
|
|
890
|
+
model = (0, lower_case_first_1.lowerCaseFirst)(model);
|
|
891
|
+
return this.modelMeta.fields[model];
|
|
892
|
+
}
|
|
862
893
|
/**
|
|
863
894
|
* Gets information for a specific model field.
|
|
864
895
|
*/
|
|
@@ -902,7 +933,6 @@ class PolicyUtil {
|
|
|
902
933
|
if (!where) {
|
|
903
934
|
throw new Error('invalid where clause');
|
|
904
935
|
}
|
|
905
|
-
extra = this.reduce(extra);
|
|
906
936
|
if (this.isTrue(extra)) {
|
|
907
937
|
return;
|
|
908
938
|
}
|