@zenstackhq/runtime 1.0.0-alpha.99 → 1.0.0-beta.10

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 (66) hide show
  1. package/browser/index.d.mts +13 -0
  2. package/browser/index.d.ts +13 -0
  3. package/browser/index.js +69 -0
  4. package/browser/index.js.map +1 -0
  5. package/browser/index.mjs +31 -0
  6. package/browser/index.mjs.map +1 -0
  7. package/constants.d.ts +31 -0
  8. package/constants.js +34 -1
  9. package/constants.js.map +1 -1
  10. package/enhancements/index.d.ts +5 -0
  11. package/enhancements/index.js +5 -0
  12. package/enhancements/index.js.map +1 -1
  13. package/enhancements/model-meta.d.ts +4 -0
  14. package/enhancements/model-meta.js +26 -5
  15. package/enhancements/model-meta.js.map +1 -1
  16. package/enhancements/nested-write-vistor.d.ts +17 -16
  17. package/enhancements/nested-write-vistor.js +86 -59
  18. package/enhancements/nested-write-vistor.js.map +1 -1
  19. package/enhancements/omit.d.ts +10 -1
  20. package/enhancements/omit.js +4 -3
  21. package/enhancements/omit.js.map +1 -1
  22. package/enhancements/password.d.ts +10 -1
  23. package/enhancements/password.js +3 -2
  24. package/enhancements/password.js.map +1 -1
  25. package/enhancements/policy/handler.d.ts +6 -3
  26. package/enhancements/policy/handler.js +99 -39
  27. package/enhancements/policy/handler.js.map +1 -1
  28. package/enhancements/policy/index.d.ts +23 -2
  29. package/enhancements/policy/index.js +39 -6
  30. package/enhancements/policy/index.js.map +1 -1
  31. package/enhancements/policy/logger.d.ts +9 -1
  32. package/enhancements/policy/logger.js +14 -3
  33. package/enhancements/policy/logger.js.map +1 -1
  34. package/enhancements/policy/policy-utils.d.ts +13 -9
  35. package/enhancements/policy/policy-utils.js +250 -138
  36. package/enhancements/policy/policy-utils.js.map +1 -1
  37. package/enhancements/preset.d.ts +9 -7
  38. package/enhancements/preset.js +3 -6
  39. package/enhancements/preset.js.map +1 -1
  40. package/enhancements/proxy.js +62 -1
  41. package/enhancements/proxy.js.map +1 -1
  42. package/enhancements/types.d.ts +10 -1
  43. package/enhancements/utils.d.ts +12 -4
  44. package/enhancements/utils.js +97 -11
  45. package/enhancements/utils.js.map +1 -1
  46. package/enhancements/where-visitor.d.ts +33 -0
  47. package/enhancements/where-visitor.js +87 -0
  48. package/enhancements/where-visitor.js.map +1 -0
  49. package/index.d.ts +2 -2
  50. package/index.js +2 -2
  51. package/index.js.map +1 -1
  52. package/package.json +33 -12
  53. package/version.js +1 -0
  54. package/version.js.map +1 -1
  55. package/zod/index.d.ts +2 -0
  56. package/zod/index.js +4 -0
  57. package/zod/input.d.ts +1 -0
  58. package/zod/input.js +8 -0
  59. package/zod/models.d.ts +1 -0
  60. package/zod/models.js +8 -0
  61. package/serialization-utils.d.ts +0 -1
  62. package/serialization-utils.js +0 -22
  63. package/serialization-utils.js.map +0 -1
  64. package/zod.d.ts +0 -10
  65. package/zod.js +0 -17
  66. package/zod.js.map +0 -1
@@ -14,27 +14,30 @@ 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 runtime_1 = require("@prisma/client/runtime");
18
- const sdk_1 = require("@zenstackhq/sdk");
19
- const change_case_1 = require("change-case");
20
- const cuid_1 = __importDefault(require("cuid"));
17
+ const cuid2_1 = require("@paralleldrive/cuid2");
21
18
  const deepcopy_1 = __importDefault(require("deepcopy"));
