@zenstackhq/runtime 1.0.0-beta.2 → 1.0.0-beta.20

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 (75) hide show
  1. package/browser/index.d.mts +13 -0
  2. package/browser/index.d.ts +13 -0
  3. package/browser/index.js +70 -0
  4. package/browser/index.js.map +1 -0
  5. package/browser/index.mjs +32 -0
  6. package/browser/index.mjs.map +1 -0
  7. package/constants.d.ts +59 -2
  8. package/constants.js +60 -2
  9. package/constants.js.map +1 -1
  10. package/enhancements/enhance.d.ts +18 -0
  11. package/enhancements/enhance.js +42 -0
  12. package/enhancements/enhance.js.map +1 -0
  13. package/enhancements/index.d.ts +5 -0
  14. package/enhancements/index.js +5 -0
  15. package/enhancements/index.js.map +1 -1
  16. package/enhancements/model-data-visitor.d.ts +16 -0
  17. package/enhancements/model-data-visitor.js +41 -0
  18. package/enhancements/model-data-visitor.js.map +1 -0
  19. package/enhancements/model-meta.d.ts +6 -1
  20. package/enhancements/model-meta.js +23 -2
  21. package/enhancements/model-meta.js.map +1 -1
  22. package/enhancements/nested-write-vistor.d.ts +21 -16
  23. package/enhancements/nested-write-vistor.js +73 -34
  24. package/enhancements/nested-write-vistor.js.map +1 -1
  25. package/enhancements/omit.d.ts +1 -1
  26. package/enhancements/policy/handler.d.ts +25 -15
  27. package/enhancements/policy/handler.js +771 -133
  28. package/enhancements/policy/handler.js.map +1 -1
  29. package/enhancements/policy/index.d.ts +5 -1
  30. package/enhancements/policy/index.js +53 -3
  31. package/enhancements/policy/index.js.map +1 -1
  32. package/enhancements/policy/logger.js +1 -1
  33. package/enhancements/policy/logger.js.map +1 -1
  34. package/enhancements/policy/policy-utils.d.ts +102 -46
  35. package/enhancements/policy/policy-utils.js +683 -550
  36. package/enhancements/policy/policy-utils.js.map +1 -1
  37. package/enhancements/preset.d.ts +3 -8
  38. package/enhancements/preset.js +2 -4
  39. package/enhancements/preset.js.map +1 -1
  40. package/enhancements/proxy.d.ts +3 -1
  41. package/enhancements/proxy.js +23 -12
  42. package/enhancements/proxy.js.map +1 -1
  43. package/enhancements/types.d.ts +25 -8
  44. package/enhancements/types.js +1 -0
  45. package/enhancements/types.js.map +1 -1
  46. package/enhancements/utils.d.ts +4 -0
  47. package/enhancements/utils.js +59 -8
  48. package/enhancements/utils.js.map +1 -1
  49. package/enhancements/where-visitor.d.ts +33 -0
  50. package/enhancements/where-visitor.js +87 -0
  51. package/enhancements/where-visitor.js.map +1 -0
  52. package/error.js +9 -3
  53. package/error.js.map +1 -1
  54. package/index.d.ts +3 -2
  55. package/index.js +3 -2
  56. package/index.js.map +1 -1
  57. package/package.json +33 -9
  58. package/types.d.ts +11 -2
  59. package/types.js +2 -0
  60. package/types.js.map +1 -1
  61. package/version.d.ts +5 -0
  62. package/version.js +34 -1
  63. package/version.js.map +1 -1
  64. package/zod/index.d.ts +2 -0
  65. package/zod/index.js +4 -0
  66. package/zod/input.d.ts +1 -0
  67. package/zod/input.js +8 -0
  68. package/zod/models.d.ts +1 -0
  69. package/zod/models.js +8 -0
  70. package/serialization-utils.d.ts +0 -1
  71. package/serialization-utils.js +0 -22
  72. package/serialization-utils.js.map +0 -1
  73. package/zod.d.ts +0 -10
  74. package/zod.js +0 -17
  75. package/zod.js.map +0 -1
@@ -9,9 +9,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  step((generator = generator.apply(thisArg, _arguments || [])).next());
10
10
  });
11
11
  };
12
+ var __rest = (this && this.__rest) || function (s, e) {
13
+ var t = {};
14
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
15
+ t[p] = s[p];
16
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
17
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
18
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
19
+ t[p[i]] = s[p[i]];
20
+ }
21
+ return t;
22
+ };
12
23
  Object.defineProperty(exports, "__esModule", { value: true });
13
24
  exports.PolicyProxyHandler = void 0;
25
+ const upper_case_first_1 = require("upper-case-first");
26
+ const zod_validation_error_1 = require("zod-validation-error");
14
27
  const constants_1 = require("../../constants");
28
+ const model_data_visitor_1 = require("../model-data-visitor");
29
+ const model_meta_1 = require("../model-meta");
30
+ const nested_write_vistor_1 = require("../nested-write-vistor");
15
31
  const utils_1 = require("../utils");
16
32
  const logger_1 = require("./logger");
17
33
  const policy_utils_1 = require("./policy-utils");
@@ -19,21 +35,23 @@ const policy_utils_1 = require("./policy-utils");
19
35
  * Prisma proxy handler for injecting access policy check.
20
36
  */
