@zenstackhq/runtime 1.0.0-beta.9 → 1.0.1

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 (77) hide show
  1. package/browser/index.js +3 -2
  2. package/browser/index.js.map +1 -1
  3. package/browser/index.mjs +1 -0
  4. package/browser/index.mjs.map +1 -1
  5. package/constants.d.ts +57 -14
  6. package/constants.js +57 -14
  7. package/constants.js.map +1 -1
  8. package/enhancements/enhance.d.ts +18 -0
  9. package/enhancements/enhance.js +42 -0
  10. package/enhancements/enhance.js.map +1 -0
  11. package/enhancements/index.d.ts +2 -1
  12. package/enhancements/index.js +2 -1
  13. package/enhancements/index.js.map +1 -1
  14. package/enhancements/model-data-visitor.d.ts +16 -0
  15. package/enhancements/model-data-visitor.js +41 -0
  16. package/enhancements/model-data-visitor.js.map +1 -0
  17. package/enhancements/model-meta.d.ts +3 -6
  18. package/enhancements/model-meta.js +3 -28
  19. package/enhancements/model-meta.js.map +1 -1
  20. package/enhancements/{nested-write-vistor.d.ts → nested-write-visitor.d.ts} +18 -14
  21. package/enhancements/{nested-write-vistor.js → nested-write-visitor.js} +68 -29
  22. package/enhancements/nested-write-visitor.js.map +1 -0
  23. package/enhancements/omit.d.ts +4 -4
  24. package/enhancements/omit.js +2 -1
  25. package/enhancements/omit.js.map +1 -1
  26. package/enhancements/password.d.ts +4 -4
  27. package/enhancements/password.js +4 -4
  28. package/enhancements/password.js.map +1 -1
  29. package/enhancements/policy/handler.d.ts +34 -18
  30. package/enhancements/policy/handler.js +844 -157
  31. package/enhancements/policy/handler.js.map +1 -1
  32. package/enhancements/policy/index.d.ts +4 -4
  33. package/enhancements/policy/index.js +20 -38
  34. package/enhancements/policy/index.js.map +1 -1
  35. package/enhancements/policy/logger.js +1 -1
  36. package/enhancements/policy/logger.js.map +1 -1
  37. package/enhancements/policy/policy-utils.d.ts +106 -48
  38. package/enhancements/policy/policy-utils.js +778 -671
  39. package/enhancements/policy/policy-utils.js.map +1 -1
  40. package/enhancements/policy/promise.d.ts +5 -0
  41. package/enhancements/policy/promise.js +42 -0
  42. package/enhancements/policy/promise.js.map +1 -0
  43. package/enhancements/preset.d.ts +3 -8
  44. package/enhancements/preset.js +2 -4
  45. package/enhancements/preset.js.map +1 -1
  46. package/enhancements/proxy.d.ts +3 -1
  47. package/enhancements/proxy.js +45 -28
  48. package/enhancements/proxy.js.map +1 -1
  49. package/enhancements/types.d.ts +24 -7
  50. package/enhancements/types.js +1 -0
  51. package/enhancements/types.js.map +1 -1
  52. package/enhancements/utils.d.ts +5 -1
  53. package/enhancements/utils.js +36 -8
  54. package/enhancements/utils.js.map +1 -1
  55. package/error.js +9 -3
  56. package/error.js.map +1 -1
  57. package/index.d.ts +2 -0
  58. package/index.js +2 -0
  59. package/index.js.map +1 -1
  60. package/loader.d.ts +22 -0
  61. package/loader.js +99 -0
  62. package/loader.js.map +1 -0
  63. package/package.json +9 -2
  64. package/types.d.ts +28 -14
  65. package/types.js +2 -0
  66. package/types.js.map +1 -1
  67. package/validation.d.ts +5 -0
  68. package/validation.js +13 -1
  69. package/validation.js.map +1 -1
  70. package/version.d.ts +5 -0
  71. package/version.js +34 -1
  72. package/version.js.map +1 -1
  73. package/zod/index.d.ts +1 -0
  74. package/zod/index.js +1 -0
  75. package/zod/objects.d.ts +1 -0
  76. package/zod/objects.js +8 -0
  77. package/enhancements/nested-write-vistor.js.map +0 -1
@@ -9,12 +9,30 @@ 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_visitor_1 = require("../nested-write-visitor");
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
  */
