@zenstackhq/runtime 2.4.0 → 2.5.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 (139) hide show
  1. package/browser/index.js +3 -2
  2. package/browser/index.js.map +1 -1
  3. package/browser/index.mjs +3 -2
  4. package/browser/index.mjs.map +1 -1
  5. package/constants.d.ts +1 -1
  6. package/constants.js +1 -1
  7. package/constants.js.map +1 -1
  8. package/edge.d.ts +1 -7
  9. package/edge.js +1 -7
  10. package/edge.js.map +1 -1
  11. package/enhance-edge.d.ts +1 -0
  12. package/enhance-edge.js +10 -0
  13. package/enhancements/edge/create-enhancement.d.ts +37 -0
  14. package/enhancements/{create-enhancement.js → edge/create-enhancement.js} +2 -2
  15. package/enhancements/edge/create-enhancement.js.map +1 -0
  16. package/enhancements/{default-auth.d.ts → edge/default-auth.d.ts} +2 -1
  17. package/enhancements/{default-auth.js → edge/default-auth.js} +1 -1
  18. package/enhancements/edge/default-auth.js.map +1 -0
  19. package/enhancements/{delegate.d.ts → edge/delegate.d.ts} +1 -1
  20. package/enhancements/{delegate.js → edge/delegate.js} +16 -3
  21. package/enhancements/edge/delegate.js.map +1 -0
  22. package/enhancements/{index.d.ts → edge/index.d.ts} +1 -1
  23. package/enhancements/{index.js → edge/index.js} +1 -1
  24. package/enhancements/edge/index.js.map +1 -0
  25. package/enhancements/edge/logger.js.map +1 -0
  26. package/enhancements/{omit.js → edge/omit.js} +33 -3
  27. package/enhancements/edge/omit.js.map +1 -0
  28. package/enhancements/{password.js → edge/password.js} +2 -2
  29. package/enhancements/edge/password.js.map +1 -0
  30. package/enhancements/edge/policy/check-utils.d.ts +5 -0
  31. package/enhancements/edge/policy/check-utils.js +20 -0
  32. package/enhancements/edge/policy/check-utils.js.map +1 -0
  33. package/enhancements/{policy → edge/policy}/handler.d.ts +24 -9
  34. package/enhancements/{policy → edge/policy}/handler.js +67 -91
  35. package/enhancements/edge/policy/handler.js.map +1 -0
  36. package/enhancements/{policy → edge/policy}/index.d.ts +2 -1
  37. package/enhancements/{policy → edge/policy}/index.js +2 -2
  38. package/enhancements/edge/policy/index.js.map +1 -0
  39. package/enhancements/{policy → edge/policy}/policy-utils.d.ts +5 -5
  40. package/enhancements/{policy → edge/policy}/policy-utils.js +27 -21
  41. package/enhancements/edge/policy/policy-utils.js.map +1 -0
  42. package/enhancements/{promise.d.ts → edge/promise.d.ts} +1 -1
  43. package/enhancements/{promise.js → edge/promise.js} +1 -1
  44. package/enhancements/edge/promise.js.map +1 -0
  45. package/enhancements/{proxy.d.ts → edge/proxy.d.ts} +26 -9
  46. package/enhancements/{proxy.js → edge/proxy.js} +31 -7
  47. package/enhancements/edge/proxy.js.map +1 -0
  48. package/enhancements/{query-utils.d.ts → edge/query-utils.d.ts} +2 -2
  49. package/enhancements/{query-utils.js → edge/query-utils.js} +3 -4
  50. package/enhancements/edge/query-utils.js.map +1 -0
  51. package/enhancements/{types.d.ts → edge/types.d.ts} +5 -12
  52. package/enhancements/{types.js.map → edge/types.js.map} +1 -1
  53. package/enhancements/{utils.d.ts → edge/utils.d.ts} +2 -2
  54. package/enhancements/{utils.js → edge/utils.js} +2 -2
  55. package/enhancements/edge/utils.js.map +1 -0
  56. package/enhancements/{where-visitor.d.ts → edge/where-visitor.d.ts} +1 -1
  57. package/enhancements/{where-visitor.js → edge/where-visitor.js} +1 -1
  58. package/enhancements/edge/where-visitor.js.map +1 -0
  59. package/enhancements/node/create-enhancement.d.ts +37 -0
  60. package/enhancements/node/create-enhancement.js +79 -0
  61. package/enhancements/node/create-enhancement.js.map +1 -0
  62. package/enhancements/node/default-auth.d.ts +8 -0
  63. package/enhancements/node/default-auth.js +129 -0
  64. package/enhancements/node/default-auth.js.map +1 -0
  65. package/enhancements/node/delegate.d.ts +69 -0
  66. package/enhancements/node/delegate.js +1006 -0
  67. package/enhancements/node/delegate.js.map +1 -0
  68. package/enhancements/node/index.d.ts +4 -0
  69. package/enhancements/node/index.js +21 -0
  70. package/enhancements/node/index.js.map +1 -0
  71. package/enhancements/node/logger.d.ts +29 -0
  72. package/enhancements/node/logger.js +65 -0
  73. package/enhancements/node/logger.js.map +1 -0
  74. package/enhancements/node/omit.d.ts +7 -0
  75. package/enhancements/node/omit.js +93 -0
  76. package/enhancements/node/omit.js.map +1 -0
  77. package/enhancements/node/password.d.ts +7 -0
  78. package/enhancements/node/password.js +65 -0
  79. package/enhancements/node/password.js.map +1 -0
  80. package/enhancements/node/policy/check-utils.d.ts +5 -0
  81. package/enhancements/node/policy/check-utils.js +87 -0
  82. package/enhancements/node/policy/check-utils.js.map +1 -0
  83. package/enhancements/node/policy/constraint-solver.js.map +1 -0
  84. package/enhancements/node/policy/handler.d.ts +94 -0
  85. package/enhancements/node/policy/handler.js +1357 -0
  86. package/enhancements/node/policy/handler.js.map +1 -0
  87. package/enhancements/node/policy/index.d.ts +13 -0
  88. package/enhancements/node/policy/index.js +42 -0
  89. package/enhancements/node/policy/index.js.map +1 -0
  90. package/enhancements/node/policy/policy-utils.d.ts +184 -0
  91. package/enhancements/node/policy/policy-utils.js +1296 -0
  92. package/enhancements/node/policy/policy-utils.js.map +1 -0
  93. package/enhancements/node/promise.d.ts +15 -0
  94. package/enhancements/node/promise.js +99 -0
  95. package/enhancements/node/promise.js.map +1 -0
  96. package/enhancements/node/proxy.d.ts +118 -0
  97. package/enhancements/node/proxy.js +267 -0
  98. package/enhancements/node/proxy.js.map +1 -0
  99. package/enhancements/node/query-utils.d.ts +38 -0
  100. package/enhancements/node/query-utils.js +185 -0
  101. package/enhancements/node/query-utils.js.map +1 -0
  102. package/enhancements/node/types.d.ts +224 -0
  103. package/enhancements/node/types.js +3 -0
  104. package/enhancements/node/types.js.map +1 -0
  105. package/enhancements/node/utils.d.ts +11 -0
  106. package/enhancements/node/utils.js +49 -0
  107. package/enhancements/node/utils.js.map +1 -0
  108. package/enhancements/node/where-visitor.d.ts +32 -0
  109. package/enhancements/node/where-visitor.js +86 -0
  110. package/enhancements/node/where-visitor.js.map +1 -0
  111. package/index.d.ts +2 -2
  112. package/index.js +2 -2
  113. package/index.js.map +1 -1
  114. package/package.json +14 -6
  115. package/types.d.ts +62 -0
  116. package/enhancements/create-enhancement.d.ts +0 -78
  117. package/enhancements/create-enhancement.js.map +0 -1
  118. package/enhancements/default-auth.js.map +0 -1
  119. package/enhancements/delegate.js.map +0 -1
  120. package/enhancements/index.js.map +0 -1
  121. package/enhancements/logger.js.map +0 -1
  122. package/enhancements/omit.js.map +0 -1
  123. package/enhancements/password.js.map +0 -1
  124. package/enhancements/policy/constraint-solver.js.map +0 -1
  125. package/enhancements/policy/handler.js.map +0 -1
  126. package/enhancements/policy/index.js.map +0 -1
  127. package/enhancements/policy/policy-utils.js.map +0 -1
  128. package/enhancements/promise.js.map +0 -1
  129. package/enhancements/proxy.js.map +0 -1
  130. package/enhancements/query-utils.js.map +0 -1
  131. package/enhancements/utils.js.map +0 -1
  132. package/enhancements/where-visitor.js.map +0 -1
  133. /package/enhancements/{logger.d.ts → edge/logger.d.ts} +0 -0
  134. /package/enhancements/{logger.js → edge/logger.js} +0 -0
  135. /package/enhancements/{omit.d.ts → edge/omit.d.ts} +0 -0
  136. /package/enhancements/{password.d.ts → edge/password.d.ts} +0 -0
  137. /package/enhancements/{types.js → edge/types.js} +0 -0
  138. /package/enhancements/{policy → node/policy}/constraint-solver.d.ts +0 -0
  139. /package/enhancements/{policy → node/policy}/constraint-solver.js +0 -0