19
+ const lower_case_first_1 = require("lower-case-first");
20
+ const pluralize_1 = __importDefault(require("pluralize"));
21
+ const upper_case_first_1 = require("upper-case-first");
22
22
  const zod_validation_error_1 = require("zod-validation-error");
23
+ const constants_1 = require("../../constants");
24
+ const error_1 = require("../../error");
23
25
  const version_1 = require("../../version");
24
26
  const model_meta_1 = require("../model-meta");
25
27
  const nested_write_vistor_1 = require("../nested-write-vistor");
26
28
  const utils_1 = require("../utils");
27
29
  const logger_1 = require("./logger");
28
- const pluralize_1 = __importDefault(require("pluralize"));
29
30
  /**
30
31
  * Access policy enforcement utilities
31
32
  */
32
33
  class PolicyUtil {
33
- constructor(db, modelMeta, policy, user) {
34
+ constructor(db, modelMeta, policy, zodSchemas, user, logPrismaQuery) {
34
35
  this.db = db;
35
36
  this.modelMeta = modelMeta;
36
37
  this.policy = policy;
38
+ this.zodSchemas = zodSchemas;
37
39
  this.user = user;
40
+ this.logPrismaQuery = logPrismaQuery;
38
41
  this.logger = new logger_1.Logger(db);
39
42
  }
40
43
  /**
@@ -43,7 +46,7 @@ class PolicyUtil {
43
46
  and(...conditions) {
44
47
  if (conditions.includes(false)) {
45
48
  // always false
46
- return { [sdk_1.GUARD_FIELD_NAME]: false };
49
+ return { [constants_1.GUARD_FIELD_NAME]: false };
47
50
  }
48
51
  const filtered = conditions.filter((c) => typeof c === 'object' && !!c && Object.keys(c).length > 0);
49
52
  if (filtered.length === 0) {
@@ -62,7 +65,7 @@ class PolicyUtil {
62
65
  or(...conditions) {
63
66
  if (conditions.includes(true)) {
64
67
  // always true
65
- return { [sdk_1.GUARD_FIELD_NAME]: true };
68
+ return { [constants_1.GUARD_FIELD_NAME]: true };
66
69
  }
67
70
  const filtered = conditions.filter((c) => typeof c === 'object' && !!c);
68
71
  if (filtered.length === 0) {
@@ -93,24 +96,26 @@ class PolicyUtil {
93
96
  * otherwise returns a guard object
94
97
  */
95
98
  getAuthGuard(model, operation, preValue) {
96
- return __awaiter(this, void 0, void 0, function* () {
97
- const guard = this.policy.guard[(0, change_case_1.camelCase)(model)];
98
- if (!guard) {
99
- throw this.unknownError(`unable to load policy guard for ${model}`);
100
- }
101
- const provider = guard[operation];
102
- if (typeof provider === 'boolean') {
103
- return provider;
104
- }
105
- if (!provider) {
106
- throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
107
- }
108
- return provider({ user: this.user, preValue });
109
- });
99
+ const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
100
+ if (!guard) {
101
+ throw this.unknownError(`unable to load policy guard for ${model}`);
102
+ }
103
+ const provider = guard[operation];
104
+ if (typeof provider === 'boolean') {
105
+ return provider;
106
+ }
107
+ if (!provider) {
108
+ throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
109
+ }
110
+ return provider({ user: this.user, preValue });
111
+ }
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;
110
115
  }
111
116
  getPreValueSelect(model) {
112
117
  return __awaiter(this, void 0, void 0, function* () {
113
- const guard = this.policy.guard[(0, change_case_1.camelCase)(model)];
118
+ const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
114
119
  if (!guard) {
115
120
  throw this.unknownError(`unable to load policy guard for ${model}`);
116
121
  }
@@ -118,9 +123,8 @@ class PolicyUtil {
118
123
  });
119
124
  }
120
125
  getModelSchema(model) {
121
- return __awaiter(this, void 0, void 0, function* () {
122
- return this.policy.schema[(0, change_case_1.camelCase)(model)];
123
- });
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`]);
124
128
  }
125
129
  /**
126
130
  * Injects model auth guard as where clause.
@@ -133,7 +137,7 @@ class PolicyUtil {
133
137
  // to-one: direct-conditions/is/isNot
134
138
  yield this.injectGuardForFields(model, args.where, operation);
135
139
  }
136
- const guard = yield this.getAuthGuard(model, operation);
140
+ const guard = this.getAuthGuard(model, operation);
137
141
  args.where = this.and(args.where, guard);
138
142
  });
139
143
  }
@@ -158,7 +162,7 @@ class PolicyUtil {
158
162
  }
159
163
  injectGuardForToManyField(fieldInfo, payload, operation) {
160
164
  return __awaiter(this, void 0, void 0, function* () {
161
- const guard = yield this.getAuthGuard(fieldInfo.type, operation);
165
+ const guard = this.getAuthGuard(fieldInfo.type, operation);
162
166
  if (payload.some) {
163
167
  yield this.injectGuardForFields(fieldInfo.type, payload.some, operation);
164
168
  // turn "some" into: { some: { AND: [guard, payload.some] } }
@@ -185,7 +189,7 @@ class PolicyUtil {
185
189
  }
186
190
  injectGuardForToOneField(fieldInfo, payload, operation) {
187
191
  return __awaiter(this, void 0, void 0, function* () {
188
- const guard = yield this.getAuthGuard(fieldInfo.type, operation);
192
+ const guard = this.getAuthGuard(fieldInfo.type, operation);
189
193
  if (payload.is || payload.isNot) {
190
194
  if (payload.is) {
191
195
  yield this.injectGuardForFields(fieldInfo.type, payload.is, operation);
@@ -228,9 +232,11 @@ class PolicyUtil {
228
232
  yield this.injectAuthGuard(args, model, 'read');
229
233
  // recursively inject read guard conditions into the query args
230
234
  yield this.injectNestedReadConditions(model, args);
231
- this.logger.info(`Reading with validation for ${model}: ${(0, utils_1.formatObject)(args)}`);
235
+ if (this.shouldLogQuery) {
236
+ this.logger.info(`[withPolicy] \`findMany\`:\n${(0, utils_1.formatObject)(args)}`);
237
+ }
232
238
  const result = yield this.db[model].findMany(args);
233
- yield Promise.all(result.map((item) => this.postProcessForRead(item, model, args, 'read')));
239
+ yield this.postProcessForRead(result, model, args, 'read');
234
240
  return result;
235
241
  });