@@ -24,78 +42,126 @@ class PolicyProxyHandler {
24
42
  this.policy = policy;
25
43
  this.modelMeta = modelMeta;
26
44
  this.zodSchemas = zodSchemas;
27
- this.model = model;
28
45
  this.user = user;
29
46
  this.logPrismaQuery = logPrismaQuery;
30
47
  this.logger = new logger_1.Logger(prisma);
31
- this.utils = new policy_utils_1.PolicyUtil(this.prisma, this.modelMeta, this.policy, this.zodSchemas, 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);
32
50
  }
33
51
  get modelClient() {
34
52
  return this.prisma[this.model];
35
53
  }
54
+ //#region Find
55
+ // find operations behaves as if the entities that don't match access policies don't exist
36
56
  findUnique(args) {
37
- var _a;
38
- return __awaiter(this, void 0, void 0, function* () {
39
- if (!args) {
40
- throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
41
- }
42
- if (!args.where) {
43
- throw (0, utils_1.prismaClientValidationError)(this.prisma, 'where field is required in query argument');
44
- }
45
- const guard = this.utils.getAuthGuard(this.model, 'read');
46
- if (guard === false) {
47
- return null;
48
- }
49
- const entities = yield this.utils.readWithCheck(this.model, args);
50
- return (_a = entities[0]) !== null && _a !== void 0 ? _a : null;
51
- });
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);
52
64
  }
53
65
  findUniqueOrThrow(args) {
54
- return __awaiter(this, void 0, void 0, function* () {
55
- const guard = this.utils.getAuthGuard(this.model, 'read');
56
- if (guard === false) {
57
- throw this.utils.notFound(this.model);
58
- }
59
- const entity = yield this.findUnique(args);
60
- if (!entity) {
61
- throw this.utils.notFound(this.model);
62
- }
63
- 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);
64
74
  });
65
75
  }
66
76
  findFirst(args) {
67
- var _a;
68
- return __awaiter(this, void 0, void 0, function* () {
69
- const guard = this.utils.getAuthGuard(this.model, 'read');
70
- if (guard === false) {
71
- return null;
72
- }
73
- const entities = yield this.utils.readWithCheck(this.model, args);
74
- return (_a = entities[0]) !== null && _a !== void 0 ? _a : null;
75
- });
77
+ return this.findWithFluentCallStubs(args, 'findFirst', false, () => null);
76
78
  }
77
79
  findFirstOrThrow(args) {
78
- return __awaiter(this, void 0, void 0, function* () {
79
- const guard = this.utils.getAuthGuard(this.model, 'read');
80
- if (guard === false) {
81
- throw this.utils.notFound(this.model);
82
- }
83
- const entity = yield this.findFirst(args);
84
- if (!entity) {
85
- throw this.utils.notFound(this.model);
86
- }
87
- return entity;
80
+ return this.findWithFluentCallStubs(args, 'findFirstOrThrow', true, () => {
81
+ throw this.utils.notFound(this.model);
88
82
  });
89
83
  }
90
84
  findMany(args) {
91
- return __awaiter(this, void 0, void 0, function* () {
92
- const guard = this.utils.getAuthGuard(this.model, 'read');
93
- if (guard === false) {
94
- return [];
95
- }
96
- 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));
97
109
  });
98
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
99
165
  create(args) {
100
166
  return __awaiter(this, void 0, void 0, function* () {
101
167
  if (!args) {
@@ -104,45 +170,316 @@ class PolicyProxyHandler {
104
170
  if (!args.data) {
105
171
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
106
172
  }
107
- yield this.tryReject('create');
173
+ this.utils.tryReject(this.prisma, this.model, 'create');
108
174
  const origArgs = args;
109
175
  args = this.utils.clone(args);
110
- // use a transaction to wrap the write so it can be reverted if the created
111
- // entity fails access policies
112
- const result = yield this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => {
113
- if (this.shouldLogQuery) {
114
- 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', undefined, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
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);
207
+ }
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_visitor_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
+ }),
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 });
115
338
  }
116
- return dbOps.create(writeArgs);
117
339
  });
340
+ // return only the ids of the top-level entity
118
341
  const ids = this.utils.getEntityIds(this.model, result);
119
- if (Object.keys(ids).length === 0) {
120
- throw this.utils.unknownError(`unexpected error: create didn't return an id`);
121
- }
122
- return this.checkReadback(origArgs, ids, 'create', 'create');
342
+ return { result: ids, postWriteChecks: [...postCreateChecks.values()] };
123
343
  });
124
344
  }