@@ -0,0 +1,1296 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PolicyUtil = void 0;
17
+ const deepmerge_1 = __importDefault(require("deepmerge"));
18
+ const is_plain_object_1 = require("is-plain-object");
19
+ const lower_case_first_1 = require("lower-case-first");
20
+ const upper_case_first_1 = require("upper-case-first");
21
+ const zod_1 = require("zod");
22
+ const zod_validation_error_1 = require("zod-validation-error");
23
+ const constants_1 = require("../../../constants");
24
+ const cross_1 = require("../../../cross");
25
+ const version_1 = require("../../../version");
26
+ const logger_1 = require("../logger");
27
+ const query_utils_1 = require("../query-utils");
28
+ const utils_1 = require("../utils");
29
+ /**
30
+ * Access policy enforcement utilities
31
+ */
32
+ class PolicyUtil extends query_utils_1.QueryUtils {
33
+ constructor(db, options, context, shouldLogQuery = false) {
34
+ super(db, options);
35
+ this.db = db;
36
+ this.shouldLogQuery = shouldLogQuery;
37
+ //#endregion
38
+ //#region Auth guard
39
+ this.FULL_OPEN_MODEL_POLICY = {
40
+ modelLevel: {
41
+ read: { guard: true },
42
+ create: { guard: true, inputChecker: true },
43
+ update: { guard: true },
44
+ delete: { guard: true },
45
+ postUpdate: { guard: true },
46
+ },
47
+ };
48
+ this.logger = new logger_1.Logger(db);
49
+ this.user = context === null || context === void 0 ? void 0 : context.user;
50
+ ({
51
+ modelMeta: this.modelMeta,
52
+ policy: this.policy,
53
+ zodSchemas: this.zodSchemas,
54
+ prismaModule: this.prismaModule,
55
+ } = options);
56
+ }
57
+ //#region Logical operators
58
+ /**
59
+ * Creates a conjunction of a list of query conditions.
60
+ */
61
+ and(...conditions) {
62
+ const filtered = conditions.filter((c) => c !== undefined);
63
+ if (filtered.length === 0) {
64
+ return this.makeTrue();
65
+ }
66
+ else if (filtered.length === 1) {
67
+ return this.reduce(filtered[0]);
68
+ }
69
+ else {
70
+ return this.reduce({ AND: filtered });
71
+ }
72
+ }
73
+ /**
74
+ * Creates a disjunction of a list of query conditions.
75
+ */
76
+ or(...conditions) {
77
+ const filtered = conditions.filter((c) => c !== undefined);
78
+ if (filtered.length === 0) {
79
+ return this.makeFalse();
80
+ }
81
+ else if (filtered.length === 1) {
82
+ return this.reduce(filtered[0]);
83
+ }
84
+ else {
85
+ return this.reduce({ OR: filtered });
86
+ }
87
+ }
88
+ /**
89
+ * Creates a negation of a query condition.
90
+ */
91
+ not(condition) {
92
+ if (condition === undefined) {
93
+ return this.makeTrue();
94
+ }
95
+ else if (typeof condition === 'boolean') {
96
+ return this.reduce(!condition);
97
+ }
98
+ else {
99
+ return this.reduce({ NOT: condition });
100
+ }
101
+ }
102
+ // Static True/False conditions
103
+ // https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
104
+ singleKey(obj, key) {
105
+ if (!obj) {
106
+ return false;
107
+ }
108
+ else {
109
+ return Object.keys(obj).length === 1 && Object.keys(obj)[0] === key;
110
+ }
111
+ }
112
+ isTrue(condition) {
113
+ if (condition === null || condition === undefined || !(0, is_plain_object_1.isPlainObject)(condition)) {
114
+ return false;
115
+ }
116
+ // {} is true
117
+ if (Object.keys(condition).length === 0) {
118
+ return true;
119
+ }
120
+ // { OR: TRUE } is true
121
+ if (this.singleKey(condition, 'OR') && typeof condition.OR === 'object' && this.isTrue(condition.OR)) {
122
+ return true;
123
+ }
124
+ // { NOT: FALSE } is true
125
+ if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isFalse(condition.NOT)) {
126
+ return true;
127
+ }
128
+ // { AND: [] } is true
129
+ if (this.singleKey(condition, 'AND') && Array.isArray(condition.AND) && condition.AND.length === 0) {
130
+ return true;
131
+ }
132
+ return false;
133
+ }
134
+ isFalse(condition) {
135
+ if (condition === null || condition === undefined || !(0, is_plain_object_1.isPlainObject)(condition)) {
136
+ return false;
137
+ }
138
+ // { AND: FALSE } is false
139
+ if (this.singleKey(condition, 'AND') && typeof condition.AND === 'object' && this.isFalse(condition.AND)) {
140
+ return true;
141
+ }
142
+ // { NOT: TRUE } is false
143
+ if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isTrue(condition.NOT)) {
144
+ return true;
145
+ }
146
+ // { OR: [] } is false
147
+ if (this.singleKey(condition, 'OR') && Array.isArray(condition.OR) && condition.OR.length === 0) {
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+ makeTrue() {
153
+ return { AND: [] };
154
+ }
155
+ makeFalse() {
156
+ return { OR: [] };
157
+ }
158
+ reduce(condition) {
159
+ if (condition === true || condition === undefined) {
160
+ return this.makeTrue();
161
+ }
162
+ if (condition === false) {
163
+ return this.makeFalse();
164
+ }
165
+ if (condition === null) {
166
+ return condition;
167
+ }
168
+ const result = {};
169
+ for (const [key, value] of Object.entries(condition)) {
170
+ if (value === null || value === undefined) {
171
+ result[key] = value;
172
+ continue;
173
+ }
174
+ switch (key) {
175
+ case 'AND': {
176
+ const children = (0, cross_1.enumerate)(value)
177
+ .map((c) => this.reduce(c))
178
+ .filter((c) => c !== undefined && !this.isTrue(c));
179
+ if (children.length === 0) {
180
+ // { ..., AND: [] }
181
+ result[key] = [];
182
+ }
183
+ else if (children.some((c) => this.isFalse(c))) {
184
+ // { ..., AND: { OR: [] } }
185
+ result[key] = this.makeFalse();
186
+ }
187
+ else {
188
+ result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
189
+ }
190
+ break;
191
+ }
192
+ case 'OR': {
193
+ const children = (0, cross_1.enumerate)(value)
194
+ .map((c) => this.reduce(c))
195
+ .filter((c) => c !== undefined && !this.isFalse(c));
196
+ if (children.length === 0) {
197
+ // { ..., OR: [] }
198
+ result[key] = [];
199
+ }
200
+ else if (children.some((c) => this.isTrue(c))) {
201
+ // { ..., OR: { AND: [] } }
202
+ result[key] = this.makeTrue();
203
+ }
204
+ else {
205
+ result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
206
+ }
207
+ break;
208
+ }
209
+ case 'NOT': {
210
+ const children = (0, cross_1.enumerate)(value).map((c) => this.reduce(c));
211
+ result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
212
+ break;
213
+ }
214
+ default: {
215
+ if (!(0, is_plain_object_1.isPlainObject)(value)) {
216
+ // don't visit into non-plain object values - could be Date, array, etc.
217
+ result[key] = value;
218
+ }
219
+ else {
220
+ result[key] = this.reduce(value);
221
+ }
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ // finally normalize constant true/false conditions
227
+ if (this.isTrue(result)) {
228
+ return this.makeTrue();
229
+ }
230
+ else if (this.isFalse(result)) {
231
+ return this.makeFalse();
232
+ }
233
+ else {
234
+ return result;
235
+ }
236
+ }
237
+ getModelPolicyDef(model) {
238
+ if (this.options.kinds && !this.options.kinds.includes('policy')) {
239
+ // policy enhancement not enabled, return an fully open guard
240
+ return this.FULL_OPEN_MODEL_POLICY;
241
+ }
242
+ const def = this.policy.policy[(0, lower_case_first_1.lowerCaseFirst)(model)];
243
+ if (!def) {
244
+ throw this.unknownError(`unable to load policy guard for ${model}`);
245
+ }
246
+ return def;
247
+ }
248
+ getModelGuardForOperation(model, operation) {
249
+ var _a;
250
+ const def = this.getModelPolicyDef(model);
251
+ return (_a = def.modelLevel[operation].guard) !== null && _a !== void 0 ? _a : true;
252
+ }
253
+ /**
254
+ * Gets pregenerated authorization guard object for a given model and operation.
255
+ *
256
+ * @returns true if operation is unconditionally allowed, false if unconditionally denied,
257
+ * otherwise returns a guard object
258
+ */
259
+ getAuthGuard(db, model, operation, preValue) {
260
+ const guard = this.getModelGuardForOperation(model, operation);
261
+ // constant guard
262
+ if (typeof guard === 'boolean') {
263
+ return this.reduce(guard);
264
+ }
265
+ // invoke guard function
266
+ const r = guard({ user: this.user, preValue }, db);
267
+ return this.reduce(r);
268
+ }
269
+ /**
270
+ * Get field-level read auth guard
271
+ */
272
+ getFieldReadAuthGuard(db, model, field) {
273
+ var _a, _b, _c;
274
+ const def = this.getModelPolicyDef(model);
275
+ const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
276
+ if (guard === undefined) {
277
+ // field access is allowed by default
278
+ return this.makeTrue();
279
+ }
280
+ if (typeof guard === 'boolean') {
281
+ return this.reduce(guard);
282
+ }
283
+ const r = guard({ user: this.user }, db);
284
+ return this.reduce(r);
285
+ }
286
+ /**
287
+ * Get field-level read auth guard that overrides the model-level
288
+ */
289
+ getFieldOverrideReadAuthGuard(db, model, field) {
290
+ var _a, _b, _c;
291
+ const def = this.getModelPolicyDef(model);
292
+ const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
293
+ if (guard === undefined) {
294
+ // field access is denied by default in override mode
295
+ return this.makeFalse();
296
+ }
297
+ if (typeof guard === 'boolean') {
298
+ return this.reduce(guard);
299
+ }
300
+ const r = guard({ user: this.user }, db);
301
+ return this.reduce(r);
302
+ }
303
+ /**
304
+ * Get field-level update auth guard
305
+ */
306
+ getFieldUpdateAuthGuard(db, model, field) {
307
+ var _a, _b, _c;
308
+ const def = this.getModelPolicyDef(model);
309
+ const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
310
+ if (guard === undefined) {
311
+ // field access is allowed by default
312
+ return this.makeTrue();
313
+ }
314
+ if (typeof guard === 'boolean') {
315
+ return this.reduce(guard);
316
+ }
317
+ const r = guard({ user: this.user }, db);
318
+ return this.reduce(r);
319
+ }
320
+ /**
321
+ * Get field-level update auth guard that overrides the model-level
322
+ */
323
+ getFieldOverrideUpdateAuthGuard(db, model, field) {
324
+ var _a, _b, _c;
325
+ const def = this.getModelPolicyDef(model);
326
+ const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
327
+ if (guard === undefined) {
328
+ // field access is denied by default in override mode
329
+ return this.makeFalse();
330
+ }
331
+ if (typeof guard === 'boolean') {
332
+ return this.reduce(guard);
333
+ }
334
+ const r = guard({ user: this.user }, db);
335
+ return this.reduce(r);
336
+ }
337
+ /**
338
+ * Checks if the given model has a policy guard for the given operation.
339
+ */
340
+ hasAuthGuard(model, operation) {
341
+ const guard = this.getModelGuardForOperation(model, operation);
342
+ return typeof guard !== 'boolean' || guard !== true;
343
+ }
344
+ /**
345
+ * Checks if the given model has any field-level override policy guard for the given operation.
346
+ */
347
+ hasOverrideAuthGuard(model, operation) {
348
+ var _a;
349
+ if (operation !== 'read' && operation !== 'update') {
350
+ return false;
351
+ }
352
+ const def = this.getModelPolicyDef(model);
353
+ if ((_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) {
354
+ return Object.values(def.fieldLevel[operation]).some((f) => f.overrideGuard !== undefined || f.overrideEntityChecker !== undefined);
355
+ }
356
+ else {
357
+ return false;
358
+ }
359
+ }
360
+ /**
361
+ * Checks model creation policy based on static analysis to the input args.
362
+ *
363
+ * @returns boolean if static analysis is enough to determine the result, undefined if not
364
+ */
365
+ checkInputGuard(model, args, operation) {
366
+ const def = this.getModelPolicyDef(model);
367
+ const guard = def.modelLevel[operation].inputChecker;
368
+ if (guard === undefined) {
369
+ return undefined;
370
+ }
371
+ if (typeof guard === 'boolean') {
372
+ return guard;
373
+ }
374
+ return guard(args, { user: this.user });
375
+ }
376
+ /**
377
+ * Injects model auth guard as where clause.
378
+ */
379
+ injectAuthGuardAsWhere(db, args, model, operation) {
380
+ let guard = this.getAuthGuard(db, model, operation);
381
+ if (operation === 'update' && args) {
382
+ // merge field-level policy guards
383
+ const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
384
+ if (fieldUpdateGuard.rejectedByField) {
385
+ // rejected
386
+ args.where = this.makeFalse();
387
+ return false;
388
+ }
389
+ else {
390
+ if (fieldUpdateGuard.guard) {
391
+ // merge field-level guard
392
+ guard = this.and(guard, fieldUpdateGuard.guard);
393
+ }
394
+ if (fieldUpdateGuard.overrideGuard) {
395
+ // merge field-level override guard on the top level
396
+ guard = this.or(guard, fieldUpdateGuard.overrideGuard);
397
+ }
398
+ }
399
+ }
400
+ if (operation === 'read') {
401
+ // merge field-level read override guards
402
+ const fieldReadOverrideGuard = this.getFieldReadGuards(db, model, args);
403
+ if (fieldReadOverrideGuard) {
404
+ guard = this.or(guard, fieldReadOverrideGuard);
405
+ }
406
+ }
407
+ if (this.isFalse(guard)) {
408
+ args.where = this.makeFalse();
409
+ return false;
410
+ }
411
+ let mergedGuard = guard;
412
+ if (args.where) {
413
+ // inject into fields:
414
+ // to-many: some/none/every
415
+ // to-one: direct-conditions/is/isNot
416
+ // regular fields
417
+ mergedGuard = this.buildReadGuardForFields(db, model, args.where, guard);
418
+ }
419
+ args.where = this.and(args.where, mergedGuard);
420
+ return true;
421
+ }
422
+ // Injects guard for relation fields nested in `payload`. The `modelGuard` parameter represents the model-level guard for `model`.
423
+ // The function returns a modified copy of `modelGuard` with field-level policies combined.
424
+ buildReadGuardForFields(db, model, payload, modelGuard) {
425
+ if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
426
+ return modelGuard;
427
+ }
428
+ const allFieldGuards = [];
429
+ const allFieldOverrideGuards = [];
430
+ for (const [field, subPayload] of Object.entries(payload)) {
431
+ if (!subPayload) {
432
+ continue;
433
+ }
434
+ allFieldGuards.push(this.getFieldReadAuthGuard(db, model, field));
435
+ allFieldOverrideGuards.push(this.getFieldOverrideReadAuthGuard(db, model, field));
436
+ const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
437
+ if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
438
+ if (fieldInfo.isArray) {
439
+ this.injectReadGuardForToManyField(db, fieldInfo, subPayload);
440
+ }
441
+ else {
442
+ this.injectReadGuardForToOneField(db, fieldInfo, subPayload);
443
+ }
444
+ }
445
+ }
446
+ // all existing field-level guards must be true
447
+ const mergedGuard = this.and(...allFieldGuards);
448
+ // all existing field-level override guards must be true for override to take effect; override is disabled by default
449
+ const mergedOverrideGuard = allFieldOverrideGuards.length === 0 ? this.makeFalse() : this.and(...allFieldOverrideGuards);
450
+ // (original-guard && field-level-guard) || field-level-override-guard
451
+ const updatedGuard = this.or(this.and(modelGuard, mergedGuard), mergedOverrideGuard);
452
+ return updatedGuard;
453
+ }
454
+ injectReadGuardForToManyField(db, fieldInfo, payload) {
455
+ const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
456
+ if (payload.some) {
457
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.some, guard);
458
+ // turn "some" into: { some: { AND: [guard, payload.some] } }
459
+ payload.some = this.and(payload.some, mergedGuard);
460
+ }
461
+ if (payload.none) {
462
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.none, guard);
463
+ // turn none into: { none: { AND: [guard, payload.none] } }
464
+ payload.none = this.and(payload.none, mergedGuard);
465
+ }
466
+ if (payload.every &&
467
+ typeof payload.every === 'object' &&
468
+ // ignore empty every clause
469
+ Object.keys(payload.every).length > 0) {
470
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.every, guard);
471
+ // turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
472
+ if (!payload.none) {
473
+ payload.none = {};
474
+ }
475
+ payload.none = this.and(payload.none, mergedGuard, this.not(payload.every));
476
+ delete payload.every;
477
+ }
478
+ }
479
+ injectReadGuardForToOneField(db, fieldInfo, payload) {
480
+ const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
481
+ // is|isNot and flat fields conditions are mutually exclusive
482
+ // is and isNot can be null value
483
+ if (payload.is !== undefined || payload.isNot !== undefined) {
484
+ if (payload.is) {
485
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.is, guard);
486
+ // merge guard with existing "is": { is: { AND: [originalIs, guard] } }
487
+ payload.is = this.and(payload.is, mergedGuard);
488
+ }
489
+ if (payload.isNot) {
490
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.isNot, guard);
491
+ // merge guard with existing "isNot": { isNot: { AND: [originalIsNot, guard] } }
492
+ payload.isNot = this.and(payload.isNot, mergedGuard);
493
+ }
494
+ }
495
+ else {
496
+ const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload, guard);
497
+ // turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
498
+ const combined = this.and((0, cross_1.clone)(payload), mergedGuard);
499
+ Object.keys(payload).forEach((key) => delete payload[key]);
500
+ payload.is = combined;
501
+ }
502
+ }
503
+ /**
504
+ * Injects auth guard for read operations.
505
+ */
506
+ injectForRead(db, model, args) {
507
+ // make select and include visible to the injection
508
+ const injected = { select: args.select, include: args.include };
509
+ if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) {
510
+ args.where = this.makeFalse();
511
+ return false;
512
+ }
513
+ if (args.where) {
514
+ // inject into fields:
515
+ // to-many: some/none/every
516
+ // to-one: direct-conditions/is/isNot
517
+ // regular fields
518
+ const mergedGuard = this.buildReadGuardForFields(db, model, args.where, {});
519
+ this.mergeWhereClause(args.where, mergedGuard);
520
+ }
521
+ if (args.where) {
522
+ if (injected.where && Object.keys(injected.where).length > 0) {
523
+ // merge injected guard with the user-provided where clause
524
+ this.mergeWhereClause(args.where, injected.where);
525
+ }
526
+ }
527
+ else if (injected.where) {
528
+ // no user-provided where clause, use the injected one
529
+ args.where = injected.where;
530
+ }
531
+ // recursively inject read guard conditions into nested select, include, and _count
532
+ const hoistedConditions = this.injectNestedReadConditions(db, model, args);
533
+ // the injection process may generate conditions that need to be hoisted to the toplevel,
534
+ // if so, merge it with the existing where
535
+ if (hoistedConditions.length > 0) {
536
+ if (!args.where) {
537
+ args.where = this.and(...hoistedConditions);
538
+ }
539
+ else {
540
+ this.mergeWhereClause(args.where, this.and(...hoistedConditions));
541
+ }
542
+ }
543
+ return true;
544
+ }
545
+ //#endregion
546
+ //#region Checker
547
+ /**
548
+ * Gets checker constraints for the given model and operation.
549
+ */
550
+ getCheckerConstraint(model, operation) {
551
+ if (this.options.kinds && !this.options.kinds.includes('policy')) {
552
+ // policy enhancement not enabled, return a constant true checker result
553
+ return true;
554
+ }
555
+ const def = this.getModelPolicyDef(model);
556
+ const checker = def.modelLevel[operation].permissionChecker;
557
+ if (checker === undefined) {
558
+ throw new Error(`Generated permission checkers not found. Please make sure the "generatePermissionChecker" option is set to true in the "@core/enhancer" plugin.`);
559
+ }
560
+ if (typeof checker === 'boolean') {
561
+ return checker;
562
+ }
563
+ if (typeof checker !== 'function') {
564
+ throw this.unknownError(`invalid ${operation} checker function for ${model}`);
565
+ }
566
+ // call checker function
567
+ return checker({ user: this.user });
568
+ }
569
+ //#endregion
570
+ /**
571
+ * Gets unique constraints for the given model.
572
+ */
573
+ getUniqueConstraints(model) {
574
+ var _a, _b;
575
+ return (_b = (_a = this.modelMeta.models[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _a === void 0 ? void 0 : _a.uniqueConstraints) !== null && _b !== void 0 ? _b : {};
576
+ }
577
+ injectNestedReadConditions(db, model, args) {
578
+ var _a;
579
+ const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
580
+ if (!injectTarget) {
581
+ return [];
582
+ }
583
+ if (injectTarget._count !== undefined) {
584
+ // _count needs to respect read policies of related models
585
+ if (injectTarget._count === true) {
586
+ // include count for all relations, expand to all fields
587
+ // so that we can inject guard conditions for each of them
588
+ injectTarget._count = { select: {} };
589
+ const modelFields = (0, cross_1.getFields)(this.modelMeta, model);
590
+ if (modelFields) {
591
+ for (const [k, v] of Object.entries(modelFields)) {
592
+ if (v.isDataModel && v.isArray) {
593
+ // create an entry for to-many relation
594
+ injectTarget._count.select[k] = {};
595
+ }
596
+ }
597
+ }
598
+ }
599
+ // inject conditions for each relation
600
+ for (const field of Object.keys(injectTarget._count.select)) {
601
+ if (typeof injectTarget._count.select[field] !== 'object') {
602
+ injectTarget._count.select[field] = {};
603
+ }
604
+ const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
605
+ if (!fieldInfo) {
606
+ continue;
607
+ }
608
+ // inject into the "where" clause inside select
609
+ this.injectAuthGuardAsWhere(db, injectTarget._count.select[field], fieldInfo.type, 'read');
610
+ }
611
+ }
612
+ // collect filter conditions that should be hoisted to the toplevel
613
+ const hoistedConditions = [];
614
+ for (const field of (0, cross_1.getModelFields)(injectTarget)) {
615
+ if (injectTarget[field] === false) {
616
+ continue;
617
+ }
618
+ const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
619
+ if (!fieldInfo || !fieldInfo.isDataModel) {
620
+ // only care about relation fields
621
+ continue;
622
+ }
623
+ let hoisted;
624
+ if (fieldInfo.isArray ||
625
+ // Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0
626
+ // https://github.com/prisma/prisma/discussions/20350
627
+ fieldInfo.isOptional) {
628
+ if (typeof injectTarget[field] !== 'object') {
629
+ injectTarget[field] = {};
630
+ }
631
+ // inject extra condition for to-many or nullable to-one relation
632
+ this.injectAuthGuardAsWhere(db, injectTarget[field], fieldInfo.type, 'read');
633
+ // recurse
634
+ const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
635
+ if (subHoisted.length > 0) {
636
+ // we can convert it to a where at this level
637
+ injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted);
638
+ }
639
+ }
640
+ else {
641
+ // hoist non-nullable to-one filter to the parent level
642
+ let injected = this.safeClone(injectTarget[field]);
643
+ if (typeof injected !== 'object') {
644
+ injected = {};
645
+ }
646
+ this.injectAuthGuardAsWhere(db, injected, fieldInfo.type, 'read');
647
+ hoisted = injected.where;
648
+ // recurse
649
+ const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
650
+ if (subHoisted.length > 0) {
651
+ hoisted = this.and(hoisted, ...subHoisted);
652
+ }
653
+ }
654
+ if (hoisted && !this.isTrue(hoisted)) {
655
+ hoistedConditions.push({ [field]: hoisted });
656
+ }
657
+ }
658
+ return hoistedConditions;
659
+ }
660
+ /**
661
+ * Given a model and a unique filter, checks the operation is allowed by policies and field validations.
662
+ * Rejects with an error if not allowed.
663
+ */
664
+ checkPolicyForUnique(model, uniqueFilter, operation, db, args, preValue) {
665
+ return __awaiter(this, void 0, void 0, function* () {
666
+ let guard = this.getAuthGuard(db, model, operation, preValue);
667
+ if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
668
+ throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
669
+ }
670
+ let entityChecker;
671
+ if (operation === 'update' && args) {
672
+ // merge field-level policy guards
673
+ const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
674
+ if (fieldUpdateGuard.rejectedByField) {
675
+ // rejected
676
+ throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
677
+ }
678
+ if (fieldUpdateGuard.guard) {
679
+ // merge field-level guard with AND
680
+ guard = this.and(guard, fieldUpdateGuard.guard);
681
+ }
682
+ if (fieldUpdateGuard.overrideGuard) {
683
+ // merge field-level override guard with OR
684
+ guard = this.or(guard, fieldUpdateGuard.overrideGuard);
685
+ }
686
+ // field-level entity checker
687
+ entityChecker = fieldUpdateGuard.entityChecker;
688
+ }
689
+ // Zod schema is to be checked for "create" and "postUpdate"
690
+ const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined;
691
+ // combine field-level entity checker with model-level
692
+ const modelEntityChecker = this.getEntityChecker(model, operation);
693
+ entityChecker = this.combineEntityChecker(entityChecker, modelEntityChecker, 'and');
694
+ if (this.isTrue(guard) && !schema && !entityChecker) {
695
+ // unconditionally allowed
696
+ return;
697
+ }
698
+ let select = schema
699
+ ? // need to validate against schema, need to fetch all fields
700
+ undefined
701
+ : // only fetch id fields
702
+ this.makeIdSelection(model);
703
+ if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
704
+ if (!select) {
705
+ select = this.makeAllScalarFieldSelect(model);
706
+ }
707
+ select = Object.assign(Object.assign({}, select), entityChecker.selector);
708
+ }
709
+ let where = this.safeClone(uniqueFilter);
710
+ // query args may have be of combined-id form, need to flatten it to call findFirst
711
+ this.flattenGeneratedUniqueField(model, where);
712
+ // query with policy guard
713
+ where = this.and(where, guard);
714
+ const query = { select, where };
715
+ if (this.shouldLogQuery) {
716
+ this.logger.info(`[policy] checking ${model} for ${operation}, \`findFirst\`:\n${(0, utils_1.formatObject)(query)}`);
717
+ }
718
+ const result = yield db[model].findFirst(query);
719
+ if (!result) {
720
+ throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
721
+ }
722
+ if (entityChecker) {
723
+ if (this.logger.enabled('info')) {
724
+ this.logger.info(`[policy] running entity checker on ${model} for ${operation}`);
725
+ }
726
+ if (!entityChecker.func(result, { user: this.user, preValue })) {
727
+ throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
728
+ }
729
+ }
730
+ if (schema) {
731
+ // TODO: push down schema check to the database
732
+ this.validateZodSchema(model, undefined, result, true, (err) => {
733
+ throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed validation: [${(0, zod_validation_error_1.fromZodError)(err)}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, err);
734
+ });
735
+ }
736
+ });
737
+ }
738
+ getEntityChecker(model, operation, field) {
739
+ var _a, _b, _c;
740
+ const def = this.getModelPolicyDef(model);
741
+ if (field) {
742
+ return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
743
+ }
744
+ else {
745
+ return def.modelLevel[operation].entityChecker;
746
+ }
747
+ }
748
+ getUpdateOverrideEntityCheckerForField(model, field) {
749
+ var _a, _b, _c;
750
+ const def = this.getModelPolicyDef(model);
751
+ return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideEntityChecker;
752
+ }
753
+ getFieldReadGuards(db, model, args) {
754
+ const allFields = Object.values((0, cross_1.getFields)(this.modelMeta, model));
755
+ // all scalar fields by default
756
+ let fields = allFields.filter((f) => !f.isDataModel);
757
+ if (args.select) {
758
+ // explicitly selected fields
759
+ fields = allFields.filter((f) => { var _a; return ((_a = args.select) === null || _a === void 0 ? void 0 : _a[f.name]) === true; });
760
+ }
761
+ else if (args.include) {
762
+ // included relations
763
+ fields.push(...allFields.filter((f) => !fields.includes(f) && args.include[f.name]));
764
+ }
765
+ if (fields.length === 0) {
766
+ // this can happen if only selecting pseudo fields like "_count"
767
+ return undefined;
768
+ }
769
+ const allFieldGuards = fields.map((field) => this.getFieldOverrideReadAuthGuard(db, model, field.name));
770
+ return this.and(...allFieldGuards);
771
+ }
772
+ getFieldUpdateGuards(db, model, args) {
773
+ var _a;
774
+ const allFieldGuards = [];
775
+ const allOverrideFieldGuards = [];
776
+ let entityChecker;
777
+ for (const [field, value] of Object.entries((_a = args.data) !== null && _a !== void 0 ? _a : args)) {
778
+ if (typeof value === 'undefined') {
779
+ continue;
780
+ }
781
+ const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
782
+ if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
783
+ // relation field update should be treated as foreign key update,
784
+ // fetch and merge all foreign key guards
785
+ if (fieldInfo.isRelationOwner && fieldInfo.foreignKeyMapping) {
786
+ const foreignKeys = Object.values(fieldInfo.foreignKeyMapping);
787
+ for (const fk of foreignKeys) {
788
+ const fieldGuard = this.getFieldUpdateAuthGuard(db, model, fk);
789
+ if (this.isFalse(fieldGuard)) {
790
+ return { guard: fieldGuard, rejectedByField: fk };
791
+ }
792
+ // add field guard
793
+ allFieldGuards.push(fieldGuard);
794
+ // add field override guard
795
+ const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, fk);
796
+ allOverrideFieldGuards.push(overrideFieldGuard);
797
+ }
798
+ }
799
+ }
800
+ else {
801
+ const fieldGuard = this.getFieldUpdateAuthGuard(db, model, field);
802
+ if (this.isFalse(fieldGuard)) {
803
+ return { guard: fieldGuard, rejectedByField: field };
804
+ }
805
+ // add field guard
806
+ allFieldGuards.push(fieldGuard);
807
+ // add field override guard
808
+ const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, field);
809
+ allOverrideFieldGuards.push(overrideFieldGuard);
810
+ }
811
+ // merge regular and override entity checkers with OR
812
+ let checker = this.getEntityChecker(model, 'update', field);
813
+ const overrideChecker = this.getUpdateOverrideEntityCheckerForField(model, field);
814
+ checker = this.combineEntityChecker(checker, overrideChecker, 'or');
815
+ // accumulate entity checker across fields
816
+ entityChecker = this.combineEntityChecker(entityChecker, checker, 'and');
817
+ }
818
+ const allFieldsCombined = this.and(...allFieldGuards);
819
+ const allOverrideFieldsCombined = allOverrideFieldGuards.length !== 0 ? this.and(...allOverrideFieldGuards) : undefined;
820
+ return {
821
+ guard: allFieldsCombined,
822
+ overrideGuard: allOverrideFieldsCombined,
823
+ rejectedByField: undefined,
824
+ entityChecker,
825
+ };
826
+ }
827
+ combineEntityChecker(left, right, combiner) {
828
+ var _a, _b;
829
+ if (!left) {
830
+ return right;
831
+ }
832
+ if (!right) {
833
+ return left;
834
+ }
835
+ const func = combiner === 'and'
836
+ ? (entity, context) => left.func(entity, context) && right.func(entity, context)
837
+ : (entity, context) => left.func(entity, context) || right.func(entity, context);
838
+ return {
839
+ func,
840
+ selector: (0, deepmerge_1.default)((_a = left.selector) !== null && _a !== void 0 ? _a : {}, (_b = right.selector) !== null && _b !== void 0 ? _b : {}),
841
+ };
842
+ }
843
+ /**
844
+ * Tries rejecting a request based on static "false" policy.
845
+ */
846
+ tryReject(db, model, operation) {
847
+ const guard = this.getAuthGuard(db, model, operation);
848
+ if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
849
+ throw this.deniedByPolicy(model, operation, undefined, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
850
+ }
851
+ }
852
+ /**
853
+ * Checks if a model exists given a unique filter.
854
+ */
855
+ checkExistence(db_1, model_1, uniqueFilter_1) {
856
+ return __awaiter(this, arguments, void 0, function* (db, model, uniqueFilter, throwIfNotFound = false) {
857
+ uniqueFilter = this.safeClone(uniqueFilter);
858
+ this.flattenGeneratedUniqueField(model, uniqueFilter);
859
+ if (this.shouldLogQuery) {
860
+ this.logger.info(`[policy] checking ${model} existence, \`findFirst\`:\n${(0, utils_1.formatObject)(uniqueFilter)}`);
861
+ }
862
+ const existing = yield db[model].findFirst({
863
+ where: uniqueFilter,
864
+ select: this.makeIdSelection(model),
865
+ });
866
+ if (!existing && throwIfNotFound) {
867
+ throw this.notFound(model);
868
+ }
869
+ return existing;
870
+ });
871
+ }
872
+ /**
873
+ * Returns an entity given a unique filter with read policy checked. Reject if not readable.
874
+ */
875
+ readBack(db, model, operation, selectInclude, uniqueFilter) {
876
+ return __awaiter(this, void 0, void 0, function* () {
877
+ uniqueFilter = this.safeClone(uniqueFilter);
878
+ this.flattenGeneratedUniqueField(model, uniqueFilter);
879
+ // make sure only select and include are picked
880
+ const selectIncludeClean = this.pick(selectInclude, 'select', 'include');
881
+ const readArgs = Object.assign(Object.assign({}, this.safeClone(selectIncludeClean)), { where: uniqueFilter });
882
+ const error = this.deniedByPolicy(model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
883
+ const injectResult = this.injectForRead(db, model, readArgs);
884
+ if (!injectResult) {
885
+ return { error, result: undefined };
886
+ }
887
+ // inject select needed for field-level read checks
888
+ this.injectReadCheckSelect(model, readArgs);
889
+ if (this.shouldLogQuery) {
890
+ this.logger.info(`[policy] checking read-back, \`findFirst\` ${model}:\n${(0, utils_1.formatObject)(readArgs)}`);
891
+ }
892
+ const result = yield db[model].findFirst(readArgs);
893
+ if (!result) {
894
+ return { error, result: undefined };
895
+ }
896
+ this.postProcessForRead(result, model, selectIncludeClean);
897
+ return { result, error: undefined };
898
+ });
899
+ }
900
+ /**
901
+ * Injects field selection needed for checking field-level read policy check and evaluating
902
+ * entity checker into query args.
903
+ */
904
+ injectReadCheckSelect(model, args) {
905
+ // we need to recurse into relation fields before injecting the current level, because
906
+ // injection into current level can result in relation being selected/included, which
907
+ // can then cause infinite recursion when we visit relation later
908
+ var _a;
909
+ // recurse into relation fields
910
+ const visitTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
911
+ if (visitTarget) {
912
+ for (const key of Object.keys(visitTarget)) {
913
+ const field = (0, cross_1.resolveField)(this.modelMeta, model, key);
914
+ if ((field === null || field === void 0 ? void 0 : field.isDataModel) && visitTarget[key]) {
915
+ if (typeof visitTarget[key] !== 'object') {
916
+ // v is "true", ensure it's an object
917
+ visitTarget[key] = {};
918
+ }
919
+ this.injectReadCheckSelect(field.type, visitTarget[key]);
920
+ }
921
+ }
922
+ }
923
+ if (this.hasFieldLevelPolicy(model)) {
924
+ // recursively inject selection for fields needed for field-level read checks
925
+ const readFieldSelect = this.getFieldReadCheckSelector(model);
926
+ if (readFieldSelect) {
927
+ this.doInjectReadCheckSelect(model, args, { select: readFieldSelect });
928
+ }
929
+ }
930
+ const entityChecker = this.getEntityChecker(model, 'read');
931
+ if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
932
+ this.doInjectReadCheckSelect(model, args, { select: entityChecker.selector });
933
+ }
934
+ }
935
+ doInjectReadCheckSelect(model, args, input) {
936
+ // omit should be ignored to avoid interfering with field selection
937
+ if (args.omit) {
938
+ delete args.omit;
939
+ }
940
+ if (!(input === null || input === void 0 ? void 0 : input.select)) {
941
+ return;
942
+ }
943
+ let target; // injection target
944
+ let isInclude = false; // if the target is include or select
945
+ if (args.select) {
946
+ target = args.select;
947
+ isInclude = false;
948
+ }
949
+ else if (args.include) {
950
+ target = args.include;
951
+ isInclude = true;
952
+ }
953
+ else {
954
+ target = args.select = this.makeAllScalarFieldSelect(model);
955
+ isInclude = false;
956
+ }
957
+ if (!isInclude) {
958
+ // merge selects
959
+ for (const [k, v] of Object.entries(input.select)) {
960
+ if (v === true) {
961
+ if (!target[k]) {
962
+ target[k] = true;
963
+ }
964
+ }
965
+ }
966
+ }
967
+ // recurse into nested selects (relation fields)
968
+ for (const [k, v] of Object.entries(input.select)) {
969
+ if (typeof v === 'object' && (v === null || v === void 0 ? void 0 : v.select)) {
970
+ const field = (0, cross_1.resolveField)(this.modelMeta, model, k);
971
+ if (field === null || field === void 0 ? void 0 : field.isDataModel) {
972
+ // recurse into relation
973
+ if (isInclude && target[k] === true) {
974
+ // select all fields for the relation
975
+ target[k] = { select: this.makeAllScalarFieldSelect(field.type) };
976
+ }
977
+ else if (!target[k]) {
978
+ // ensure an empty select clause
979
+ target[k] = { select: {} };
980
+ }
981
+ // recurse
982
+ this.doInjectReadCheckSelect(field.type, target[k], v);
983
+ }
984
+ }
985
+ }
986
+ }
987
+ makeAllScalarFieldSelect(model) {
988
+ const fields = this.getModelFields(model);
989
+ const result = {};
990
+ if (fields) {
991
+ Object.entries(fields).forEach(([k, v]) => {
992
+ if (!v.isDataModel) {
993
+ result[k] = true;
994
+ }
995
+ });
996
+ }
997
+ return result;
998
+ }
999
+ //#endregion
1000
+ //#region Errors
1001
+ deniedByPolicy(model, operation, extra, reason, zodErrors) {
1002
+ const args = { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINT_FAILED, meta: {} };
1003
+ if (reason) {
1004
+ args.meta.reason = reason;
1005
+ }
1006
+ if (zodErrors) {
1007
+ args.meta.zodErrors = zodErrors;
1008
+ }
1009
+ return (0, utils_1.prismaClientKnownRequestError)(this.db, this.prismaModule, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, args);
1010
+ }
1011
+ notFound(model) {
1012
+ return (0, utils_1.prismaClientKnownRequestError)(this.db, this.prismaModule, `entity not found for model ${model}`, {
1013
+ clientVersion: (0, version_1.getVersion)(),
1014
+ code: 'P2025',
1015
+ });
1016
+ }
1017
+ //#endregion
1018
+ //#region Misc
1019
+ /**
1020
+ * Gets field selection for fetching pre-update entity values for the given model.
1021
+ */
1022
+ getPreValueSelect(model) {
1023
+ const def = this.getModelPolicyDef(model);
1024
+ return def.modelLevel.postUpdate.preUpdateSelector;
1025
+ }
1026
+ // get a merged selector object for all field-level read policies
1027
+ getFieldReadCheckSelector(model) {
1028
+ var _a, _b, _c;
1029
+ const def = this.getModelPolicyDef(model);
1030
+ let result = {};
1031
+ const fieldLevel = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read;
1032
+ if (fieldLevel) {
1033
+ for (const def of Object.values(fieldLevel)) {
1034
+ if ((_b = def.entityChecker) === null || _b === void 0 ? void 0 : _b.selector) {
1035
+ result = (0, deepmerge_1.default)(result, def.entityChecker.selector);
1036
+ }
1037
+ if ((_c = def.overrideEntityChecker) === null || _c === void 0 ? void 0 : _c.selector) {
1038
+ result = (0, deepmerge_1.default)(result, def.overrideEntityChecker.selector);
1039
+ }
1040
+ }
1041
+ }
1042
+ return Object.keys(result).length > 0 ? result : undefined;
1043
+ }
1044
+ checkReadField(model, field, entity) {
1045
+ var _a, _b, _c, _d, _e, _f;
1046
+ const def = this.getModelPolicyDef(model);
1047
+ // combine regular and override field-level entity checkers with OR
1048
+ const checker = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
1049
+ const overrideChecker = (_f = (_e = (_d = def.fieldLevel) === null || _d === void 0 ? void 0 : _d.read) === null || _e === void 0 ? void 0 : _e[field]) === null || _f === void 0 ? void 0 : _f.overrideEntityChecker;
1050
+ const combinedChecker = this.combineEntityChecker(checker, overrideChecker, 'or');
1051
+ if (combinedChecker === undefined) {
1052
+ return true;
1053
+ }
1054
+ else {
1055
+ return combinedChecker.func(entity, { user: this.user });
1056
+ }
1057
+ }
1058
+ hasFieldValidation(model) {
1059
+ var _a, _b;
1060
+ 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;
1061
+ }
1062
+ hasFieldLevelPolicy(model) {
1063
+ var _a, _b;
1064
+ const def = this.getModelPolicyDef(model);
1065
+ return Object.keys((_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) !== null && _b !== void 0 ? _b : {}).length > 0;
1066
+ }
1067
+ /**
1068
+ * Gets Zod schema for the given model and access kind.
1069
+ *
1070
+ * @param kind kind of Zod schema to get for. If undefined, returns the full schema.
1071
+ */
1072
+ getZodSchema(model, excludePasswordFields = true, kind = undefined) {
1073
+ var _a, _b, _c, _d;
1074
+ if (!this.hasFieldValidation(model)) {
1075
+ return undefined;
1076
+ }
1077
+ const schemaKey = `${(0, upper_case_first_1.upperCaseFirst)(model)}${kind ? 'Prisma' + (0, upper_case_first_1.upperCaseFirst)(kind) : ''}Schema`;
1078
+ let result = (_b = (_a = this.zodSchemas) === null || _a === void 0 ? void 0 : _a.models) === null || _b === void 0 ? void 0 : _b[schemaKey];
1079
+ if (result && excludePasswordFields) {
1080
+ // fields with `@password` attribute changes at runtime, so we cannot directly use the generated
1081
+ // zod schema to validate it, instead, the validation happens when checking the input of "create"
1082
+ // and "update" operations
1083
+ const modelFields = (_c = this.modelMeta.models[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _c === void 0 ? void 0 : _c.fields;
1084
+ if (modelFields) {
1085
+ for (const [key, field] of Object.entries(modelFields)) {
1086
+ if ((_d = field.attributes) === null || _d === void 0 ? void 0 : _d.some((attr) => attr.name === '@password')) {
1087
+ // override `@password` field schema with a string schema
1088
+ let pwFieldSchema = zod_1.z.string();
1089
+ if (field.isOptional) {
1090
+ pwFieldSchema = pwFieldSchema.nullish();
1091
+ }
1092
+ result = result === null || result === void 0 ? void 0 : result.merge(zod_1.z.object({ [key]: pwFieldSchema }));
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+ return result;
1098
+ }
1099
+ /**
1100
+ * Validates the given data against the Zod schema for the given model and kind.
1101
+ *
1102
+ * @param model model
1103
+ * @param kind validation kind. Pass undefined to validate against the full schema.
1104
+ * @param data input data
1105
+ * @param excludePasswordFields whether exclude schema validation for `@password` fields
1106
+ * @param onError error callback
1107
+ * @returns Zod-validated data
1108
+ */
1109
+ validateZodSchema(model, kind, data, excludePasswordFields, onError) {
1110
+ const schema = this.getZodSchema(model, excludePasswordFields, kind);
1111
+ if (!schema) {
1112
+ return data;
1113
+ }
1114
+ const parseResult = schema.safeParse(data);
1115
+ if (!parseResult.success) {
1116
+ if (this.logger.enabled('info')) {
1117
+ this.logger.info(`entity ${model} failed validation for operation ${kind}: ${(0, zod_validation_error_1.fromZodError)(parseResult.error)}`);
1118
+ }
1119
+ onError(parseResult.error);
1120
+ return undefined;
1121
+ }
1122
+ return parseResult.data;
1123
+ }
1124
+ /**
1125
+ * Post processing checks and clean-up for read model entities.
1126
+ */
1127
+ postProcessForRead(data, model, queryArgs) {
1128
+ // preserve the original data as it may be needed for checking field-level readability,
1129
+ // while the "data" will be manipulated during traversal (deleting unreadable fields)
1130
+ const origData = this.safeClone(data);
1131
+ return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
1132
+ }
1133
+ doPostProcessForRead(data, model, fullData, queryArgs, hasFieldLevelPolicy, path = '') {
1134
+ var _a, _b, _c;
1135
+ if (data === null || data === undefined) {
1136
+ return data;
1137
+ }
1138
+ let filteredData = data;
1139
+ let filteredFullData = fullData;
1140
+ const entityChecker = this.getEntityChecker(model, 'read');
1141
+ if (entityChecker) {
1142
+ if (Array.isArray(data)) {
1143
+ filteredData = [];
1144
+ filteredFullData = [];
1145
+ for (const [entityData, entityFullData] of (0, cross_1.zip)(data, fullData)) {
1146
+ if (!entityChecker.func(entityData, { user: this.user })) {
1147
+ if (this.shouldLogQuery) {
1148
+ this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
1149
+ }
1150
+ }
1151
+ else {
1152
+ filteredData.push(entityData);
1153
+ filteredFullData.push(entityFullData);
1154
+ }
1155
+ }
1156
+ }
1157
+ else {
1158
+ if (!entityChecker.func(data, { user: this.user })) {
1159
+ if (this.shouldLogQuery) {
1160
+ this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
1161
+ }
1162
+ return null;
1163
+ }
1164
+ }
1165
+ }
1166
+ for (const [entityData, entityFullData] of (0, cross_1.zip)(filteredData, filteredFullData)) {
1167
+ if (typeof entityData !== 'object' || !entityData) {
1168
+ continue;
1169
+ }
1170
+ for (const [field, fieldData] of Object.entries(entityData)) {
1171
+ if (fieldData === undefined) {
1172
+ continue;
1173
+ }
1174
+ const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
1175
+ if (!fieldInfo) {
1176
+ // could be _count, etc.
1177
+ continue;
1178
+ }
1179
+ if (((_a = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.omit) === null || _a === void 0 ? void 0 : _a[field]) === true) {
1180
+ // respect `{ omit: { [field]: true } }`
1181
+ delete entityData[field];
1182
+ continue;
1183
+ }
1184
+ if (hasFieldLevelPolicy) {
1185
+ // 1. remove fields selected for checking field-level policies but not selected by the original query args
1186
+ // 2. evaluate field-level policies and remove fields that are not readable
1187
+ if (!fieldInfo.isDataModel) {
1188
+ // scalar field, delete unselected ones
1189
+ const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
1190
+ if (select && typeof select === 'object' && select[field] !== true) {
1191
+ // there's a select clause but this field is not included
1192
+ delete entityData[field];
1193
+ continue;
1194
+ }
1195
+ }
1196
+ else {
1197
+ // relation field, delete if not selected or included
1198
+ const include = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include;
1199
+ const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
1200
+ if (!(include === null || include === void 0 ? void 0 : include[field]) && !(select === null || select === void 0 ? void 0 : select[field])) {
1201
+ // relation field not included or selected
1202
+ delete entityData[field];
1203
+ continue;
1204
+ }
1205
+ }
1206
+ // delete unreadable fields
1207
+ if (!this.checkReadField(model, field, entityFullData)) {
1208
+ if (this.shouldLogQuery) {
1209
+ this.logger.info(`[policy] dropping unreadable field ${path ? path + '.' : ''}${field}`);
1210
+ }
1211
+ delete entityData[field];
1212
+ continue;
1213
+ }
1214
+ }
1215
+ if (fieldInfo.isDataModel) {
1216
+ // recurse into nested fields
1217
+ const nextArgs = (_c = ((_b = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select) !== null && _b !== void 0 ? _b : queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include)) === null || _c === void 0 ? void 0 : _c[field];
1218
+ const nestedResult = this.doPostProcessForRead(fieldData, fieldInfo.type, entityFullData[field], nextArgs, this.hasFieldLevelPolicy(fieldInfo.type), path ? path + '.' + field : field);
1219
+ if (nestedResult === undefined) {
1220
+ delete entityData[field];
1221
+ }
1222
+ else {
1223
+ entityData[field] = nestedResult;
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1228
+ return filteredData;
1229
+ }
1230
+ /**
1231
+ * Replace content of `target` object with `withObject` in-place.
1232
+ */
1233
+ replace(target, withObject) {
1234
+ if (!target || typeof target !== 'object' || !withObject || typeof withObject !== 'object') {
1235
+ return;
1236
+ }
1237
+ // remove missing keys
1238
+ for (const key of Object.keys(target)) {
1239
+ if (!(key in withObject)) {
1240
+ delete target[key];
1241
+ }
1242
+ }
1243
+ // overwrite keys
1244
+ for (const [key, value] of Object.entries(withObject)) {
1245
+ target[key] = value;
1246
+ }
1247
+ }
1248
+ /**
1249
+ * Picks properties from an object.
1250
+ */
1251
+ pick(value, ...props) {
1252
+ const v = value;
1253
+ return props.reduce(function (result, prop) {
1254
+ if (prop in v) {
1255
+ result[prop] = v[prop];
1256
+ }
1257
+ return result;
1258
+ }, {});
1259
+ }
1260
+ mergeWhereClause(where, extra) {
1261
+ var _a;
1262
+ if (!where) {
1263
+ throw new Error('invalid where clause');
1264
+ }
1265
+ if (this.isTrue(extra)) {
1266
+ return;
1267
+ }
1268
+ // instead of simply wrapping with AND, we preserve the structure
1269
+ // of the original where clause and merge `extra` into it so that
1270
+ // unique query can continue working
1271
+ if (where.AND) {
1272
+ // merge into existing AND clause
1273
+ const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND];
1274
+ conditions.push(extra);
1275
+ const combined = this.and(...conditions);
1276
+ // make sure the merging always goes under AND
1277
+ where.AND = (_a = combined.AND) !== null && _a !== void 0 ? _a : combined;
1278
+ }
1279
+ else {
1280
+ // insert an AND clause
1281
+ where.AND = [extra];
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Given an entity data, returns an object only containing id fields.
1286
+ */
1287
+ getIdFieldValues(model, data) {
1288
+ if (!data) {
1289
+ return undefined;
1290
+ }
1291
+ const idFields = this.getIdFields(model);
1292
+ return Object.fromEntries(idFields.map((f) => [f.name, data[f.name]]));
1293
+ }
1294
+ }
1295
+ exports.PolicyUtil = PolicyUtil;
1296
+ //# sourceMappingURL=policy-utils.js.map