236
242
  }
@@ -239,9 +245,9 @@ class PolicyUtil {
239
245
  var _a;
240
246
  return __awaiter(this, void 0, void 0, function* () {
241
247
  // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
242
- const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, change_case_1.camelCase)(model)];
248
+ const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)];
243
249
  let flattened = false;
244
- if (uniqueConstraints) {
250
+ if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) {
245
251
  for (const [field, value] of Object.entries(args)) {
246
252
  if (uniqueConstraints[field] && typeof value === 'object') {
247
253
  for (const [f, v] of Object.entries(value)) {
@@ -253,7 +259,8 @@ class PolicyUtil {
253
259
  }
254
260
  }
255
261
  if (flattened) {
256
- this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
262
+ // DEBUG
263
+ // this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
257
264
  }
258
265
  });
259
266
  }
@@ -264,6 +271,35 @@ class PolicyUtil {
264
271
  if (!injectTarget) {
265
272
  return;
266
273
  }
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
+ }
287
+ }
288
+ }
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] = {};
294
+ }
295
+ const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
296
+ if (!fieldInfo) {
297
+ continue;
298
+ }
299
+ // inject into the "where" clause inside select
300
+ yield this.injectAuthGuard(injectTarget._count.select[field], fieldInfo.type, 'read');
301
+ }
302
+ }
267
303
  const idFields = this.getIdFields(model);