125
- createMany(args, skipDuplicates) {
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_visitor_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;
382
+ });
383
+ }
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, parseResult.error);
391
+ }
392
+ }
393
+ }
394
+ createMany(args) {
126
395
  return __awaiter(this, void 0, void 0, function* () {
127
396
  if (!args) {
128
397
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
129
398
  }
130
399
  if (!args.data) {
131
- 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');
132
401
  }
133
- yield this.tryReject('create');
402
+ this.utils.tryReject(this.prisma, this.model, 'create');
134
403
  args = this.utils.clone(args);
135
- // use a transaction to wrap the write so it can be reverted if any created
136
- // entity fails access policies
137
- 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', undefined, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
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
+ }
138
459
  if (this.shouldLogQuery) {
139
- this.logger.info(`[withPolicy] \`createMany\`: ${(0, utils_1.formatObject)(writeArgs)}`);
460
+ this.logger.info(`[policy] \`create\` ${model}: ${(0, utils_1.formatObject)(item)}`);
140
461
  }
141
- return dbOps.createMany(writeArgs, skipDuplicates);
142
- });
143
- 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
+ };
144
474
  });
145
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
146
483
  update(args) {
147
484
  return __awaiter(this, void 0, void 0, function* () {
148
485
  if (!args) {
@@ -154,24 +491,282 @@ class PolicyProxyHandler {
154
491
  if (!args.data) {
155
492
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
156
493
  }
157
- yield this.tryReject('update');
158
- const origArgs = args;
159
494
  args = this.utils.clone(args);
160
- // use a transaction to wrap the write so it can be reverted if any nested
161
- // create fails access policies
162
- const result = yield this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => {
163
- if (this.shouldLogQuery) {
164
- 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 });
165
526
  }
166
- return dbOps.update(writeArgs);
167
527
  });
168
- const ids = this.utils.getEntityIds(this.model, result);
169
- if (Object.keys(ids).length === 0) {
170
- 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
+ // Check if the create payload contains any "unsafe" assignment:
537
+ // assign id or foreign key fields.
538
+ //
539
+ // The reason why we need to do that is Prisma's mutations payload
540
+ // structure has two mutually exclusive forms for safe and unsafe
541
+ // operations. E.g.:
542
+ // - safe: { data: { user: { connect: { id: 1 }} } }
543
+ // - unsafe: { data: { userId: 1 } }
544
+ const unsafe = this.isUnsafeMutate(model, args);
545
+ // handles the connection to upstream entity
546
+ const reversedQuery = this.utils.buildReversedQuery(context, true, unsafe);
547
+ if (reversedQuery[context.field.backLink]) {
548
+ // the built reverse query contains a condition for the backlink field, build a "connect" with it
549
+ createData = Object.assign(Object.assign({}, createData), { [context.field.backLink]: {
550
+ connect: reversedQuery[context.field.backLink],
551
+ } });
552
+ }
553
+ else {
554
+ // otherwise, the reverse query is translated to foreign key setting, merge it to the create data
555
+ createData = Object.assign(Object.assign({}, createData), reversedQuery);
556
+ }
557
+ }
558
+ // proceed with the create and collect post-create checks
559
+ const { postWriteChecks: checks } = yield this.doCreate(model, { data: createData }, db);
560
+ postWriteChecks.push(...checks);
561
+ });
562
+ const _createMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
563
+ var _b;
564
+ if ((_b = context.field) === null || _b === void 0 ? void 0 : _b.backLink) {
565
+ // handles the connection to upstream entity
566
+ const reversedQuery = this.utils.buildReversedQuery(context);
567
+ for (const item of (0, utils_1.enumerate)(args.data)) {
568
+ Object.assign(item, reversedQuery);
569
+ }
570
+ }
571
+ // proceed with the create and collect post-create checks
572
+ const { postWriteChecks: checks } = yield this.doCreateMany(model, args, db);
573
+ postWriteChecks.push(...checks);
574
+ });
575
+ const _connectDisconnect = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
576
+ var _c;
577
+ if ((_c = context.field) === null || _c === void 0 ? void 0 : _c.backLink) {
578
+ const backLinkField = this.utils.getModelField(model, context.field.backLink);
579
+ if (backLinkField.isRelationOwner) {
580
+ // update happens on the related model, require updatable
581
+ yield this.utils.checkPolicyForUnique(model, args, 'update', db, args);
582
+ // register post-update check
583
+ yield _registerPostUpdateCheck(model, args);
584
+ }
585
+ }
586
+ });
587
+ // visit nested writes
588
+ const visitor = new nested_write_visitor_1.NestedWriteVisitor(this.modelMeta, {
589
+ update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
590
+ var _d;
591
+ // build a unique query including upstream conditions
592
+ const uniqueFilter = this.utils.buildReversedQuery(context);
593
+ // handle not-found
594
+ const existing = yield this.utils.checkExistence(db, model, uniqueFilter, true);
595
+ // check if the update actually writes to this model
596
+ let thisModelUpdate = false;
597
+ const updatePayload = (_d = args.data) !== null && _d !== void 0 ? _d : args;
598
+ if (updatePayload) {
599
+ for (const key of Object.keys(updatePayload)) {
600
+ const field = (0, model_meta_1.resolveField)(this.modelMeta, model, key);
601
+ if (field) {
602
+ if (!field.isDataModel) {
603
+ // scalar field, require this model to be updatable
604
+ thisModelUpdate = true;
605
+ break;
606
+ }
607
+ else if (field.isRelationOwner) {
608
+ // relation is being updated and this model owns foreign key, require updatable
609
+ thisModelUpdate = true;
610
+ break;
611
+ }
612
+ }
613
+ }
614
+ }
615
+ if (thisModelUpdate) {
616
+ this.utils.tryReject(db, this.model, 'update');
617
+ // check pre-update guard
618
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args);
619
+ // handles the case where id fields are updated
620
+ const ids = this.utils.clone(existing);
621
+ for (const key of Object.keys(existing)) {
622
+ const updateValue = args.data ? args.data[key] : args[key];
623
+ if (typeof updateValue === 'string' ||
624
+ typeof updateValue === 'number' ||
625
+ typeof updateValue === 'bigint') {
626
+ ids[key] = updateValue;
627
+ }
628
+ }
629
+ // register post-update check
630
+ yield _registerPostUpdateCheck(model, ids);
631
+ }
632
+ }),
633
+ updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
634
+ // injects auth guard into where clause
635
+ this.utils.injectAuthGuard(db, args, model, 'update');
636
+ // prepare for post-update check
637
+ if (this.utils.hasAuthGuard(model, 'postUpdate') || this.utils.getZodSchema(model)) {
638
+ let select = this.utils.makeIdSelection(model);
639
+ const preValueSelect = this.utils.getPreValueSelect(model);
640
+ if (preValueSelect) {
641
+ select = Object.assign(Object.assign({}, select), preValueSelect);
642
+ }
643
+ const reversedQuery = this.utils.buildReversedQuery(context);
644
+ const currentSetQuery = { select, where: reversedQuery };
645
+ this.utils.injectAuthGuard(db, currentSetQuery, model, 'read');
646
+ if (this.shouldLogQuery) {
647
+ this.logger.info(`[policy] \`findMany\` ${model}:\n${(0, utils_1.formatObject)(currentSetQuery)}`);
648
+ }
649
+ const currentSet = yield db[model].findMany(currentSetQuery);
650
+ postWriteChecks.push(...currentSet.map((preValue) => ({
651
+ model,
652
+ operation: 'postUpdate',
653
+ uniqueFilter: preValue,
654
+ preValue: preValueSelect ? preValue : undefined,
655
+ })));
656
+ }
657
+ }),
658
+ create: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
659
+ // process the entire create subtree separately
660
+ yield _create(model, args, context);
661
+ // remove it from the update payload
662
+ delete context.parent.create;
663
+ // don't visit payload
664
+ return false;
665
+ }),
666
+ createMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
667
+ // process createMany separately
668
+ yield _createMany(model, args, context);
669
+ // remove it from the update payload
670
+ delete context.parent.createMany;
671
+ // don't visit payload
672
+ return false;
673
+ }),
674
+ upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
675
+ // build a unique query including upstream conditions
676
+ const uniqueFilter = this.utils.buildReversedQuery(context);
677
+ // branch based on if the update target exists
678
+ const existing = yield this.utils.checkExistence(db, model, uniqueFilter);
679
+ if (existing) {
680
+ // update case
681
+ // check pre-update guard
682
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'update', db, args);
683
+ // register post-update check
684
+ yield _registerPostUpdateCheck(model, uniqueFilter);
685
+ // convert upsert to update
686
+ context.parent.update = { where: args.where, data: args.update };
687
+ delete context.parent.upsert;
688
+ // continue visiting the new payload
689
+ return context.parent.update;
690
+ }
691
+ else {
692
+ // create case
693
+ // process the entire create subtree separately
694
+ yield _create(model, args.create, context);
695
+ // remove it from the update payload
696
+ delete context.parent.upsert;
697
+ // don't visit payload
698
+ return false;
699
+ }
700
+ }),
701
+ connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () { return _connectDisconnect(model, args, context); }),
702
+ connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
703
+ // the where condition is already unique, so we can use it to check if the target exists
704
+ const existing = yield this.utils.checkExistence(db, model, args.where);
705
+ if (existing) {
706
+ // connect
707
+ yield _connectDisconnect(model, args.where, context);
708
+ }
709
+ else {
710
+ // create
711
+ yield _create(model, args.create, context);
712
+ }
713
+ }),
714
+ disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () { return _connectDisconnect(model, args, context); }),
715
+ set: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
716
+ // find the set of items to be replaced
717
+ const reversedQuery = this.utils.buildReversedQuery(context);
718
+ const findCurrSetArgs = {
719
+ select: this.utils.makeIdSelection(model),
720
+ where: reversedQuery,
721
+ };
722
+ if (this.shouldLogQuery) {
723
+ this.logger.info(`[policy] \`findMany\` ${model}:\n${(0, utils_1.formatObject)(findCurrSetArgs)}`);
724
+ }
725
+ const currentSet = yield db[model].findMany(findCurrSetArgs);
726
+ // register current set for update (foreign key)
727
+ yield Promise.all(currentSet.map((item) => _connectDisconnect(model, item, context)));
728
+ // proceed with connecting the new set
729
+ yield Promise.all((0, utils_1.enumerate)(args).map((item) => _connectDisconnect(model, item, context)));
730
+ }),
731
+ delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
732
+ // build a unique query including upstream conditions
733
+ const uniqueFilter = this.utils.buildReversedQuery(context);
734
+ // handle not-found
735
+ yield this.utils.checkExistence(db, model, uniqueFilter, true);
736
+ // check delete guard
737
+ yield this.utils.checkPolicyForUnique(model, uniqueFilter, 'delete', db, args);
738
+ }),
739
+ deleteMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
740
+ // inject delete guard
741
+ const guard = yield this.utils.getAuthGuard(db, model, 'delete');
742
+ context.parent.deleteMany = this.utils.and(args, guard);
743
+ }),
744
+ });
745
+ yield visitor.visit(this.model, 'update', args);
746
+ // finally proceed with the update
747
+ if (this.shouldLogQuery) {
748
+ this.logger.info(`[policy] \`update\` ${this.model}: ${(0, utils_1.formatObject)(args)}`);
171
749
  }