21
37
  class PolicyProxyHandler {
22
- constructor(prisma, policy, modelMeta, model, user, logPrismaQuery) {
38
+ constructor(prisma, policy, modelMeta, zodSchemas, model, user, logPrismaQuery) {
23
39
  this.prisma = prisma;
24
40
  this.policy = policy;
25
41
  this.modelMeta = modelMeta;
42
+ this.zodSchemas = zodSchemas;
26
43
  this.model = model;
27
44
  this.user = user;
28
45
  this.logPrismaQuery = logPrismaQuery;
29
46
  this.logger = new logger_1.Logger(prisma);
30
- this.utils = new policy_utils_1.PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user, this.logPrismaQuery);
47
+ this.utils = new policy_utils_1.PolicyUtil(this.prisma, this.modelMeta, this.policy, this.zodSchemas, this.user, this.shouldLogQuery);
31
48
  }
32
49
  get modelClient() {
33
50
  return this.prisma[this.model];
34
51
  }
52
+ //#region Find
53
+ // find operations behaves as if the entities that don't match access policies don't exist
35
54
  findUnique(args) {
36
- var _a;
37
55
  return __awaiter(this, void 0, void 0, function* () {
38
56
  if (!args) {
39
57
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
@@ -41,60 +59,92 @@ class PolicyProxyHandler {
41
59
  if (!args.where) {
42
60
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'where field is required in query argument');
43
61
  }
44
- const guard = yield this.utils.getAuthGuard(this.model, 'read');
45
- if (guard === false) {
62
+ const origArgs = args;
63
+ args = this.utils.clone(args);
64
+ if (!(yield this.utils.injectForRead(this.prisma, this.model, args))) {
46
65
  return null;
47
66
  }
48
- const entities = yield this.utils.readWithCheck(this.model, args);
49
- return (_a = entities[0]) !== null && _a !== void 0 ? _a : null;
67
+ this.utils.injectReadCheckSelect(this.model, args);
68
+ if (this.shouldLogQuery) {
69
+ this.logger.info(`[policy] \`findUnique\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
70
+ }
71
+ const result = yield this.modelClient.findUnique(args);
72
+ this.utils.postProcessForRead(result, this.model, origArgs);
73
+ return result;
50
74
  });
51
75
  }
52
76
  findUniqueOrThrow(args) {
53
77
  return __awaiter(this, void 0, void 0, function* () {
54
- const guard = yield this.utils.getAuthGuard(this.model, 'read');
55
- if (guard === false) {
56
- throw this.utils.notFound(this.model);
78
+ if (!args) {
79
+ throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
80
+ }
81
+ if (!args.where) {
82
+ throw (0, utils_1.prismaClientValidationError)(this.prisma, 'where field is required in query argument');
57
83
  }
58
- const entity = yield this.findUnique(args);
59
- if (!entity) {
84
+ const origArgs = args;
85
+ args = this.utils.clone(args);
86
+ if (!(yield this.utils.injectForRead(this.prisma, this.model, args))) {
60
87
  throw this.utils.notFound(this.model);
61
88
  }
62
- return entity;
89
+ this.utils.injectReadCheckSelect(this.model, args);
90
+ if (this.shouldLogQuery) {
91
+ this.logger.info(`[policy] \`findUniqueOrThrow\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
92
+ }
93
+ const result = yield this.modelClient.findUniqueOrThrow(args);
94
+ this.utils.postProcessForRead(result, this.model, origArgs);
95
+ return result;
63
96
  });
64
97
  }
65
98
  findFirst(args) {
66
- var _a;
67
99
  return __awaiter(this, void 0, void 0, function* () {
68
- const guard = yield this.utils.getAuthGuard(this.model, 'read');
69
- if (guard === false) {
100
+ const origArgs = args;
101
+ args = args ? this.utils.clone(args) : {};
102
+ if (!(yield this.utils.injectForRead(this.prisma, this.model, args))) {
70
103
  return null;
71
104
  }
72
- const entities = yield this.utils.readWithCheck(this.model, args);
73
- return (_a = entities[0]) !== null && _a !== void 0 ? _a : null;
105
+ this.utils.injectReadCheckSelect(this.model, args);
106
+ if (this.shouldLogQuery) {
107
+ this.logger.info(`[policy] \`findFirst\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
108
+ }
109
+ const result = yield this.modelClient.findFirst(args);
110
+ this.utils.postProcessForRead(result, this.model, origArgs);
111
+ return result;
74
112
  });
75
113
  }
76
114
  findFirstOrThrow(args) {
77
115
  return __awaiter(this, void 0, void 0, function* () {
78
- const guard = yield this.utils.getAuthGuard(this.model, 'read');
79
- if (guard === false) {
116
+ const origArgs = args;
117
+ args = args ? this.utils.clone(args) : {};
118
+ if (!(yield this.utils.injectForRead(this.prisma, this.model, args))) {
80
119
  throw this.utils.notFound(this.model);
81
120
  }
82
- const entity = yield this.findFirst(args);
83
- if (!entity) {
84
- throw this.utils.notFound(this.model);
121
+ this.utils.injectReadCheckSelect(this.model, args);
122
+ if (this.shouldLogQuery) {
123
+ this.logger.info(`[policy] \`findFirstOrThrow\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
85
124
  }
86
- return entity;
125
+ const result = yield this.modelClient.findFirstOrThrow(args);
126
+ this.utils.postProcessForRead(result, this.model, origArgs);
127
+ return result;
87
128
  });
88
129
  }
89
130
  findMany(args) {
90
131
  return __awaiter(this, void 0, void 0, function* () {
91
- const guard = yield this.utils.getAuthGuard(this.model, 'read');
92
- if (guard === false) {
132
+ const origArgs = args;
133
+ args = args ? this.utils.clone(args) : {};
134
+ if (!(yield this.utils.injectForRead(this.prisma, this.model, args))) {
93
135
  return [];
94
136
  }
95
- return this.utils.readWithCheck(this.model, args);
137
+ this.utils.injectReadCheckSelect(this.model, args);
138
+ if (this.shouldLogQuery) {
139
+ this.logger.info(`[policy] \`findMany\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
140
+ }
141
+ const result = yield this.modelClient.findMany(args);
142
+ this.utils.postProcessForRead(result, this.model, origArgs);
143
+ return result;
96
144
  });
97
145
  }
146
+ //#endregion
147
+ //#region Create
98
148
  create(args) {
99
149
  return __awaiter(this, void 0, void 0, function* () {
100
150
  if (!args) {
@@ -103,45 +153,316 @@ class PolicyProxyHandler {
103
153
  if (!args.data) {
104
154
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
105
155
  }
106
- yield this.tryReject('create');
156
+ yield this.utils.tryReject(this.prisma, this.model, 'create');
107
157
  const origArgs = args;
108
158
  args = this.utils.clone(args);
109
- // use a transaction to wrap the write so it can be reverted if the created
110
- // entity fails access policies
111
- const result = yield this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => {
112
- if (this.shouldLogQuery) {
113
- this.logger.info(`[withPolicy] \`create\`: ${(0, utils_1.formatObject)(writeArgs)}`);
159
+ // static input policy check for top-level create data
160
+ const inputCheck = this.utils.checkInputGuard(this.model, args.data, 'create');
161
+ if (inputCheck === false) {
162
+ throw this.utils.deniedByPolicy(this.model, 'create');
163
+ }
164
+ const hasNestedCreateOrConnect = yield this.hasNestedCreateOrConnect(args);
165
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
166
+ if (
167
+ // MUST check true here since inputCheck can be undefined (meaning static input check not possible)
168
+ inputCheck === true &&
169
+ // simple create: no nested create/connect
170
+ !hasNestedCreateOrConnect) {
171
+ // there's no nested write and we've passed input check, proceed with the create directly
172
+ // validate zod schema if any
173
+ this.validateCreateInputSchema(this.model, args.data);
174
+ // make a create args only containing data and ID selection
175
+ const createArgs = { data: args.data, select: this.utils.makeIdSelection(this.model) };
176
+ if (this.shouldLogQuery) {
177
+ this.logger.info(`[policy] \`create\` ${this.model}: ${(0, utils_1.formatObject)(createArgs)}`);
178
+ }
179
+ const result = yield tx[this.model].create(createArgs);
180
+ // filter the read-back data
181
+ return this.utils.readBack(tx, this.model, 'create', args, result);
114
182
  }
115
- return dbOps.create(writeArgs);
183
+ else {
184
+ // proceed with a complex create and collect post-write checks
185
+ const { result, postWriteChecks } = yield this.doCreate(this.model, args, tx);
186
+ // execute post-write checks
187
+ yield this.runPostWriteChecks(postWriteChecks, tx);
188
+ // filter the read-back data
189
+ return this.utils.readBack(tx, this.model, 'create', origArgs, result);
190
+ }
191
+ }));
192
+ if (error) {
193
+ throw error;
194
+ }
195
+ else {
196
+ return result;
197
+ }
198
+ });
199
+ }
200
+ // create with nested write
201
+ doCreate(model, args, db) {
202
+ return __awaiter(this, void 0, void 0, function* () {
203
+ // record id fields involved in the nesting context
204
+ const idSelections = [];
205
+ const pushIdFields = (model, context) => {
206
+ const idFields = (0, utils_1.getIdFields)(this.modelMeta, model);
207
+ idSelections.push({
208
+ path: context.nestingPath.map((p) => p.field).filter((f) => !!f),
209
+ ids: idFields.map((f) => f.name),
210
+ });
211
+ };
212
+ // create a string key that uniquely identifies an entity
213
+ const getEntityKey = (model, ids) => `${(0, upper_case_first_1.upperCaseFirst)(model)}#${Object.keys(ids)
214
+ .sort()
215
+ .map((f) => { var _a; return `${f}:${(_a = ids[f]) === null || _a === void 0 ? void 0 : _a.toString()}`; })
216
+ .join('_')}`;
217
+ // record keys of entities that are connected instead of created
218
+ const connectedEntities = new Set();
219
+ // visit the create payload
220
+ const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
221
+ create: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
222
+ this.validateCreateInputSchema(model, args);
223
+ pushIdFields(model, context);
224
+ }),
225
+ createMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
226
+ (0, utils_1.enumerate)(args.data).forEach((item) => this.validateCreateInputSchema(model, item));
227
+ pushIdFields(model, context);
228
+ }),
229
+ connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
230
+ var _a;
231
+ if (!args.where) {
232
+ throw this.utils.validationError(`'where' field is required for connectOrCreate`);
233
+ }
234
+ this.validateCreateInputSchema(model, args.create);
235
+ const existing = yield this.utils.checkExistence(db, model, args.where);
236
+ if (existing) {
237
+ // connect case
238
+ if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
239
+ const backLinkField = (0, model_meta_1.resolveField)(this.modelMeta, model, context.field.backLink);
240
+ if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isRelationOwner) {
241
+ // the target side of relation owns the relation,
242
+ // check if it's updatable
243
+ yield this.utils.checkPolicyForUnique(model, args.where, 'update', db, args);
244
+ }
245
+ }
246
+ if (context.parent.connect) {
247
+ // if the payload parent already has a "connect" clause, merge it
248
+ if (Array.isArray(context.parent.connect)) {
249
+ context.parent.connect.push(args.where);
250
+ }
251
+ else {
252
+ context.parent.connect = [context.parent.connect, args.where];
253
+ }
254
+ }
255
+ else {
256
+ // otherwise, create a new "connect" clause
257
+ context.parent.connect = args.where;
258
+ }
259
+ // record the key of connected entities so we can avoid validating them later
260
+ connectedEntities.add(getEntityKey(model, existing));
261
+ }
262
+ else {
263
+ // create case
264
+ pushIdFields(model, context);
265
+ // create a new "create" clause at the parent level
266
+ context.parent.create = args.create;
267
+ }
268
+ // remove the connectOrCreate clause
269
+ delete context.parent['connectOrCreate'];
270
+ // return false to prevent visiting the nested payload
271
+ return false;
272
+ }),
273
+ connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
274
+ var _b;
275
+ if (!args || typeof args !== 'object' || Object.keys(args).length === 0) {
276
+ throw this.utils.validationError(`'connect' field must be an non-empty object`);
277
+ }
278
+ if ((_b = context.field) === null || _b === void 0 ? void 0 : _b.backLink) {
279
+ const backLinkField = (0, model_meta_1.resolveField)(this.modelMeta, model, context.field.backLink);
280
+ if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isRelationOwner) {
281
+ // check existence
282
+ yield this.utils.checkExistence(db, model, args, true);
283
+ // the target side of relation owns the relation,
284
+ // check if it's updatable
285
+ yield this.utils.checkPolicyForUnique(model, args, 'update', db, args);
286
+ }
287
+ }
288
+ }),
116
289
  });
