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