268
304
  for (const field of (0, utils_1.getModelFields)(injectTarget)) {
269
305
  const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
@@ -301,41 +337,68 @@ class PolicyUtil {
301
337
  * (which can't be trimmed at query time) and removes fields that should be
302
338
  * omitted.
303
339
  */
304
- postProcessForRead(entityData, model, args, operation) {
340
+ postProcessForRead(data, model, args, operation) {
305
341
  var _a;
306
342
  return __awaiter(this, void 0, void 0, function* () {
307
- const ids = this.getEntityIds(model, entityData);
308
- if (Object.keys(ids).length === 0) {
309
- return;
310
- }
311
- // strip auxiliary fields
312
- for (const auxField of sdk_1.AUXILIARY_FIELDS) {
313
- if (auxField in entityData) {
314
- delete entityData[auxField];
315
- }
316
- }
317
- const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
318
- if (!injectTarget) {
319
- return;
320
- }
321
- // to-one relation data cannot be trimmed by injected guards, we have to
322
- // post-check them
323
- for (const field of (0, utils_1.getModelFields)(injectTarget)) {
324
- if (!(entityData === null || entityData === void 0 ? void 0 : entityData[field])) {
343
+ for (const entityData of (0, utils_1.enumerate)(data)) {
344
+ if (typeof entityData !== 'object' || !entityData) {
325
345
  continue;
326
346
  }
327
- const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
328
- if (!fieldInfo || !fieldInfo.isDataModel || fieldInfo.isArray) {
329
- continue;
347
+ // strip auxiliary fields
348
+ for (const auxField of constants_1.AUXILIARY_FIELDS) {
349
+ if (auxField in entityData) {
350
+ delete entityData[auxField];
351
+ }
330
352
  }
331
- const ids = this.getEntityIds(fieldInfo.type, entityData[field]);
332
- if (Object.keys(ids).length === 0) {
353
+ const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
354
+ if (!injectTarget) {
333
355
  continue;
334
356
  }
335
- this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${(0, utils_1.formatObject)(ids)}`);
336
- yield this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
337
- // recurse
338
- yield this.postProcessForRead(entityData[field], fieldInfo.type, injectTarget[field], operation);
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);
400
+ }
401
+ }
339
402
  }
340
403
  });
341
404
  }
@@ -371,17 +434,17 @@ class PolicyUtil {
371
434
  }
372
435
  // use a transaction to conduct write, so in case any create or nested create
373
436
  // fails access policies, we can roll back the entire operation
374
- const transactionId = (0, cuid_1.default)();
437
+ const transactionId = (0, cuid2_1.createId)();
375
438
  // args processor for create
376
439
  const processCreate = (model, args) => __awaiter(this, void 0, void 0, function* () {
377
- const guard = yield this.getAuthGuard(model, 'create');
378
- const schema = yield this.getModelSchema(model);
440
+ const guard = this.getAuthGuard(model, 'create');
441
+ const schema = this.getModelSchema(model);
379
442
  if (guard === false) {
380
443
  throw this.deniedByPolicy(model, 'create');
381
444
  }
382
445
  else if (guard !== true || schema) {
383
446
  // mark the create with a transaction tag so we can check them later
384
- args[sdk_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
447
+ args[constants_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
385
448
  createdModels.add(model);
386
449
  }
387
450
  });
@@ -390,10 +453,16 @@ class PolicyUtil {
390
453
  let result, currQuery;
391
454
  let currField;
392
455
  for (let i = context.nestingPath.length - 1; i >= 0; i--) {
393
- const { field, where, unique } = context.nestingPath[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
+ }
394
463
  if (!result) {
395
464
  // first segment (bottom), just use its where clause
396
- result = currQuery = Object.assign({}, where);
465
+ result = currQuery = Object.assign({}, visitWhere);
397
466
  currField = field;
398
467
  }
399
468
  else {
@@ -403,7 +472,14 @@ class PolicyUtil {
403
472
  if (!currField.backLink) {
404
473
  throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
405
474
  }
406
- currQuery[currField.backLink] = Object.assign({}, where);
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
+ }
407
483
  currQuery = currQuery[currField.backLink];
408
484
  currField = field;
409
485
  }
@@ -416,7 +492,7 @@ class PolicyUtil {
416
492
  });
417
493
  // args processor for update/upsert
418
494
  const processUpdate = (model, where, context) => __awaiter(this, void 0, void 0, function* () {
419
- const preGuard = yield this.getAuthGuard(model, 'update');
495
+ const preGuard = this.getAuthGuard(model, 'update');
420
496
  if (preGuard === false) {
421
497
  throw this.deniedByPolicy(model, 'update');
422
498
  }
@@ -467,7 +543,7 @@ class PolicyUtil {
467
543
  });
468
544
  // args processor for updateMany
469
545
  const processUpdateMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
470
- const guard = yield this.getAuthGuard(model, 'update');
546
+ const guard = this.getAuthGuard(model, 'update');
471
547
  if (guard === false) {
472
548
  throw this.deniedByPolicy(model, 'update');
473
549
  }
@@ -480,8 +556,8 @@ class PolicyUtil {
480
556
  // for models with post-update rules, we need to read and store
481
557
  // entity values before the update for post-update check
482
558
  const preparePostUpdateCheck = (model, context) => __awaiter(this, void 0, void 0, function* () {
483
- const postGuard = yield this.getAuthGuard(model, 'postUpdate');
484
- const schema = yield this.getModelSchema(model);
559
+ const postGuard = this.getAuthGuard(model, 'postUpdate');
560
+ const schema = this.getModelSchema(model);
485
561
  // post-update check is needed if there's post-update rule or validation schema
486
562
  if (postGuard !== true || schema) {
487
563
  // fetch preValue selection (analyzed from the post-update rules)
@@ -498,7 +574,9 @@ class PolicyUtil {
498
574
  select[idField.name] = true;
499
575
  }
500
576
  const query = { where: filter, select };
501
- this.logger.info(`fetching pre-update entities for ${model}: ${(0, utils_1.formatObject)(query)})}`);
577
+ if (this.shouldLogQuery) {
578
+ this.logger.info(`[withPolicy] \`findMany\` for fetching pre-update entities:\n${(0, utils_1.formatObject)(args)}`);
579
+ }
502
580
  const entities = yield this.db[model].findMany(query);
503
581
  entities.forEach((entity) => {
504
582
  addUpdatedEntity(model, this.getEntityIds(model, entity), entity);
@@ -507,7 +585,7 @@ class PolicyUtil {
507
585
  });
508
586
  // args processor for delete
509
587
  const processDelete = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
510
- const guard = yield this.getAuthGuard(model, 'delete');
588
+ const guard = this.getAuthGuard(model, 'delete');
511
589
  if (guard === false) {
512
590
  throw this.deniedByPolicy(model, 'delete');
513
591
  }
@@ -524,6 +602,8 @@ class PolicyUtil {
524
602
  });
525
603
  // process relation updates: connect, connectOrCreate, and disconnect
526
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);
527
607
  var _a;
528
608
  if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
529
609
  // fetch the backlink field of the model being connected
@@ -538,66 +618,56 @@ class PolicyUtil {
538
618
  // use a visitor to process args before conducting the write action
539
619
  const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
540
620
  create: (model, args) => __awaiter(this, void 0, void 0, function* () {
541
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
542
- yield processCreate(model, oneArgs);
543
- }
621
+ yield processCreate(model, args);
544
622
  }),
545
623
  connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
546
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
547
- if (oneArgs.create) {
548
- yield processCreate(model, oneArgs.create);
549
- }
550
- if (oneArgs.where) {
551
- yield processRelationUpdate(model, oneArgs.where, context);
552
- }
624
+ if (args.create) {
625
+ yield processCreate(model, args.create);
626
+ }
627
+ if (args.where) {
628
+ yield processRelationUpdate(model, args.where, context);
553
629
  }
554
630
  }),
555
631
  connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
556
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
557
- yield processRelationUpdate(model, oneArgs, context);
558
- }
632
+ yield processRelationUpdate(model, args, context);
559
633
  }),
560
634
  disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
561
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
562
- yield processRelationUpdate(model, oneArgs, context);
563
- }
635
+ yield processRelationUpdate(model, args, context);
564
636
  }),