290
+ yield visitor.visit(model, 'create', args);
291
+ // build the final "select" clause including all nested ID fields
292
+ let select = undefined;
293
+ if (idSelections.length > 0) {
294
+ select = {};
295
+ idSelections.forEach(({ path, ids }) => {
296
+ let curr = select;
297
+ for (const p of path) {
298
+ if (!curr[p.name]) {
299
+ curr[p.name] = { select: {} };
300
+ }
301
+ curr = curr[p.name].select;
302
+ }
303
+ Object.assign(curr, ...ids.map((f) => ({ [f]: true })));
304
+ });
305
+ }
306
+ // proceed with the create
307
+ const createArgs = { data: args.data, select };
308
+ if (this.shouldLogQuery) {
309
+ this.logger.info(`[policy] \`create\` ${model}: ${(0, utils_1.formatObject)(createArgs)}`);
310
+ }
311
+ const result = yield db[model].create(createArgs);
312
+ // post create policy check for the top-level and nested creates
313
+ const postCreateChecks = new Map();
314
+ // visit the create result and collect entities that need to be post-checked
315
+ const modelDataVisitor = new model_data_visitor_1.ModelDataVisitor(this.modelMeta);
316
+ modelDataVisitor.visit(model, result, (model, _data, scalarData) => {
317
+ const key = getEntityKey(model, scalarData);
318
+ // only check if entity is created, not connected
319
+ if (!connectedEntities.has(key) && !postCreateChecks.has(key)) {
320
+ postCreateChecks.set(key, { model, operation: 'create', uniqueFilter: scalarData });
321
+ }
322
+ });
323
+ // return only the ids of the top-level entity
117
324
  const ids = this.utils.getEntityIds(this.model, result);