172
- return this.checkReadback(origArgs, ids, 'update', 'update');
750
+ const result = yield db[this.model].update({
751
+ where: args.where,
752
+ data: args.data,
753
+ select: this.utils.makeIdSelection(this.model),
754
+ });
755
+ return { result, postWriteChecks };
173
756
  });
174
757
  }
758
+ isUnsafeMutate(model, args) {
759
+ if (!args) {
760
+ return false;
761
+ }
762
+ for (const k of Object.keys(args)) {
763
+ const field = (0, model_meta_1.resolveField)(this.modelMeta, model, k);
764
+ if ((field === null || field === void 0 ? void 0 : field.isId) || (field === null || field === void 0 ? void 0 : field.isForeignKey)) {
765
+ return true;
766
+ }
767
+ }
768
+ return false;
769
+ }
175
770
  updateMany(args) {
176
771
  return __awaiter(this, void 0, void 0, function* () {
177
772
  if (!args) {
@@ -180,17 +775,45 @@ class PolicyProxyHandler {
180
775
  if (!args.data) {
181
776
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
182
777
  }
183
- yield this.tryReject('update');
778
+ this.utils.tryReject(this.prisma, this.model, 'update');
184
779
  args = this.utils.clone(args);
185
- // use a transaction to wrap the write so it can be reverted if any nested
186
- // create fails access policies
187
- const result = yield this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => {
780
+ this.utils.injectAuthGuard(this.prisma, args, this.model, 'update');
781
+ if (this.utils.hasAuthGuard(this.model, 'postUpdate') || this.utils.getZodSchema(this.model)) {
782
+ // use a transaction to do post-update checks
783
+ const postWriteChecks = [];
784
+ return this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
785
+ // collect pre-update values
786
+ let select = this.utils.makeIdSelection(this.model);
787
+ const preValueSelect = this.utils.getPreValueSelect(this.model);
788
+ if (preValueSelect) {
789
+ select = Object.assign(Object.assign({}, select), preValueSelect);
790
+ }
791
+ const currentSetQuery = { select, where: args.where };
792
+ this.utils.injectAuthGuard(tx, currentSetQuery, this.model, 'read');
793
+ if (this.shouldLogQuery) {
794
+ this.logger.info(`[policy] \`findMany\` ${this.model}: ${(0, utils_1.formatObject)(currentSetQuery)}`);
795
+ }
796
+ const currentSet = yield tx[this.model].findMany(currentSetQuery);
797
+ postWriteChecks.push(...currentSet.map((preValue) => ({
798
+ model: this.model,
799
+ operation: 'postUpdate',
800
+ uniqueFilter: this.utils.getEntityIds(this.model, preValue),
801
+ preValue: preValueSelect ? preValue : undefined,
802
+ })));
803
+ // proceed with the update
804
+ const result = yield tx[this.model].updateMany(args);
805
+ // run post-write checks
806
+ yield this.runPostWriteChecks(postWriteChecks, tx);
807
+ return result;
808
+ }));
809
+ }
810
+ else {
811
+ // proceed without a transaction
188
812
  if (this.shouldLogQuery) {
189
- this.logger.info(`[withPolicy] \`updateMany\`: ${(0, utils_1.formatObject)(writeArgs)}`);
813
+ this.logger.info(`[policy] \`updateMany\` ${this.model}: ${(0, utils_1.formatObject)(args)}`);
190
814
  }
191
- return dbOps.updateMany(writeArgs);
192
- });
193
- return result;
815
+ return this.modelClient.updateMany(args);
816
+ }
194
817
  });