565
637
  update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
566
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
567
- yield processUpdate(model, oneArgs.where, context);
568
- }
638
+ yield processUpdate(model, args.where, context);
569
639
  }),
570
640
  updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
571
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
572
- yield processUpdateMany(model, oneArgs, context);
573
- }
641
+ yield processUpdateMany(model, args, context);
574
642
  }),
575
643
  upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
576
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
577
- if (oneArgs.create) {
578
- yield processCreate(model, oneArgs.create);
579
- }
580
- if (oneArgs.update) {
581
- yield processUpdate(model, oneArgs.where, context);
582
- }
644
+ if (args.create) {
645
+ yield processCreate(model, args.create);
646
+ }
647
+ if (args.update) {
648
+ yield processUpdate(model, args.where, context);
583
649
  }
584
650
  }),
585
651
  delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
586
- for (const oneArgs of (0, utils_1.enumerate)(args)) {
587
- yield processDelete(model, oneArgs, context);
588
- }
652
+ yield processDelete(model, args, context);
589
653
  }),
590
- deleteMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
591
- const guard = yield this.getAuthGuard(model, 'delete');
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');
592
657
  if (guard === false) {
593
658
  throw this.deniedByPolicy(model, 'delete');
594
659
  }
595
660
  else if (guard !== true) {
596
- if (Array.isArray(args)) {
597
- context.parent.deleteMany = args.map((oneArgs) => this.and(oneArgs, guard));
661
+ if (args.where) {
662
+ args.where = this.and(args.where, guard);
598
663
  }
599
664
  else {
600
- context.parent.deleteMany = this.and(args, guard);
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);
601
671
  }
602
672
  }