118
- if (Object.keys(ids).length === 0) {
119
- throw this.utils.unknownError(`unexpected error: create didn't return an id`);
120
- }
121
- return this.checkReadback(origArgs, ids, 'create', 'create');
325
+ return { result: ids, postWriteChecks: [...postCreateChecks.values()] };
326
+ });
327
+ }
328
+ // Checks if the given create payload has nested create or connect
329
+ hasNestedCreateOrConnect(args) {
330
+ return __awaiter(this, void 0, void 0, function* () {
331
+ let hasNestedCreateOrConnect = false;
332
+ const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
333
+ create(_model, _args, context) {
334
+ return __awaiter(this, void 0, void 0, function* () {
335
+ if (context.field) {
336
+ hasNestedCreateOrConnect = true;
337
+ return false;
338
+ }
339
+ else {
340
+ return true;
341
+ }
342
+ });
343
+ },
344
+ connect() {
345
+ return __awaiter(this, void 0, void 0, function* () {
346
+ hasNestedCreateOrConnect = true;
347
+ return false;
348
+ });
349
+ },
350
+ connectOrCreate() {
351
+ return __awaiter(this, void 0, void 0, function* () {
352
+ hasNestedCreateOrConnect = true;
353
+ return false;
354
+ });
355
+ },
356
+ createMany() {
357
+ return __awaiter(this, void 0, void 0, function* () {
358
+ hasNestedCreateOrConnect = true;
359
+ return false;
360
+ });
361
+ },
362
+ });
363
+ yield visitor.visit(this.model, 'create', args);
364
+ return hasNestedCreateOrConnect;
122
365
  });
123
366
  }
124
- createMany(args, skipDuplicates) {
367
+ // Validates the given create payload against Zod schema if any
368
+ validateCreateInputSchema(model, data) {
369
+ const schema = this.utils.getZodSchema(model, 'create');
370
+ if (schema) {
371
+ const parseResult = schema.safeParse(data);
372
+ if (!parseResult.success) {
373
+ throw this.utils.deniedByPolicy(model, 'create', `input failed validation: ${(0, zod_validation_error_1.fromZodError)(parseResult.error)}`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
374
+ }
375
+ }
376
+ }
377
+ createMany(args) {
125
378
  return __awaiter(this, void 0, void 0, function* () {
126
379
  if (!args) {
127
380
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
128
381
  }
129
382
  if (!args.data) {
130
- throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required and must be an array');
383
+ throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
131
384
  }
132
- yield this.tryReject('create');
385
+ this.utils.tryReject(this.prisma, this.model, 'create');
133
386
  args = this.utils.clone(args);
134
- // use a transaction to wrap the write so it can be reverted if any created
135
- // entity fails access policies
136
- const result = yield this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => {
387
+ // do static input validation and check if post-create checks are needed
388
+ let needPostCreateCheck = false;
389
+ for (const item of (0, utils_1.enumerate)(args.data)) {
390
+ const inputCheck = this.utils.checkInputGuard(this.model, item, 'create');
391
+ if (inputCheck === false) {
392
+ throw this.utils.deniedByPolicy(this.model, 'create');
393
+ }
394
+ else if (inputCheck === true) {
395
+ this.validateCreateInputSchema(this.model, item);
396
+ }
397
+ else if (inputCheck === undefined) {
398
+ // static policy check is not possible, need to do post-create check
399
+ needPostCreateCheck = true;
400
+ break;
401
+ }
402
+ }
403
+ if (!needPostCreateCheck) {
404
+ return this.modelClient.createMany(args);
405
+ }
406
+ else {
407
+ // create entities in a transaction with post-create checks
408
+ return this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
409
+ const { result, postWriteChecks } = yield this.doCreateMany(this.model, args, tx);
410
+ // post-create check
411
+ yield this.runPostWriteChecks(postWriteChecks, tx);
412
+ return result;
413
+ }));
414
+ }
415
+ });
416
+ }
417
+ doCreateMany(model, args, db) {
418
+ return __awaiter(this, void 0, void 0, function* () {
419
+ // We can't call the native "createMany" because we can't get back what was created
420
+ // for post-create checks. Instead, do a "create" for each item and collect the results.
421
+ let createResult = yield Promise.all((0, utils_1.enumerate)(args.data).map((item) => __awaiter(this, void 0, void 0, function* () {
422
+ if (args.skipDuplicates) {
423
+ // check unique constraint conflicts
424
+ // we can't rely on try/catch/ignore constraint violation error: https://github.com/prisma/prisma/issues/20496
425
+ // TODO: for simple cases we should be able to translate it to an `upsert` with empty `update` payload
426
+ // for each unique constraint, check if the input item has all fields set, and if so, check if
427
+ // an entity already exists, and ignore accordingly
428
+ const uniqueConstraints = this.utils.getUniqueConstraints(model);
429
+ for (const constraint of Object.values(uniqueConstraints)) {
430
+ if (constraint.fields.every((f) => item[f] !== undefined)) {
431
+ const uniqueFilter = constraint.fields.reduce((acc, f) => (Object.assign(Object.assign({}, acc), { [f]: item[f] })), {});
432
+ const existing = yield this.utils.checkExistence(db, model, uniqueFilter);
433
+ if (existing) {
434
+ if (this.shouldLogQuery) {
435
+ this.logger.info(`[policy] skipping duplicate ${(0, utils_1.formatObject)(item)}`);
436
+ }
437
+ return undefined;
438
+ }
439
+ }
440
+ }
441
+ }
137
442
  if (this.shouldLogQuery) {
138
- this.logger.info(`[withPolicy] \`createMany\`: ${(0, utils_1.formatObject)(writeArgs)}`);
443
+ this.logger.info(`[policy] \`create\` ${model}: ${(0, utils_1.formatObject)(item)}`);
139
444
  }
140
- return dbOps.createMany(writeArgs, skipDuplicates);
141
- });
142
- return result;
445
+ return yield db[model].create({ select: this.utils.makeIdSelection(model), data: item });
446
+ })));
447
+ // filter undefined values due to skipDuplicates
448
+ createResult = createResult.filter((p) => !!p);
449
+ return {
450
+ result: { count: createResult.length },
451
+ postWriteChecks: createResult.map((item) => ({
452
+ model,
453
+ operation: 'create',
454
+ uniqueFilter: item,
455
+ })),
456
+ };
143
457
  });
