@zenstackhq/runtime 1.0.0-beta.12 → 1.0.0-beta.15

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