603
673
  }),
@@ -613,7 +683,7 @@ class PolicyUtil {
613
683
  const result = yield writeAction(tx[model], args);
614
684
  if (createdModels.size > 0) {
615
685
  // do post-check on created entities
616
- yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [sdk_1.TRANSACTION_FIELD_NAME]: `${transactionId}:create` }, 'create', tx)));
686
+ yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [constants_1.TRANSACTION_FIELD_NAME]: `${transactionId}:create` }, 'create', tx)));
617
687
  }
618
688
  if (updatedModels.size > 0) {
619
689
  // do post-check on updated entities
@@ -626,6 +696,11 @@ class PolicyUtil {
626
696
  }
627
697
  });
628
698
  }
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];
703
+ }
629
704
  transaction(db, action) {
630
705
  if (db.__zenstack_tx) {
631
706
  // already in transaction, don't nest
@@ -636,16 +711,16 @@ class PolicyUtil {
636
711
  }
637
712
  }
638
713
  deniedByPolicy(model, operation, extra, reason) {
639
- return new runtime_1.PrismaClientKnownRequestError(`denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code: 'P2004', meta: { 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 } });
640
715
  }
641
716
  notFound(model) {
642
- return new runtime_1.PrismaClientKnownRequestError(`entity not found for model ${model}`, {
717
+ return (0, utils_1.prismaClientKnownRequestError)(this.db, `entity not found for model ${model}`, {
643
718
  clientVersion: (0, version_1.getVersion)(),
644
719
  code: 'P2025',
645
720
  });
646
721
  }
647
722
  unknownError(message) {
648
- return new runtime_1.PrismaClientUnknownRequestError(message, {
723
+ return (0, utils_1.prismaClientUnknownRequestError)(this.db, message, {
649
724
  clientVersion: (0, version_1.getVersion)(),
650
725
  });
651
726
  }
@@ -655,37 +730,73 @@ class PolicyUtil {
655
730
  */
656
731
  checkPolicyForFilter(model, filter, operation, db) {
657
732
  return __awaiter(this, void 0, void 0, function* () {
658
- this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`);
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
+ // }
659
742
  const queryFilter = (0, deepcopy_1.default)(filter);
660
743
  // query args will be used with findMany, so we need to
661
744
  // translate unique constraint filters into a flat filter
662
745
  // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
663
746
  yield this.flattenGeneratedUniqueField(model, queryFilter);
664
- const count = (yield db[model].count({ where: queryFilter }));
665
- const guard = yield this.getAuthGuard(model, operation);
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
756
+ return;
757
+ }
758
+ if (guard === false) {
759
+ // unconditionally denied
760
+ throw this.deniedByPolicy(model, operation, `${count} ${(0, pluralize_1.default)('entity', count)} failed policy check`);
761
+ }
666
762
  // build a query condition with policy injected
667
763
  const guardedQuery = { where: this.and(queryFilter, guard) };
668
- const schema = (operation === 'create' || operation === 'update') && (yield this.getModelSchema(model));
669
764
  if (schema) {
670
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
+ // }
671
771
  const entities = yield db[model].findMany(guardedQuery);
672
772
  if (entities.length < count) {
673
- this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
773
+ if (this.logger.enabled('info')) {
774
+ this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
775
+ }
674
776
  throw this.deniedByPolicy(model, operation, `${count - entities.length} ${(0, pluralize_1.default)('entity', count - entities.length)} failed policy check`);
675
777
  }
676
778
  // TODO: push down schema check to the database
677
779
  const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success);
678
780
  if (schemaCheckErrors.length > 0) {
679
781
  const error = schemaCheckErrors.map((r) => !r.success && (0, zod_validation_error_1.fromZodError)(r.error).message).join(', ');
680
- this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
681
- throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`);
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);
682
786
  }
683
787
  }
684
788
  else {
685
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
+ // }
686
795
  const guardedCount = (yield db[model].count(guardedQuery));
687
796
  if (guardedCount < count) {
688
- this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
797
+ if (this.logger.enabled('info')) {
798
+ this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
799
+ }
689
800
  throw this.deniedByPolicy(model, operation, `${count - guardedCount} ${(0, pluralize_1.default)('entity', count - guardedCount)} failed policy check`);
690
801
  }
691
802
  }
@@ -693,24 +804,30 @@ class PolicyUtil {
693
804
  }
694
805
  checkPostUpdate(model, ids, db, preValue) {
695
806
  return __awaiter(this, void 0, void 0, function* () {
696
- this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${(0, utils_1.formatObject)(preValue)}`);
697
- const guard = yield this.getAuthGuard(model, 'postUpdate', preValue);
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);
698
811
  // build a query condition with policy injected