144
458
  }
459
+ //#endregion
460
+ //#region Update & Upsert
461
+ // "update" and "upsert" work against unique entity, so we actively rejects the request if the
462
+ // entity fails policy check
463
+ //
464
+ // "updateMany" works against a set of entities, entities not passing policy check are silently
465
+ // ignored
145
466
  update(args) {
146
467
  return __awaiter(this, void 0, void 0, function* () {
147
468
  if (!args) {
@@ -153,22 +474,259 @@ class PolicyProxyHandler {
153
474
  if (!args.data) {
154
475
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
155
476
  }
156
- yield this.tryReject('update');
157
- const origArgs = args;
158
477
  args = this.utils.clone(args);
159
- // use a transaction to wrap the write so it can be reverted if any nested
160
- // create fails access policies
161
- const result = yield this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => {
162
- if (this.shouldLogQuery) {
163
- this.logger.info(`[withPolicy] \`update\`: ${(0, utils_1.formatObject)(writeArgs)}`);
478
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
479
+ // proceed with nested writes and collect post-write checks
480
+ const { result, postWriteChecks } = yield this.doUpdate(args, tx);
481
+ // post-write check
482
+ yield this.runPostWriteChecks(postWriteChecks, tx);
483
+ // filter the read-back data
484
+ return this.utils.readBack(tx, this.model, 'update', args, result);
485
+ }));
486
+ if (error) {
487
+ throw error;
488
+ }
489
+ else {
490
+ return result;
491
+ }
492
+ });
493
+ }
494
+ doUpdate(args, db) {
495
+ return __awaiter(this, void 0, void 0, function* () {
496
+ // collected post-update checks
497
+ const postWriteChecks = [];
498
+ // registers a post-update check task
499
+ const _registerPostUpdateCheck = (model, uniqueFilter) => __awaiter(this, void 0, void 0, function* () {
500
+ // both "post-update" rules and Zod schemas require a post-update check
501
+ if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) {
502
+ // select pre-update field values
503
+ let preValue;
504
+ const preValueSelect = this.utils.getPreValueSelect(model);
505
+ if (preValueSelect && Object.keys(preValueSelect).length > 0) {
506
+ preValue = yield db[model].findFirst({ where: uniqueFilter, select: preValueSelect });
507
+ }
508
+ postWriteChecks.push({ model, operation: 'postUpdate', uniqueFilter, preValue });
164
509
  }
165
- return dbOps.update(writeArgs);
166
510
  });
167
- const ids = this.utils.getEntityIds(this.model, result);
168
- if (Object.keys(ids).length === 0) {
169
- throw this.utils.unknownError(`unexpected error: update didn't return an id`);
511
+ // We can't let the native "update" to handle nested "create" because we can't get back what
512
+ // was created for doing post-update checks.
513
+ // Instead, handle nested create inside update as an atomic operation that creates an entire
514
+ // subtree (containing nested creates/connects)
515
+ const _create = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
516
+ var _a;
517
+ let createData = args;
518
+ if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
519
+ // handles the connection to upstream entity
520
+ const reversedQuery = yield this.utils.buildReversedQuery(context);
521
+ if (reversedQuery[context.field.backLink]) {
522
+ // the built reverse query contains a condition for the backlink field, build a "connect" with it
523
+ createData = Object.assign(Object.assign({}, createData), { [context.field.backLink]: {
524
+ connect: reversedQuery[context.field.backLink],
525
+ } });
526
+ }
527
+ else {
528
+ // otherwise, the reverse query is translated to foreign key setting, merge it to the create data
529
+ createData = Object.assign(Object.assign({}, createData), reversedQuery);
530
+ }
531
+ }
532
+ // proceed with the create and collect post-create checks
533
+ const { postWriteChecks: checks } = yield this.doCreate(model, { data: createData }, db);
534
+ postWriteChecks.push(...checks);
535
+ });
536
+ const _createMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
537
+ var _b;
538
+ if ((_b = context.field) === null || _b === void 0 ? void 0 : _b.backLink) {
539
+ // handles the connection to upstream entity
540
+ const reversedQuery = yield this.utils.buildReversedQuery(context);
541
+ for (const item of (0, utils_1.enumerate)(args.data)) {
542
+ Object.assign(item, reversedQuery);
543
+ }
544
+ }
545
+ // proceed with the create and collect post-create checks
546
+ const { postWriteChecks: checks } = yield this.doCreateMany(model, args, db);
547
+ postWriteChecks.push(...checks);
548
+ });
549
+ const _connectDisconnect = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
550
+ var _c;
551
+ if ((_c = context.field) === null || _c === void 0 ? void 0 : _c.backLink) {
552
+ const backLinkField = this.utils.getModelField(model, context.field.backLink);
553
+ if (backLinkField.isRelationOwner) {
554
+ // update happens on the related model, require updatable
555
+ yield this.utils.checkPolicyForUnique(model, args, 'update', db, args);
556
+ // register post-update check
557
+ yield _registerPostUpdateCheck(model, args);
558
+ }
559
+ }
560
+ });
561
+ // visit nested writes
562
+ const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
563
+ update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
564
+ var _d;
565
+ // build a unique query including upstream conditions
566
+ const uniqueFilter = yield this.utils.buildReversedQuery(context);
567
+ // handle not-found
568
+ const existing = yield this.utils.checkExistence(db, model, uniqueFilter, true);
569
+ // check if the update actually writes to this model
570
+ let thisModelUpdate = false;
571
+ const updatePayload = (_d = args.data) !== null && _d !== void 0 ? _d : args;
572
+ if (updatePayload) {
573
+ for (const key of Object.keys(updatePayload)) {
574
+ const field = (0, model_meta_1.resolveField)(this.modelMeta, model, key);
575
+ if (field) {
576
+ if (!field.isDataModel) {
577
+ // scalar field, require this model to be updatable
578
+ thisModelUpdate = true;
579
+ break;
580
+ }
581
+ else if (field.isRelationOwner) {
582
+ // relation is being updated and this model owns foreign key, require updatable
583
+ thisModelUpdate = true;
584
+ break;
585
+ }
586
+ }
587
+ }
588
+ }
589
+ if (thisModelUpdate) {
590
+ this.utils.tryReject(db, this.model, 'update');
591
+ // check pre-update guard
592
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args);
593
+ // handles the case where id fields are updated
594
+ const ids = this.utils.clone(existing);
595
+ for (const key of Object.keys(existing)) {
596
+ const updateValue = args.data ? args.data[key] : args[key];
597
+ if (typeof updateValue === 'string' ||
598
+ typeof updateValue === 'number' ||
599
+ typeof updateValue === 'bigint') {
600
+ ids[key] = updateValue;
601
+ }
602
+ }
603
+ // register post-update check
604
+ yield _registerPostUpdateCheck(model, ids);
605
+ }
606
+ }),
607
+ updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
608
+ // injects auth guard into where clause
609
+ yield this.utils.injectAuthGuard(db, args, model, 'update');
610
+ // prepare for post-update check
611
+ if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) {
612
+ let select = this.utils.makeIdSelection(model);
613
+ const preValueSelect = this.utils.getPreValueSelect(model);
614
+ if (preValueSelect) {
615
+ select = Object.assign(Object.assign({}, select), preValueSelect);
616
+ }
617
+ const reversedQuery = yield this.utils.buildReversedQuery(context);
618
+ const currentSetQuery = { select, where: reversedQuery };
619
+ yield this.utils.injectAuthGuard(db, currentSetQuery, model, 'read');
620
+ if (this.shouldLogQuery) {
621
+ this.logger.info(`[policy] \`findMany\` ${model}:\n${(0, utils_1.formatObject)(currentSetQuery)}`);
622
+ }
623
+ const currentSet = yield db[model].findMany(currentSetQuery);
624
+ postWriteChecks.push(...currentSet.map((preValue) => ({
625
+ model,
626
+ operation: 'postUpdate',
627
+ uniqueFilter: preValue,
628
+ preValue: preValueSelect ? preValue : undefined,
629
+ })));
630
+ }
631
+ }),
632
+ create: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
633
+ // process the entire create subtree separately
634
+ yield _create(model, args, context);
635
+ // remove it from the update payload
636
+ delete context.parent.create;
637
+ // don't visit payload
638
+ return false;
639
+ }),
640
+ createMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
641
+ // process createMany separately
642
+ yield _createMany(model, args, context);
643
+ // remove it from the update payload
644
+ delete context.parent.createMany;
645
+ // don't visit payload
646
+ return false;
647
+ }),
648
+ upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
649
+ // build a unique query including upstream conditions
650
+ const uniqueFilter = yield this.utils.buildReversedQuery(context);
651
+ // branch based on if the update target exists
652
+ const existing = yield this.utils.checkExistence(db, model, uniqueFilter);
653
+ if (existing) {
654
+ // update case
655
+ // check pre-update guard
656
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args);
657
+ // register post-update check
658
+ yield _registerPostUpdateCheck(model, uniqueFilter);
659
+ // convert upsert to update
660
+ context.parent.update = { where: args.where, data: args.update };
661
+ delete context.parent.upsert;
662
+ // continue visiting the new payload
663
+ return context.parent.update;
664
+ }
665
+ else {
666
+ // create case
667
+ // process the entire create subtree separately
668
+ yield _create(model, args.create, context);
669
+ // remove it from the update payload
670
+ delete context.parent.upsert;
671
+ // don't visit payload
672
+ return false;
673
+ }
674
+ }),
675
+ connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () { return _connectDisconnect(model, args, context); }),
676
+ connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
677
+ // the where condition is already unique, so we can use it to check if the target exists
678
+ const existing = yield this.utils.checkExistence(db, model, args.where);
679
+ if (existing) {
680
+ // connect
681
+ yield _connectDisconnect(model, args.where, context);
682
+ }
683
+ else {
684
+ // create
685
+ yield _create(model, args.create, context);
686
+ }
687
+ }),
688
+ disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () { return _connectDisconnect(model, args, context); }),
689
+ set: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
690
+ // find the set of items to be replaced
691
+ const reversedQuery = yield this.utils.buildReversedQuery(context);
692
+ const findCurrSetArgs = {
693
+ select: this.utils.makeIdSelection(model),
694
+ where: reversedQuery,
695
+ };
696
+ if (this.shouldLogQuery) {
697
+ this.logger.info(`[policy] \`findMany\` ${model}:\n${(0, utils_1.formatObject)(findCurrSetArgs)}`);
698
+ }
699
+ const currentSet = yield db[model].findMany(findCurrSetArgs);
700
+ // register current set for update (foreign key)
701
+ yield Promise.all(currentSet.map((item) => _connectDisconnect(model, item, context)));
702
+ // proceed with connecting the new set
703
+ yield Promise.all((0, utils_1.enumerate)(args).map((item) => _connectDisconnect(model, item, context)));
704
+ }),
705
+ delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
706
+ // build a unique query including upstream conditions
707
+ const uniqueFilter = yield this.utils.buildReversedQuery(context);
708
+ // handle not-found
709
+ yield this.utils.checkExistence(db, model, uniqueFilter, true);
710
+ // check delete guard
711
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, args);
712
+ }),
713
+ deleteMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
714
+ // inject delete guard
715
+ const guard = yield this.utils.getAuthGuard(db, model, 'delete');
716
+ context.parent.deleteMany = this.utils.and(args, guard);
717
+ }),
718
+ });
719
+ yield visitor.visit(this.model, 'update', args);
720
+ // finally proceed with the update
721
+ if (this.shouldLogQuery) {
722
+ this.logger.info(`[policy] \`update\` ${this.model}: ${(0, utils_1.formatObject)(args)}`);
170
723
  }