195
818
  }
196
819
  upsert(args) {
@@ -207,25 +830,39 @@ class PolicyProxyHandler {
207
830
  if (!args.update) {
208
831
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'update field is required in query argument');
209
832
  }
210
- const origArgs = args;
833
+ this.utils.tryReject(this.prisma, this.model, 'create');
834
+ this.utils.tryReject(this.prisma, this.model, 'update');
211
835
  args = this.utils.clone(args);
212
- yield this.tryReject('create');
213
- yield this.tryReject('update');
214
- // use a transaction to wrap the write so it can be reverted if any nested
215
- // create fails access policies
216
- const result = yield this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => {
217
- if (this.shouldLogQuery) {
218
- this.logger.info(`[withPolicy] \`upsert\`: ${(0, utils_1.formatObject)(writeArgs)}`);
836
+ // We can call the native "upsert" because we can't tell if an entity was created or updated
837
+ // for doing post-write check accordingly. Instead, decompose it into create or update.
838
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
839
+ const { where, create, update } = args, rest = __rest(args, ["where", "create", "update"]);
840
+ const existing = yield this.utils.checkExistence(tx, this.model, args.where);
841
+ if (existing) {
842
+ // update case
843
+ const { result, postWriteChecks } = yield this.doUpdate(Object.assign({ where, data: update }, rest), tx);
844
+ yield this.runPostWriteChecks(postWriteChecks, tx);
845
+ return this.utils.readBack(tx, this.model, 'update', args, result);
219
846
  }
220
- return dbOps.upsert(writeArgs);
221
- });
222
- const ids = this.utils.getEntityIds(this.model, result);
223
- if (Object.keys(ids).length === 0) {
224
- throw this.utils.unknownError(`unexpected error: upsert didn't return an id`);
847
+ else {
848
+ // create case
849
+ const { result, postWriteChecks } = yield this.doCreate(this.model, Object.assign({ data: create }, rest), tx);
850
+ yield this.runPostWriteChecks(postWriteChecks, tx);
851
+ return this.utils.readBack(tx, this.model, 'create', args, result);
852
+ }
853
+ }));
854
+ if (error) {
855
+ throw error;
856
+ }
857
+ else {
858
+ return result;
225
859
  }
