@zenstackhq/runtime 1.0.0-beta.13 → 1.0.0-beta.15

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