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