699
812
  const guardedQuery = { where: this.and(ids, guard) };
700
813
  // query with policy injected
701
814
  const entity = yield db[model].findFirst(guardedQuery);
702
815
  // see if we get fewer items with policy, if so, reject with an throw
703
816
  if (!entity) {
704
- this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
817
+ if (this.logger.enabled('info')) {
818
+ this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
819
+ }
705
820
  throw this.deniedByPolicy(model, 'postUpdate');
706
821
  }
707
822
  // TODO: push down schema check to the database
708
- const schema = yield this.getModelSchema(model);
823
+ const schema = this.getModelSchema(model);
709
824
  if (schema) {
710
825
  const schemaCheckResult = schema.safeParse(entity);
711
826
  if (!schemaCheckResult.success) {
712
827
  const error = (0, zod_validation_error_1.fromZodError)(schemaCheckResult.error).message;
713
- this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
828
+ if (this.logger.enabled('info')) {
829
+ this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
830
+ }
714
831
  throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`);
715
832
  }
716
833
  }
@@ -729,15 +846,7 @@ class PolicyUtil {
729
846
  * Gets "id" field for a given model.
730
847
  */
731
848
  getIdFields(model) {
732
- const fields = this.modelMeta.fields[(0, change_case_1.camelCase)(model)];
733
- if (!fields) {
734
- throw this.unknownError(`Unable to load fields for ${model}`);
735
- }
736
- const result = Object.values(fields).filter((f) => f.isId);
737
- if (result.length === 0) {
738
- throw this.unknownError(`model ${model} does not have an id field`);
739
- }
740
- return result;
849
+ return (0, utils_1.getIdFields)(this.modelMeta, model, true);
741
850
  }
742
851
  /**
743
852
  * Gets id field value from an entity.
@@ -750,6 +859,9 @@ class PolicyUtil {
750
859
  }
751
860
  return result;
752
861
  }
862
+ get shouldLogQuery() {
863
+ return this.logPrismaQuery && this.logger.enabled('info');
864
+ }
753
865
  }
754
866
  exports.PolicyUtil = PolicyUtil;
755
867
  //# sourceMappingURL=policy-utils.js.map