171
- return this.checkReadback(origArgs, ids, 'update', 'update');
724
+ const result = yield db[this.model].update({
725
+ where: args.where,
726
+ data: args.data,
727
+ select: this.utils.makeIdSelection(this.model),
728
+ });
729
+ return { result, postWriteChecks };
172
730
  });
173
731
  }
174
732
  updateMany(args) {
@@ -179,17 +737,45 @@ class PolicyProxyHandler {
179
737
  if (!args.data) {
180
738
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
181
739
  }
182
- yield this.tryReject('update');
740
+ yield this.utils.tryReject(this.prisma, this.model, 'update');
183
741
  args = this.utils.clone(args);
184
- // use a transaction to wrap the write so it can be reverted if any nested
185
- // create fails access policies
186
- const result = yield this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => {
742
+ yield this.utils.injectAuthGuard(this.prisma, args, this.model, 'update');
743
+ if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) {
744
+ // use a transaction to do post-update checks
745
+ const postWriteChecks = [];
746
+ return this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
747
+ // collect pre-update values
748
+ let select = this.utils.makeIdSelection(this.model);
749
+ const preValueSelect = this.utils.getPreValueSelect(this.model);
750
+ if (preValueSelect) {
751
+ select = Object.assign(Object.assign({}, select), preValueSelect);
752
+ }
753
+ const currentSetQuery = { select, where: args.where };
754
+ yield this.utils.injectAuthGuard(tx, currentSetQuery, this.model, 'read');
755
+ if (this.shouldLogQuery) {
756
+ this.logger.info(`[policy] \`findMany\` ${this.model}: ${(0, utils_1.formatObject)(currentSetQuery)}`);
757
+ }
758
+ const currentSet = yield tx[this.model].findMany(currentSetQuery);
759
+ postWriteChecks.push(...currentSet.map((preValue) => ({
760
+ model: this.model,
761
+ operation: 'postUpdate',
762
+ uniqueFilter: this.utils.getEntityIds(this.model, preValue),
763
+ preValue: preValueSelect ? preValue : undefined,
764
+ })));
765
+ // proceed with the update
766
+ const result = yield tx[this.model].updateMany(args);
767
+ // run post-write checks
768
+ yield this.runPostWriteChecks(postWriteChecks, tx);
769
+ return result;
770
+ }));
771
+ }
772
+ else {
773
+ // proceed without a transaction
187
774
  if (this.shouldLogQuery) {
188
- this.logger.info(`[withPolicy] \`updateMany\`: ${(0, utils_1.formatObject)(writeArgs)}`);
775
+ this.logger.info(`[policy] \`updateMany\` ${this.model}: ${(0, utils_1.formatObject)(args)}`);
189
776
  }
190
- return dbOps.updateMany(writeArgs);
191
- });
192
- return result;
777
+ return this.modelClient.updateMany(args);
778
+ }
193
779
  });