226
- return this.checkReadback(origArgs, ids, 'upsert', 'update');
227
860
  });
228
861
  }
862
+ //#endregion
863
+ //#region Delete
864
+ // "delete" works against a single entity, and is rejected if the entity fails policy check.
865
+ // "deleteMany" works against a set of entities, entities that fail policy check are filtered out.
229
866
  delete(args) {
230
867
  return __awaiter(this, void 0, void 0, function* () {
231
868
  if (!args) {
@@ -234,55 +871,56 @@ class PolicyProxyHandler {
234
871
  if (!args.where) {
235
872
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'where field is required in query argument');
236
873
  }
237
- yield this.tryReject('delete');
238
- // ensures the item under deletion passes policy check
239
- yield this.utils.checkPolicyForFilter(this.model, args.where, 'delete', this.prisma);
240
- // read the entity under deletion with respect to read policies
241
- let readResult;
242
- try {
243
- const items = yield this.utils.readWithCheck(this.model, args);
244
- readResult = items[0];
245
- }
246
- catch (err) {
247
- // not readable
248
- readResult = undefined;
249
- }
250
- // conduct the deletion
251
- if (this.shouldLogQuery) {
252
- this.logger.info(`[withPolicy] \`delete\`:\n${(0, utils_1.formatObject)(args)}`);
253
- }
254
- yield this.modelClient.delete(args);
255
- if (!readResult) {
256
- throw this.utils.deniedByPolicy(this.model, 'delete', 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
874
+ this.utils.tryReject(this.prisma, this.model, 'delete');
875
+ const { result, error } = yield this.transaction((tx) => __awaiter(this, void 0, void 0, function* () {
876
+ // do a read-back before delete
877
+ const r = yield this.utils.readBack(tx, this.model, 'delete', args, args.where);
878
+ const error = r.error;
879
+ const read = r.result;
880
+ // check existence
881
+ yield this.utils.checkExistence(tx, this.model, args.where, true);
882
+ // inject delete guard
883
+ yield this.utils.checkPolicyForUnique(this.model, args.where, 'delete', tx, args);
884
+ // proceed with the deletion
885
+ if (this.shouldLogQuery) {
886
+ this.logger.info(`[policy] \`delete\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
887
+ }
888
+ yield tx[this.model].delete(args);
889
+ return { result: read, error };
890
+ }));
891
+ if (error) {
892
+ throw error;
257
893
  }
258
894
  else {
259
- return readResult;
895
+ return result;
260
896
  }
261
897
  });
262
898
  }
263
899
  deleteMany(args) {
264
900
  return __awaiter(this, void 0, void 0, function* () {
265
- yield this.tryReject('delete');
901
+ this.utils.tryReject(this.prisma, this.model, 'delete');
266
902
  // inject policy conditions
267
903
  args = args !== null && args !== void 0 ? args : {};
268
- yield this.utils.injectAuthGuard(args, this.model, 'delete');
904
+ this.utils.injectAuthGuard(this.prisma, args, this.model, 'delete');
269
905
  // conduct the deletion
270
906
  if (this.shouldLogQuery) {
271
- this.logger.info(`[withPolicy] \`deleteMany\`:\n${(0, utils_1.formatObject)(args)}`);
907
+ this.logger.info(`[policy] \`deleteMany\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
272
908
  }
273
909
  return this.modelClient.deleteMany(args);
274
910
  });
275
911
  }
912
+ //#endregion
913
+ //#region Aggregation
276
914
  aggregate(args) {
277
915
  return __awaiter(this, void 0, void 0, function* () {
278
916
  if (!args) {
279
917
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
280
918
  }
281
- yield this.tryReject('read');
919
+ args = this.utils.clone(args);
282
920
  // inject policy conditions
283
- yield this.utils.injectAuthGuard(args, this.model, 'read');
921
+ this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
284
922
  if (this.shouldLogQuery) {
285
- this.logger.info(`[withPolicy] \`aggregate\`:\n${(0, utils_1.formatObject)(args)}`);
923
+ this.logger.info(`[policy] \`aggregate\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
286
924
  }
287
925
  return this.modelClient.aggregate(args);
288
926
  });
@@ -292,49 +930,98 @@ class PolicyProxyHandler {
292
930
  if (!args) {
293
931
  throw (0, utils_1.prismaClientValidationError)(this.prisma, 'query argument is required');
294
932
  }
295
- yield this.tryReject('read');
933
+ args = this.utils.clone(args);
296
934
  // inject policy conditions
297
- yield this.utils.injectAuthGuard(args, this.model, 'read');
935
+ this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
298
936
  if (this.shouldLogQuery) {
299
- this.logger.info(`[withPolicy] \`groupBy\`:\n${(0, utils_1.formatObject)(args)}`);
937
+ this.logger.info(`[policy] \`groupBy\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
300
938
  }
301
939
  return this.modelClient.groupBy(args);
302
940
  });
303
941
  }
304
942
  count(args) {
305
943
  return __awaiter(this, void 0, void 0, function* () {
306
- yield this.tryReject('read');
307
944
  // inject policy conditions
308
- args = args !== null && args !== void 0 ? args : {};
309
- yield this.utils.injectAuthGuard(args, this.model, 'read');
945
+ args = args ? this.utils.clone(args) : {};
946
+ this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
310
947
  if (this.shouldLogQuery) {
311
- this.logger.info(`[withPolicy] \`count\`:\n${(0, utils_1.formatObject)(args)}`);
948
+ this.logger.info(`[policy] \`count\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
312
949
  }
313
950
  return this.modelClient.count(args);
314
951
  });
315
952
  }
316
- tryReject(operation) {
317
- const guard = this.utils.getAuthGuard(this.model, operation);
318
- if (guard === false) {
319
- throw this.utils.deniedByPolicy(this.model, operation);
320
- }
321
- }
322
- checkReadback(origArgs, ids, action, operation) {
953
+ //#endregion
954
+ //#region Subscribe (Prisma Pulse)
955
+ subscribe(args) {
323
956
  return __awaiter(this, void 0, void 0, function* () {
324
- const readArgs = { select: origArgs.select, include: origArgs.include, where: ids };
325
- const result = yield this.utils.readWithCheck(this.model, readArgs);
326
- if (result.length === 0) {
327
- this.logger.info(`${action} result cannot be read back`);
328
- throw this.utils.deniedByPolicy(this.model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
957
+ const readGuard = this.utils.getAuthGuard(this.prisma, this.model, 'read');
958
+ if (this.utils.isTrue(readGuard)) {
959
+ // no need to inject
960
+ if (this.shouldLogQuery) {
961
+ this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
962
+ }
963
+ return this.modelClient.subscribe(args);
329
964
  }
330
- else if (result.length > 1) {
331
- throw this.utils.unknownError('write unexpected resulted in multiple readback entities');
965
+ if (!args) {
966
+ // include all
967
+ args = { create: {}, update: {}, delete: {} };
332
968
  }
333
- return result[0];
969
+ else {
970
+ if (typeof args !== 'object') {
971
+ throw (0, utils_1.prismaClientValidationError)(this.prisma, 'argument must be an object');
972
+ }
973
+ if (Object.keys(args).length === 0) {
974
+ // include all
975
+ args = { create: {}, update: {}, delete: {} };
976
+ }
977
+ else {
978
+ args = this.utils.clone(args);
979
+ }
980
+ }
981
+ // inject into subscribe conditions
982
+ if (args.create) {
983
+ args.create.after = this.utils.and(args.create.after, readGuard);
984
+ }
985
+ if (args.update) {
986
+ args.update.after = this.utils.and(args.update.after, readGuard);
987
+ }
988
+ if (args.delete) {
989
+ args.delete.before = this.utils.and(args.delete.before, readGuard);
990
+ }
991
+ if (this.shouldLogQuery) {
992
+ this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${(0, utils_1.formatObject)(args)}`);
993
+ }
994
+ return this.modelClient.subscribe(args);
334
995
  });
335
996
  }
997
+ //#endregion
998
+ //#region Utils
336
999
  get shouldLogQuery() {
337
- return this.logPrismaQuery && this.logger.enabled('info');
1000
+ return !!this.logPrismaQuery && this.logger.enabled('info');
1001
+ }
1002
+ transaction(action) {
1003
+ if (this.prisma[constants_1.PRISMA_TX_FLAG]) {
1004
+ // already in transaction, don't nest
1005
+ return action(this.prisma);
1006
+ }
1007
+ else {
1008
+ return this.prisma.$transaction((tx) => action(tx));
1009
+ }
1010
+ }
1011
+ runPostWriteChecks(postWriteChecks, db) {
1012
+ return __awaiter(this, void 0, void 0, function* () {
1013
+ 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); })));
1014
+ });
1015
+ }
1016
+ makeHandler(model) {
1017
+ return new PolicyProxyHandler(this.prisma, this.policy, this.modelMeta, this.zodSchemas, model, this.user, this.logPrismaQuery);
1018
+ }
1019
+ requireBackLink(fieldInfo) {
1020
+ const backLinkField = fieldInfo.backLink && (0, model_meta_1.resolveField)(this.modelMeta, fieldInfo.type, fieldInfo.backLink);
1021
+ if (!backLinkField) {
1022
+ throw new Error('Missing back link for field: ' + fieldInfo.name);
1023
+ }
1024
+ return backLinkField;
338
1025
  }
339
1026
  }
340
1027
  exports.PolicyProxyHandler = PolicyProxyHandler;