@zenstackhq/runtime 1.0.0-beta.9 → 1.0.0

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