194
780
  }
195
781
  upsert(args) {
@@ -206,25 +792,39 @@ class PolicyProxyHandler {
206
792
  if (!args.update) {
207
793
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'update field is required in query argument');
208
794
  }
209
- const origArgs = args;
795
+ yield this.utils.tryReject(this.prisma, this.model, 'create');
796
+ yield this.utils.tryReject(this.prisma, this.model, 'update');
210
797
  args = this.utils.clone(args);
211
- yield this.tryReject('create');
212
- yield this.tryReject('update');
213
- // use a transaction to wrap the write so it can be reverted if any nested
214
- // create fails access policies
215
- const result = yield this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => {
216
- if (this.shouldLogQuery) {
217
- this.logger.info(`[withPolicy] \`upsert\`: ${(0, utils_1.formatObject)(writeArgs)}`);
798
+ // We can call the native "upsert" because we can't tell if an entity was created or updated
799
+ // for doing post-write check accordingly. Instead, decompose it into create or update.
800
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
801
+ const { where, create, update } = args, rest = __rest(args, ["where", "create", "update"]);
802
+ const existing = yield this.utils.checkExistence(tx, this.model, args.where);
803
+ if (existing) {
804
+ // update case
805
+ const { result, postWriteChecks } = yield this.doUpdate(Object.assign({ where, data: update }, rest), tx);
806
+ yield this.runPostWriteChecks(postWriteChecks, tx);
807
+ return this.utils.readBack(tx, this.model, 'update', args, result);
218
808
  }
219
- return dbOps.upsert(writeArgs);
220
- });
221
- const ids = this.utils.getEntityIds(this.model, result);
222
- if (Object.keys(ids).length === 0) {
223
- throw this.utils.unknownError(`unexpected error: upsert didn't return an id`);
809
+ else {
810
+ // create case
811
+ const { result, postWriteChecks } = yield this.doCreate(this.model, Object.assign({ data: create }, rest), tx);
812
+ yield this.runPostWriteChecks(postWriteChecks, tx);
813
+ return this.utils.readBack(tx, this.model, 'create', args, result);
814
+ }
815
+ }));
816
+ if (error) {
817
+ throw error;
818
+ }
819
+ else {
820
+ return result;
224
821
  }
225
- return this.checkReadback(origArgs, ids, 'upsert', 'update');
226
822
  });
227
823
  }
