@zenstackhq/runtime 1.0.0-beta.13 → 1.0.0-beta.16
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.
- package/constants.d.ts +21 -2
- package/constants.js +21 -2
- package/constants.js.map +1 -1
- package/enhancements/model-data-visitor.d.ts +16 -0
- package/enhancements/model-data-visitor.js +41 -0
- package/enhancements/model-data-visitor.js.map +1 -0
- package/enhancements/model-meta.d.ts +3 -2
- package/enhancements/model-meta.js.map +1 -1
- package/enhancements/nested-write-vistor.d.ts +18 -14
- package/enhancements/nested-write-vistor.js +67 -28
- package/enhancements/nested-write-vistor.js.map +1 -1
- package/enhancements/omit.d.ts +1 -1
- package/enhancements/policy/handler.d.ts +21 -13
- package/enhancements/policy/handler.js +711 -129
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/index.js +9 -0
- package/enhancements/policy/index.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +83 -49
- package/enhancements/policy/policy-utils.js +389 -588
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/proxy.js +5 -5
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +6 -0
- package/enhancements/utils.js +4 -1
- package/enhancements/utils.js.map +1 -1
- package/error.js +9 -3
- package/error.js.map +1 -1
- package/package.json +1 -2
- package/types.d.ts +6 -2
- package/types.js +1 -0
- package/types.js.map +1 -1
|
@@ -9,9 +9,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
13
|
+
var t = {};
|
|
14
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
15
|
+
t[p] = s[p];
|
|
16
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
17
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
18
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
19
|
+
t[p[i]] = s[p[i]];
|
|
20
|
+
}
|
|
21
|
+
return t;
|
|
22
|
+
};
|
|
12
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
24
|
exports.PolicyProxyHandler = void 0;
|
|
25
|
+
const upper_case_first_1 = require("upper-case-first");
|
|
26
|
+
const zod_validation_error_1 = require("zod-validation-error");
|
|
14
27
|
const constants_1 = require("../../constants");
|
|
28
|
+
const model_data_visitor_1 = require("../model-data-visitor");
|
|
29
|
+
const model_meta_1 = require("../model-meta");
|
|
30
|
+
const nested_write_vistor_1 = require("../nested-write-vistor");
|
|
15
31
|
const utils_1 = require("../utils");
|
|
16
32
|
const logger_1 = require("./logger");
|
|
17
33
|
const policy_utils_1 = require("./policy-utils");
|
|
@@ -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.
|
|
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
|
-
|
|
46
|
-
if (
|
|
62
|
+
args = this.utils.clone(args);
|
|
63
|
+
if (!(yield this.utils.injectForRead(this.model, args))) {
|
|
47
64
|
return null;
|
|
48
65
|
}
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
if (
|
|
96
|
+
args = args ? this.utils.clone(args) : {};
|
|
97
|
+
if (!(yield this.utils.injectForRead(this.model, args))) {
|
|
71
98
|
return null;
|
|
72
99
|
}
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
80
|
-
if (
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
if (
|
|
124
|
+
args = args ? this.utils.clone(args) : {};
|
|
125
|
+
if (!(yield this.utils.injectForRead(this.model, args))) {
|
|
94
126
|
return [];
|
|
95
127
|
}
|
|
96
|
-
|
|
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
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
373
|
+
throw (0, utils_1.prismaClientValidationError)(this.prisma, 'data field is required in query argument');
|
|
132
374
|
}
|
|
133
|
-
|
|
375
|
+
this.utils.tryReject(this.model, 'create');
|
|
134
376
|
args = this.utils.clone(args);
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
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(`[
|
|
433
|
+
this.logger.info(`[policy] \`create\` ${model}: ${(0, utils_1.formatObject)(item)}`);
|
|
140
434
|
}
|
|
141
|
-
return
|
|
142
|
-
});
|
|
143
|
-
|
|
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.
|
|
158
|
-
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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(`[
|
|
765
|
+
this.logger.info(`[policy] \`updateMany\` ${this.model}: ${(0, utils_1.formatObject)(args)}`);
|
|
190
766
|
}
|
|
191
|
-
return
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
916
|
+
runPostWriteChecks(postWriteChecks, db) {
|
|
323
917
|
return __awaiter(this, void 0, void 0, function* () {
|
|
324
|
-
|
|
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
|