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

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