824
+ //#endregion
825
+ //#region Delete
826
+ // "delete" works against a single entity, and is rejected if the entity fails policy check.
827
+ // "deleteMany" works against a set of entities, entities that fail policy check are filtered out.
228
828
  delete(args) {
229
829
  return __awaiter(this, void 0, void 0, function* () {
230
830
  if (!args) {
@@ -233,55 +833,56 @@ class PolicyProxyHandler {
233
833
  if (!args.where) {
234
834
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'where field is required in query argument');
235
835
  }
236
- yield this.tryReject('delete');
237
- // ensures the item under deletion passes policy check
238
- yield this.utils.checkPolicyForFilter(this.model, args.where, 'delete', this.prisma);
239
- // read the entity under deletion with respect to read policies
240
- let readResult;
241
- try {
242
- const items = yield this.utils.readWithCheck(this.model, args);
243
- readResult = items[0];
244
- }
245
- catch (err) {
246
- // not readable
247
- readResult = undefined;
248
- }
249
- // conduct the deletion
250
- if (this.shouldLogQuery) {
251
- this.logger.info(`[withPolicy] \`delete\`:\n${(0, utils_1.formatObject)(args)}`);
252
- }
253
- yield this.modelClient.delete(args);
254
- if (!readResult) {
255
- throw this.utils.deniedByPolicy(this.model, 'delete', 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
836
+ yield this.utils.tryReject(this.prisma, this.model, 'delete');
837
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
838
+ // do a read-back before delete
839
+ const r = yield this.utils.readBack(tx, this.model, 'delete', args, args.where);
840
+ const error = r.error;
841
+ const read = r.result;
842
+ // check existence
843
+ yield this.utils.checkExistence(tx, this.model, args.where, true);
844
+ // inject delete guard
845
+ yield this.utils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args);
846
+ // proceed with the deletion
847
+ if (this.shouldLogQuery) {
848
+ this.logger.info(`[policy] \`delete\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
849
+ }
850
+ yield tx[this.model].delete(args);
851
+ return { result: read, error };
852
+ }));
853
+ if (error) {
854
+ throw error;
256
855
  }
257
856
  else {
258
- return readResult;
857
+ return result;
259
858
  }
260
859
  });
261
860
  }
262
861
  deleteMany(args) {
263
862
  return __awaiter(this, void 0, void 0, function* () {
264
- yield this.tryReject('delete');
863
+ yield this.utils.tryReject(this.prisma, this.model, 'delete');
265
864
  // inject policy conditions
266
865
  args = args !== null && args !== void 0 ? args : {};
267
- yield this.utils.injectAuthGuard(args, this.model, 'delete');
866
+ yield this.utils.injectAuthGuard(this.prisma, args, this.model, 'delete');
268
867
  // conduct the deletion
269
868
  if (this.shouldLogQuery) {
270
- this.logger.info(`[withPolicy] \`deleteMany\`:\n${(0, utils_1.formatObject)(args)}`);
869
+ this.logger.info(`[policy] \`deleteMany\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
271
870
  }
272
871
  return this.modelClient.deleteMany(args);
273
872
  });
274
873
  }
874
+ //#endregion
875
+ //#region Aggregation
275
876
  aggregate(args) {
276
877
  return __awaiter(this, void 0, void 0, function* () {
277
878
  if (!args) {
278
879
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
279
880
  }
280
- yield this.tryReject('read');
881
+ args = this.utils.clone(args);
281
882
  // inject policy conditions
282
- yield this.utils.injectAuthGuard(args, this.model, 'read');
883
+ yield this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
283
884
  if (this.shouldLogQuery) {
284
- this.logger.info(`[withPolicy] \`aggregate\`:\n${(0, utils_1.formatObject)(args)}`);
885
+ this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
285
886
  }
286
887
  return this.modelClient.aggregate(args);
287
888
  });
@@ -291,51 +892,88 @@ class PolicyProxyHandler {
291
892
  if (!args) {
292
893
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
293
894
  }
294
- yield this.tryReject('read');
895
+ args = this.utils.clone(args);
295
896
  // inject policy conditions
296
- yield this.utils.injectAuthGuard(args, this.model, 'read');
897
+ yield this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
297
898
  if (this.shouldLogQuery) {
298
- this.logger.info(`[withPolicy] \`groupBy\`:\n${(0, utils_1.formatObject)(args)}`);
899
+ this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
299
900
  }
300
901
  return this.modelClient.groupBy(args);
301
902
  });
302
903
  }
303
904
  count(args) {
304
905
  return __awaiter(this, void 0, void 0, function* () {
305
- yield this.tryReject('read');
306
906
  // inject policy conditions
307
- args = args !== null && args !== void 0 ? args : {};
308
- yield this.utils.injectAuthGuard(args, this.model, 'read');
907
+ args = args ? this.utils.clone(args) : {};
908
+ yield this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
309
909
  if (this.shouldLogQuery) {
310
- this.logger.info(`[withPolicy] \`count\`:\n${(0, utils_1.formatObject)(args)}`);
910
+ this.logger.info(`[policy] \`count\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
311
911
  }
312
912
  return this.modelClient.count(args);
313
913
  });
314
914
  }
315
- tryReject(operation) {
915
+ //#endregion
916
+ //#region Subscribe (Prisma Pulse)
917
+ subscribe(args) {
316
918
  return __awaiter(this, void 0, void 0, function* () {
317
- const guard = yield this.utils.getAuthGuard(this.model, operation);
318
- if (guard === false) {
319
- throw this.utils.deniedByPolicy(this.model, operation);
919
+ const readGuard = this.utils.getAuthGuard(this.prisma, this.model, 'read');
920
+ if (this.utils.isTrue(readGuard)) {
921
+ // no need to inject
922
+ if (this.shouldLogQuery) {
923
+ this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
924
+ }
925
+ return this.modelClient.subscribe(args);
320
926
  }
321
- });
322
- }
323
- checkReadback(origArgs, ids, action, operation) {
324
- return __awaiter(this, void 0, void 0, function* () {
325
- const readArgs = { select: origArgs.select, include: origArgs.include, where: ids };
326
- const result = yield this.utils.readWithCheck(this.model, readArgs);
327
- if (result.length === 0) {
328
- this.logger.info(`${action} result cannot be read back`);
329
- throw this.utils.deniedByPolicy(this.model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
927
+ if (!args) {
928
+ // include all
929
+ args = { create: {}, update: {}, delete: {} };
930
+ }
931
+ else {
932
+ if (typeof args !== 'object') {
933
+ throw (0, utils_1.prismaClientValidationError)(this.prisma, 'argument must be an object');
934
+ }
935
+ if (Object.keys(args).length === 0) {
936
+ // include all
937
+ args = { create: {}, update: {}, delete: {} };
938
+ }
939
+ else {
940
+ args = this.utils.clone(args);
941
+ }
942
+ }
943
+ // inject into subscribe conditions
944
+ if (args.create) {
945
+ args.create.after = this.utils.and(args.create.after, readGuard);
330
946
  }
331
- else if (result.length > 1) {
332
- throw this.utils.unknownError('write unexpected resulted in multiple readback entities');
947
+ if (args.update) {
948
+ args.update.after = this.utils.and(args.update.after, readGuard);
333
949
  }
334
- return result[0];
950
+ if (args.delete) {
951
+ args.delete.before = this.utils.and(args.delete.before, readGuard);
952
+ }
953
+ if (this.shouldLogQuery) {
954
+ this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
955
+ }
956
+ return this.modelClient.subscribe(args);
335
957
  });
336
958
  }
959
+ //#endregion
960
+ //#region Utils
337
961
  get shouldLogQuery() {
338
- return this.logPrismaQuery && this.logger.enabled('info');
962
+ return !!this.logPrismaQuery && this.logger.enabled('info');
963
+ }
964
+ transaction(action) {
965
+ if (this.prisma[constants_1.PRISMA_TX_FLAG]) {
966
+ // already in transaction, don't nest
967
+ return action(this.prisma);
968
+ }
969
+ else {
970
+ return this.prisma.$transaction((tx) => action(tx));
971
+ }
972
+ }
973
+ runPostWriteChecks(postWriteChecks, db) {
974
+ return __awaiter(this, void 0, void 0, function* () {
975
+ yield Promise.all(postWriteChecks.map(({ model, operation, uniqueFilter, preValue }) => __awaiter(this, void 0, void 0, function* () { return this.utils.checkPolicyForUnique(model, uniqueFilter, operation, db, undefined, preValue); })));
976
+ });
339
977
  }
340
978
  }
341
979
  exports.PolicyProxyHandler = PolicyProxyHandler;