@zenstackhq/plugin-policy 3.3.1 → 3.3.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +1798 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +6 -6
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/functions.ts
|
|
5
|
+
import { invariant as invariant4 } from "@zenstackhq/common-helpers";
|
|
6
|
+
import { CRUD, QueryUtils as QueryUtils3 } from "@zenstackhq/orm";
|
|
7
|
+
import { ExpressionWrapper as ExpressionWrapper2, ValueNode as ValueNode4 } from "kysely";
|
|
8
|
+
|
|
9
|
+
// src/policy-handler.ts
|
|
10
|
+
import { invariant as invariant3 } from "@zenstackhq/common-helpers";
|
|
11
|
+
import { getCrudDialect as getCrudDialect2, QueryUtils as QueryUtils2, RejectedByPolicyReason, SchemaUtils as SchemaUtils2 } from "@zenstackhq/orm";
|
|
12
|
+
import { ExpressionUtils as ExpressionUtils4 } from "@zenstackhq/orm/schema";
|
|
13
|
+
import { AliasNode as AliasNode3, BinaryOperationNode as BinaryOperationNode3, ColumnNode as ColumnNode3, DeleteQueryNode, expressionBuilder as expressionBuilder2, ExpressionWrapper, FromNode as FromNode2, FunctionNode as FunctionNode3, IdentifierNode as IdentifierNode2, InsertQueryNode, OperationNodeTransformer, OperatorNode as OperatorNode3, ParensNode as ParensNode2, PrimitiveValueListNode, RawNode, ReferenceNode as ReferenceNode3, ReturningNode, SelectAllNode, SelectionNode as SelectionNode2, SelectQueryNode as SelectQueryNode2, sql, TableNode as TableNode3, UpdateQueryNode, ValueListNode as ValueListNode2, ValueNode as ValueNode3, ValuesNode, WhereNode as WhereNode2 } from "kysely";
|
|
14
|
+
import { match as match3 } from "ts-pattern";
|
|
15
|
+
|
|
16
|
+
// src/column-collector.ts
|
|
17
|
+
import { KyselyUtils } from "@zenstackhq/orm";
|
|
18
|
+
var ColumnCollector = class extends KyselyUtils.DefaultOperationNodeVisitor {
|
|
19
|
+
static {
|
|
20
|
+
__name(this, "ColumnCollector");
|
|
21
|
+
}
|
|
22
|
+
columns = [];
|
|
23
|
+
collect(node) {
|
|
24
|
+
this.columns = [];
|
|
25
|
+
this.visitNode(node);
|
|
26
|
+
return this.columns;
|
|
27
|
+
}
|
|
28
|
+
visitColumn(node) {
|
|
29
|
+
if (!this.columns.includes(node.column.name)) {
|
|
30
|
+
this.columns.push(node.column.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/expression-transformer.ts
|
|
36
|
+
import { invariant as invariant2 } from "@zenstackhq/common-helpers";
|
|
37
|
+
import { getCrudDialect, QueryUtils, SchemaUtils } from "@zenstackhq/orm";
|
|
38
|
+
import { ExpressionUtils as ExpressionUtils3 } from "@zenstackhq/orm/schema";
|
|
39
|
+
import { AliasNode as AliasNode2, BinaryOperationNode as BinaryOperationNode2, ColumnNode as ColumnNode2, expressionBuilder, FromNode, FunctionNode as FunctionNode2, IdentifierNode, OperatorNode as OperatorNode2, ReferenceNode as ReferenceNode2, SelectionNode, SelectQueryNode, TableNode as TableNode2, ValueListNode, ValueNode as ValueNode2, WhereNode } from "kysely";
|
|
40
|
+
import { match as match2 } from "ts-pattern";
|
|
41
|
+
|
|
42
|
+
// src/expression-evaluator.ts
|
|
43
|
+
import { invariant } from "@zenstackhq/common-helpers";
|
|
44
|
+
import { match } from "ts-pattern";
|
|
45
|
+
import { ExpressionUtils } from "@zenstackhq/orm/schema";
|
|
46
|
+
var ExpressionEvaluator = class {
|
|
47
|
+
static {
|
|
48
|
+
__name(this, "ExpressionEvaluator");
|
|
49
|
+
}
|
|
50
|
+
evaluate(expression, context) {
|
|
51
|
+
const result = match(expression).when(ExpressionUtils.isArray, (expr2) => this.evaluateArray(expr2, context)).when(ExpressionUtils.isBinary, (expr2) => this.evaluateBinary(expr2, context)).when(ExpressionUtils.isField, (expr2) => this.evaluateField(expr2, context)).when(ExpressionUtils.isLiteral, (expr2) => this.evaluateLiteral(expr2)).when(ExpressionUtils.isMember, (expr2) => this.evaluateMember(expr2, context)).when(ExpressionUtils.isUnary, (expr2) => this.evaluateUnary(expr2, context)).when(ExpressionUtils.isCall, (expr2) => this.evaluateCall(expr2, context)).when(ExpressionUtils.isThis, () => context.thisValue).when(ExpressionUtils.isNull, () => null).exhaustive();
|
|
52
|
+
return result ?? null;
|
|
53
|
+
}
|
|
54
|
+
evaluateCall(expr2, context) {
|
|
55
|
+
if (expr2.function === "auth") {
|
|
56
|
+
return context.auth;
|
|
57
|
+
} else {
|
|
58
|
+
throw new Error(`Unsupported call expression function: ${expr2.function}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
evaluateUnary(expr2, context) {
|
|
62
|
+
return match(expr2.op).with("!", () => !this.evaluate(expr2.operand, context)).exhaustive();
|
|
63
|
+
}
|
|
64
|
+
evaluateMember(expr2, context) {
|
|
65
|
+
let val = this.evaluate(expr2.receiver, context);
|
|
66
|
+
for (const member of expr2.members) {
|
|
67
|
+
val = val?.[member];
|
|
68
|
+
}
|
|
69
|
+
return val;
|
|
70
|
+
}
|
|
71
|
+
evaluateLiteral(expr2) {
|
|
72
|
+
return expr2.value;
|
|
73
|
+
}
|
|
74
|
+
evaluateField(expr2, context) {
|
|
75
|
+
return context.thisValue?.[expr2.field];
|
|
76
|
+
}
|
|
77
|
+
evaluateArray(expr2, context) {
|
|
78
|
+
return expr2.items.map((item) => this.evaluate(item, context));
|
|
79
|
+
}
|
|
80
|
+
evaluateBinary(expr2, context) {
|
|
81
|
+
if (expr2.op === "?" || expr2.op === "!" || expr2.op === "^") {
|
|
82
|
+
return this.evaluateCollectionPredicate(expr2, context);
|
|
83
|
+
}
|
|
84
|
+
const left = this.evaluate(expr2.left, context);
|
|
85
|
+
const right = this.evaluate(expr2.right, context);
|
|
86
|
+
if (![
|
|
87
|
+
"==",
|
|
88
|
+
"!="
|
|
89
|
+
].includes(expr2.op) && (left === null || right === null)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return match(expr2.op).with("==", () => left === right).with("!=", () => left !== right).with(">", () => left > right).with(">=", () => left >= right).with("<", () => left < right).with("<=", () => left <= right).with("&&", () => left && right).with("||", () => left || right).with("in", () => {
|
|
93
|
+
const _right = right ?? [];
|
|
94
|
+
invariant(Array.isArray(_right), 'expected array for "in" operator');
|
|
95
|
+
return _right.includes(left);
|
|
96
|
+
}).exhaustive();
|
|
97
|
+
}
|
|
98
|
+
evaluateCollectionPredicate(expr2, context) {
|
|
99
|
+
const op = expr2.op;
|
|
100
|
+
invariant(op === "?" || op === "!" || op === "^", 'expected "?" or "!" or "^" operator');
|
|
101
|
+
const left = this.evaluate(expr2.left, context);
|
|
102
|
+
if (left === null || left === void 0) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
invariant(Array.isArray(left), "expected array");
|
|
106
|
+
return match(op).with("?", () => left.some((item) => this.evaluate(expr2.right, {
|
|
107
|
+
...context,
|
|
108
|
+
thisValue: item
|
|
109
|
+
}))).with("!", () => left.every((item) => this.evaluate(expr2.right, {
|
|
110
|
+
...context,
|
|
111
|
+
thisValue: item
|
|
112
|
+
}))).with("^", () => !left.some((item) => this.evaluate(expr2.right, {
|
|
113
|
+
...context,
|
|
114
|
+
thisValue: item
|
|
115
|
+
}))).exhaustive();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/types.ts
|
|
120
|
+
var CollectionPredicateOperator = [
|
|
121
|
+
"?",
|
|
122
|
+
"!",
|
|
123
|
+
"^"
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// src/utils.ts
|
|
127
|
+
import { ORMError, ORMErrorReason } from "@zenstackhq/orm";
|
|
128
|
+
import { ExpressionUtils as ExpressionUtils2 } from "@zenstackhq/orm/schema";
|
|
129
|
+
import { AliasNode, AndNode, BinaryOperationNode, ColumnNode, FunctionNode, OperatorNode, OrNode, ParensNode, ReferenceNode, TableNode, UnaryOperationNode, ValueNode } from "kysely";
|
|
130
|
+
function trueNode(dialect) {
|
|
131
|
+
return ValueNode.createImmediate(dialect.transformPrimitive(true, "Boolean", false));
|
|
132
|
+
}
|
|
133
|
+
__name(trueNode, "trueNode");
|
|
134
|
+
function falseNode(dialect) {
|
|
135
|
+
return ValueNode.createImmediate(dialect.transformPrimitive(false, "Boolean", false));
|
|
136
|
+
}
|
|
137
|
+
__name(falseNode, "falseNode");
|
|
138
|
+
function isTrueNode(node) {
|
|
139
|
+
return ValueNode.is(node) && (node.value === true || node.value === 1);
|
|
140
|
+
}
|
|
141
|
+
__name(isTrueNode, "isTrueNode");
|
|
142
|
+
function isFalseNode(node) {
|
|
143
|
+
return ValueNode.is(node) && (node.value === false || node.value === 0);
|
|
144
|
+
}
|
|
145
|
+
__name(isFalseNode, "isFalseNode");
|
|
146
|
+
function conjunction(dialect, nodes) {
|
|
147
|
+
if (nodes.length === 0) {
|
|
148
|
+
return trueNode(dialect);
|
|
149
|
+
}
|
|
150
|
+
if (nodes.length === 1) {
|
|
151
|
+
return nodes[0];
|
|
152
|
+
}
|
|
153
|
+
if (nodes.some(isFalseNode)) {
|
|
154
|
+
return falseNode(dialect);
|
|
155
|
+
}
|
|
156
|
+
const items = nodes.filter((n) => !isTrueNode(n));
|
|
157
|
+
if (items.length === 0) {
|
|
158
|
+
return trueNode(dialect);
|
|
159
|
+
}
|
|
160
|
+
return items.reduce((acc, node) => AndNode.create(wrapParensIf(acc, OrNode.is), wrapParensIf(node, OrNode.is)));
|
|
161
|
+
}
|
|
162
|
+
__name(conjunction, "conjunction");
|
|
163
|
+
function disjunction(dialect, nodes) {
|
|
164
|
+
if (nodes.length === 0) {
|
|
165
|
+
return falseNode(dialect);
|
|
166
|
+
}
|
|
167
|
+
if (nodes.length === 1) {
|
|
168
|
+
return nodes[0];
|
|
169
|
+
}
|
|
170
|
+
if (nodes.some(isTrueNode)) {
|
|
171
|
+
return trueNode(dialect);
|
|
172
|
+
}
|
|
173
|
+
const items = nodes.filter((n) => !isFalseNode(n));
|
|
174
|
+
if (items.length === 0) {
|
|
175
|
+
return falseNode(dialect);
|
|
176
|
+
}
|
|
177
|
+
return items.reduce((acc, node) => OrNode.create(wrapParensIf(acc, AndNode.is), wrapParensIf(node, AndNode.is)));
|
|
178
|
+
}
|
|
179
|
+
__name(disjunction, "disjunction");
|
|
180
|
+
function logicalNot(dialect, node) {
|
|
181
|
+
if (isTrueNode(node)) {
|
|
182
|
+
return falseNode(dialect);
|
|
183
|
+
}
|
|
184
|
+
if (isFalseNode(node)) {
|
|
185
|
+
return trueNode(dialect);
|
|
186
|
+
}
|
|
187
|
+
return UnaryOperationNode.create(OperatorNode.create("not"), wrapParensIf(node, (n) => AndNode.is(n) || OrNode.is(n)));
|
|
188
|
+
}
|
|
189
|
+
__name(logicalNot, "logicalNot");
|
|
190
|
+
function wrapParensIf(node, predicate) {
|
|
191
|
+
return predicate(node) ? ParensNode.create(node) : node;
|
|
192
|
+
}
|
|
193
|
+
__name(wrapParensIf, "wrapParensIf");
|
|
194
|
+
function buildIsFalse(node, dialect) {
|
|
195
|
+
if (isFalseNode(node)) {
|
|
196
|
+
return trueNode(dialect);
|
|
197
|
+
} else if (isTrueNode(node)) {
|
|
198
|
+
return falseNode(dialect);
|
|
199
|
+
}
|
|
200
|
+
return BinaryOperationNode.create(
|
|
201
|
+
// coalesce so null is treated as false
|
|
202
|
+
FunctionNode.create("coalesce", [
|
|
203
|
+
node,
|
|
204
|
+
falseNode(dialect)
|
|
205
|
+
]),
|
|
206
|
+
OperatorNode.create("="),
|
|
207
|
+
falseNode(dialect)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
__name(buildIsFalse, "buildIsFalse");
|
|
211
|
+
function getTableName(node) {
|
|
212
|
+
if (!node) {
|
|
213
|
+
return node;
|
|
214
|
+
}
|
|
215
|
+
if (TableNode.is(node)) {
|
|
216
|
+
return node.table.identifier.name;
|
|
217
|
+
} else if (AliasNode.is(node)) {
|
|
218
|
+
return getTableName(node.node);
|
|
219
|
+
} else if (ReferenceNode.is(node) && node.table) {
|
|
220
|
+
return getTableName(node.table);
|
|
221
|
+
}
|
|
222
|
+
return void 0;
|
|
223
|
+
}
|
|
224
|
+
__name(getTableName, "getTableName");
|
|
225
|
+
function isBeforeInvocation(expr2) {
|
|
226
|
+
return ExpressionUtils2.isCall(expr2) && expr2.function === "before";
|
|
227
|
+
}
|
|
228
|
+
__name(isBeforeInvocation, "isBeforeInvocation");
|
|
229
|
+
function createRejectedByPolicyError(model, reason, message) {
|
|
230
|
+
const err = new ORMError(ORMErrorReason.REJECTED_BY_POLICY, message ?? "operation is rejected by access policies");
|
|
231
|
+
err.rejectedByPolicyReason = reason;
|
|
232
|
+
err.model = model;
|
|
233
|
+
return err;
|
|
234
|
+
}
|
|
235
|
+
__name(createRejectedByPolicyError, "createRejectedByPolicyError");
|
|
236
|
+
function createUnsupportedError(message) {
|
|
237
|
+
return new ORMError(ORMErrorReason.NOT_SUPPORTED, message);
|
|
238
|
+
}
|
|
239
|
+
__name(createUnsupportedError, "createUnsupportedError");
|
|
240
|
+
|
|
241
|
+
// src/expression-transformer.ts
|
|
242
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
243
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
244
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
245
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
246
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
247
|
+
}
|
|
248
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
249
|
+
function _ts_metadata(k, v) {
|
|
250
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
251
|
+
}
|
|
252
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
253
|
+
var expressionHandlers = /* @__PURE__ */ new Map();
|
|
254
|
+
function expr(kind) {
|
|
255
|
+
return function(_target, _propertyKey, descriptor) {
|
|
256
|
+
if (!expressionHandlers.get(kind)) {
|
|
257
|
+
expressionHandlers.set(kind, descriptor);
|
|
258
|
+
}
|
|
259
|
+
return descriptor;
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
__name(expr, "expr");
|
|
263
|
+
var ExpressionTransformer = class {
|
|
264
|
+
static {
|
|
265
|
+
__name(this, "ExpressionTransformer");
|
|
266
|
+
}
|
|
267
|
+
client;
|
|
268
|
+
dialect;
|
|
269
|
+
constructor(client) {
|
|
270
|
+
this.client = client;
|
|
271
|
+
this.dialect = getCrudDialect(this.schema, this.clientOptions);
|
|
272
|
+
}
|
|
273
|
+
get schema() {
|
|
274
|
+
return this.client.$schema;
|
|
275
|
+
}
|
|
276
|
+
get clientOptions() {
|
|
277
|
+
return this.client.$options;
|
|
278
|
+
}
|
|
279
|
+
get auth() {
|
|
280
|
+
return this.client.$auth;
|
|
281
|
+
}
|
|
282
|
+
get authType() {
|
|
283
|
+
if (!this.schema.authType) {
|
|
284
|
+
invariant2(false, 'Schema does not have an "authType" specified');
|
|
285
|
+
}
|
|
286
|
+
return this.schema.authType;
|
|
287
|
+
}
|
|
288
|
+
transform(expression, context) {
|
|
289
|
+
const handler = expressionHandlers.get(expression.kind);
|
|
290
|
+
if (!handler) {
|
|
291
|
+
throw new Error(`Unsupported expression kind: ${expression.kind}`);
|
|
292
|
+
}
|
|
293
|
+
return handler.value.call(this, expression, context);
|
|
294
|
+
}
|
|
295
|
+
_literal(expr2) {
|
|
296
|
+
return this.transformValue(expr2.value, typeof expr2.value === "string" ? "String" : typeof expr2.value === "boolean" ? "Boolean" : "Int");
|
|
297
|
+
}
|
|
298
|
+
_array(expr2, context) {
|
|
299
|
+
return ValueListNode.create(expr2.items.map((item) => this.transform(item, context)));
|
|
300
|
+
}
|
|
301
|
+
_field(expr2, context) {
|
|
302
|
+
if (context.contextValue) {
|
|
303
|
+
const fieldDef2 = QueryUtils.requireField(this.schema, context.modelOrType, expr2.field);
|
|
304
|
+
return this.transformValue(context.contextValue[expr2.field], fieldDef2.type);
|
|
305
|
+
}
|
|
306
|
+
const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, expr2.field);
|
|
307
|
+
if (!fieldDef.relation) {
|
|
308
|
+
return this.createColumnRef(expr2.field, context);
|
|
309
|
+
} else {
|
|
310
|
+
const { memberFilter, memberSelect, ...restContext } = context;
|
|
311
|
+
const relation = this.transformRelationAccess(expr2.field, fieldDef.type, restContext);
|
|
312
|
+
return {
|
|
313
|
+
...relation,
|
|
314
|
+
where: this.mergeWhere(relation.where, memberFilter),
|
|
315
|
+
selections: memberSelect ? [
|
|
316
|
+
memberSelect
|
|
317
|
+
] : relation.selections
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
mergeWhere(where, memberFilter) {
|
|
322
|
+
if (!where) {
|
|
323
|
+
return WhereNode.create(memberFilter ?? trueNode(this.dialect));
|
|
324
|
+
}
|
|
325
|
+
if (!memberFilter) {
|
|
326
|
+
return where;
|
|
327
|
+
}
|
|
328
|
+
return WhereNode.create(conjunction(this.dialect, [
|
|
329
|
+
where.where,
|
|
330
|
+
memberFilter
|
|
331
|
+
]));
|
|
332
|
+
}
|
|
333
|
+
_null() {
|
|
334
|
+
return ValueNode2.createImmediate(null);
|
|
335
|
+
}
|
|
336
|
+
_binary(expr2, context) {
|
|
337
|
+
if (expr2.op === "&&") {
|
|
338
|
+
return conjunction(this.dialect, [
|
|
339
|
+
this.transform(expr2.left, context),
|
|
340
|
+
this.transform(expr2.right, context)
|
|
341
|
+
]);
|
|
342
|
+
} else if (expr2.op === "||") {
|
|
343
|
+
return disjunction(this.dialect, [
|
|
344
|
+
this.transform(expr2.left, context),
|
|
345
|
+
this.transform(expr2.right, context)
|
|
346
|
+
]);
|
|
347
|
+
}
|
|
348
|
+
if (this.isAuthCall(expr2.left) || this.isAuthCall(expr2.right)) {
|
|
349
|
+
return this.transformAuthBinary(expr2, context);
|
|
350
|
+
}
|
|
351
|
+
const op = expr2.op;
|
|
352
|
+
if (op === "?" || op === "!" || op === "^") {
|
|
353
|
+
return this.transformCollectionPredicate(expr2, context);
|
|
354
|
+
}
|
|
355
|
+
const { normalizedLeft, normalizedRight } = this.normalizeBinaryOperationOperands(expr2, context);
|
|
356
|
+
const left = this.transform(normalizedLeft, context);
|
|
357
|
+
const right = this.transform(normalizedRight, context);
|
|
358
|
+
if (op === "in") {
|
|
359
|
+
if (this.isNullNode(left)) {
|
|
360
|
+
return this.transformValue(false, "Boolean");
|
|
361
|
+
} else {
|
|
362
|
+
if (ValueListNode.is(right)) {
|
|
363
|
+
return BinaryOperationNode2.create(left, OperatorNode2.create("in"), right);
|
|
364
|
+
} else {
|
|
365
|
+
return BinaryOperationNode2.create(left, OperatorNode2.create("="), FunctionNode2.create("any", [
|
|
366
|
+
right
|
|
367
|
+
]));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (this.isNullNode(right)) {
|
|
372
|
+
return this.transformNullCheck(left, expr2.op);
|
|
373
|
+
} else if (this.isNullNode(left)) {
|
|
374
|
+
return this.transformNullCheck(right, expr2.op);
|
|
375
|
+
} else {
|
|
376
|
+
return BinaryOperationNode2.create(left, this.transformOperator(op), right);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
transformNullCheck(expr2, operator) {
|
|
380
|
+
if (operator === "==" || operator === "!=") {
|
|
381
|
+
if (ValueNode2.is(expr2)) {
|
|
382
|
+
if (expr2.value === null) {
|
|
383
|
+
return operator === "==" ? trueNode(this.dialect) : falseNode(this.dialect);
|
|
384
|
+
} else {
|
|
385
|
+
return operator === "==" ? falseNode(this.dialect) : trueNode(this.dialect);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
return operator === "==" ? BinaryOperationNode2.create(expr2, OperatorNode2.create("is"), ValueNode2.createImmediate(null)) : BinaryOperationNode2.create(expr2, OperatorNode2.create("is not"), ValueNode2.createImmediate(null));
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
return ValueNode2.createImmediate(null);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
normalizeBinaryOperationOperands(expr2, context) {
|
|
395
|
+
if (context.contextValue) {
|
|
396
|
+
return {
|
|
397
|
+
normalizedLeft: expr2.left,
|
|
398
|
+
normalizedRight: expr2.right
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
let normalizedLeft = expr2.left;
|
|
402
|
+
if (this.isRelationField(expr2.left, context.modelOrType)) {
|
|
403
|
+
invariant2(ExpressionUtils3.isNull(expr2.right), "only null comparison is supported for relation field");
|
|
404
|
+
const leftRelDef = this.getFieldDefFromFieldRef(expr2.left, context.modelOrType);
|
|
405
|
+
invariant2(leftRelDef, "failed to get relation field definition");
|
|
406
|
+
const idFields = QueryUtils.requireIdFields(this.schema, leftRelDef.type);
|
|
407
|
+
normalizedLeft = this.makeOrAppendMember(normalizedLeft, idFields[0]);
|
|
408
|
+
}
|
|
409
|
+
let normalizedRight = expr2.right;
|
|
410
|
+
if (this.isRelationField(expr2.right, context.modelOrType)) {
|
|
411
|
+
invariant2(ExpressionUtils3.isNull(expr2.left), "only null comparison is supported for relation field");
|
|
412
|
+
const rightRelDef = this.getFieldDefFromFieldRef(expr2.right, context.modelOrType);
|
|
413
|
+
invariant2(rightRelDef, "failed to get relation field definition");
|
|
414
|
+
const idFields = QueryUtils.requireIdFields(this.schema, rightRelDef.type);
|
|
415
|
+
normalizedRight = this.makeOrAppendMember(normalizedRight, idFields[0]);
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
normalizedLeft,
|
|
419
|
+
normalizedRight
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
transformCollectionPredicate(expr2, context) {
|
|
423
|
+
this.ensureCollectionPredicateOperator(expr2.op);
|
|
424
|
+
if (this.isAuthMember(expr2.left) || context.contextValue) {
|
|
425
|
+
invariant2(ExpressionUtils3.isMember(expr2.left) || ExpressionUtils3.isField(expr2.left), "expected member or field expression");
|
|
426
|
+
const evaluator = new ExpressionEvaluator();
|
|
427
|
+
const receiver = evaluator.evaluate(expr2.left, {
|
|
428
|
+
thisValue: context.contextValue,
|
|
429
|
+
auth: this.auth
|
|
430
|
+
});
|
|
431
|
+
const baseType = this.isAuthMember(expr2.left) ? this.authType : context.modelOrType;
|
|
432
|
+
const memberType = this.getMemberType(baseType, expr2.left);
|
|
433
|
+
return this.transformValueCollectionPredicate(receiver, expr2, {
|
|
434
|
+
...context,
|
|
435
|
+
modelOrType: memberType
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
invariant2(ExpressionUtils3.isField(expr2.left) || ExpressionUtils3.isMember(expr2.left), "left operand must be field or member access");
|
|
439
|
+
let newContextModel;
|
|
440
|
+
const fieldDef = this.getFieldDefFromFieldRef(expr2.left, context.modelOrType);
|
|
441
|
+
if (fieldDef) {
|
|
442
|
+
invariant2(fieldDef.relation, `field is not a relation: ${JSON.stringify(expr2.left)}`);
|
|
443
|
+
newContextModel = fieldDef.type;
|
|
444
|
+
} else {
|
|
445
|
+
invariant2(ExpressionUtils3.isMember(expr2.left) && ExpressionUtils3.isField(expr2.left.receiver), "left operand must be member access with field receiver");
|
|
446
|
+
const fieldDef2 = QueryUtils.requireField(this.schema, context.modelOrType, expr2.left.receiver.field);
|
|
447
|
+
newContextModel = fieldDef2.type;
|
|
448
|
+
for (const member of expr2.left.members) {
|
|
449
|
+
const memberDef = QueryUtils.requireField(this.schema, newContextModel, member);
|
|
450
|
+
newContextModel = memberDef.type;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
let predicateFilter = this.transform(expr2.right, {
|
|
454
|
+
...context,
|
|
455
|
+
modelOrType: newContextModel,
|
|
456
|
+
alias: void 0
|
|
457
|
+
});
|
|
458
|
+
if (expr2.op === "!") {
|
|
459
|
+
predicateFilter = logicalNot(this.dialect, predicateFilter);
|
|
460
|
+
}
|
|
461
|
+
const count = FunctionNode2.create("count", [
|
|
462
|
+
ValueNode2.createImmediate(1)
|
|
463
|
+
]);
|
|
464
|
+
const predicateResult = match2(expr2.op).with("?", () => BinaryOperationNode2.create(count, OperatorNode2.create(">"), ValueNode2.createImmediate(0))).with("!", () => BinaryOperationNode2.create(count, OperatorNode2.create("="), ValueNode2.createImmediate(0))).with("^", () => BinaryOperationNode2.create(count, OperatorNode2.create("="), ValueNode2.createImmediate(0))).exhaustive();
|
|
465
|
+
return this.transform(expr2.left, {
|
|
466
|
+
...context,
|
|
467
|
+
memberSelect: SelectionNode.create(AliasNode2.create(predicateResult, IdentifierNode.create("$t"))),
|
|
468
|
+
memberFilter: predicateFilter
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
ensureCollectionPredicateOperator(op) {
|
|
472
|
+
invariant2(CollectionPredicateOperator.includes(op), 'expected "?" or "!" or "^" operator');
|
|
473
|
+
}
|
|
474
|
+
transformValueCollectionPredicate(receiver, expr2, context) {
|
|
475
|
+
if (!receiver) {
|
|
476
|
+
return ValueNode2.createImmediate(null);
|
|
477
|
+
}
|
|
478
|
+
this.ensureCollectionPredicateOperator(expr2.op);
|
|
479
|
+
const visitor = new SchemaUtils.MatchingExpressionVisitor((e) => ExpressionUtils3.isThis(e));
|
|
480
|
+
if (!visitor.find(expr2.right)) {
|
|
481
|
+
const value = new ExpressionEvaluator().evaluate(expr2, {
|
|
482
|
+
auth: this.auth,
|
|
483
|
+
thisValue: context.contextValue
|
|
484
|
+
});
|
|
485
|
+
return this.transformValue(value, "Boolean");
|
|
486
|
+
} else {
|
|
487
|
+
invariant2(Array.isArray(receiver), "array value is expected");
|
|
488
|
+
const components = receiver.map((item) => this.transform(expr2.right, {
|
|
489
|
+
operation: context.operation,
|
|
490
|
+
thisType: context.thisType,
|
|
491
|
+
thisAlias: context.thisAlias,
|
|
492
|
+
modelOrType: context.modelOrType,
|
|
493
|
+
contextValue: item
|
|
494
|
+
}));
|
|
495
|
+
return match2(expr2.op).with("?", () => disjunction(this.dialect, components)).with("!", () => conjunction(this.dialect, components)).with("^", () => logicalNot(this.dialect, disjunction(this.dialect, components))).exhaustive();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
getMemberType(receiverType, expr2) {
|
|
499
|
+
if (ExpressionUtils3.isField(expr2)) {
|
|
500
|
+
const fieldDef = QueryUtils.requireField(this.schema, receiverType, expr2.field);
|
|
501
|
+
return fieldDef.type;
|
|
502
|
+
} else {
|
|
503
|
+
let currType = receiverType;
|
|
504
|
+
for (const member of expr2.members) {
|
|
505
|
+
const fieldDef = QueryUtils.requireField(this.schema, currType, member);
|
|
506
|
+
currType = fieldDef.type;
|
|
507
|
+
}
|
|
508
|
+
return currType;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
transformAuthBinary(expr2, context) {
|
|
512
|
+
if (expr2.op !== "==" && expr2.op !== "!=") {
|
|
513
|
+
throw createUnsupportedError(`Unsupported operator for \`auth()\` in policy of model "${context.modelOrType}": ${expr2.op}`);
|
|
514
|
+
}
|
|
515
|
+
let authExpr;
|
|
516
|
+
let other;
|
|
517
|
+
if (this.isAuthCall(expr2.left)) {
|
|
518
|
+
authExpr = expr2.left;
|
|
519
|
+
other = expr2.right;
|
|
520
|
+
} else {
|
|
521
|
+
authExpr = expr2.right;
|
|
522
|
+
other = expr2.left;
|
|
523
|
+
}
|
|
524
|
+
if (ExpressionUtils3.isNull(other)) {
|
|
525
|
+
return this.transformValue(expr2.op === "==" ? !this.auth : !!this.auth, "Boolean");
|
|
526
|
+
} else {
|
|
527
|
+
const authModel = QueryUtils.getModel(this.schema, this.authType);
|
|
528
|
+
if (!authModel) {
|
|
529
|
+
throw createUnsupportedError(`Unsupported use of \`auth()\` in policy of model "${context.modelOrType}", comparing with \`auth()\` is only possible when auth type is a model`);
|
|
530
|
+
}
|
|
531
|
+
const idFields = Object.values(authModel.fields).filter((f) => f.id).map((f) => f.name);
|
|
532
|
+
invariant2(idFields.length > 0, "auth type model must have at least one id field");
|
|
533
|
+
const conditions = idFields.map((fieldName) => ExpressionUtils3.binary(ExpressionUtils3.member(authExpr, [
|
|
534
|
+
fieldName
|
|
535
|
+
]), "==", this.makeOrAppendMember(other, fieldName)));
|
|
536
|
+
let result = this.buildAnd(conditions);
|
|
537
|
+
if (expr2.op === "!=") {
|
|
538
|
+
result = this.buildLogicalNot(result);
|
|
539
|
+
}
|
|
540
|
+
return this.transform(result, context);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
makeOrAppendMember(other, fieldName) {
|
|
544
|
+
if (ExpressionUtils3.isMember(other)) {
|
|
545
|
+
return ExpressionUtils3.member(other.receiver, [
|
|
546
|
+
...other.members,
|
|
547
|
+
fieldName
|
|
548
|
+
]);
|
|
549
|
+
} else {
|
|
550
|
+
return ExpressionUtils3.member(other, [
|
|
551
|
+
fieldName
|
|
552
|
+
]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
transformValue(value, type) {
|
|
556
|
+
if (value === true) {
|
|
557
|
+
return trueNode(this.dialect);
|
|
558
|
+
} else if (value === false) {
|
|
559
|
+
return falseNode(this.dialect);
|
|
560
|
+
} else {
|
|
561
|
+
const transformed = this.dialect.transformPrimitive(value, type, false) ?? null;
|
|
562
|
+
if (!Array.isArray(transformed)) {
|
|
563
|
+
return ValueNode2.createImmediate(transformed);
|
|
564
|
+
} else {
|
|
565
|
+
return ValueNode2.create(transformed);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
_unary(expr2, context) {
|
|
570
|
+
invariant2(expr2.op === "!", 'only "!" operator is supported');
|
|
571
|
+
return logicalNot(this.dialect, this.transform(expr2.operand, context));
|
|
572
|
+
}
|
|
573
|
+
transformOperator(op) {
|
|
574
|
+
const mappedOp = match2(op).with("==", () => "=").otherwise(() => op);
|
|
575
|
+
return OperatorNode2.create(mappedOp);
|
|
576
|
+
}
|
|
577
|
+
_call(expr2, context) {
|
|
578
|
+
const result = this.transformCall(expr2, context);
|
|
579
|
+
return result.toOperationNode();
|
|
580
|
+
}
|
|
581
|
+
transformCall(expr2, context) {
|
|
582
|
+
const func = this.getFunctionImpl(expr2.function);
|
|
583
|
+
if (!func) {
|
|
584
|
+
throw createUnsupportedError(`Function not implemented: ${expr2.function}`);
|
|
585
|
+
}
|
|
586
|
+
const eb = expressionBuilder();
|
|
587
|
+
return func(eb, (expr2.args ?? []).map((arg) => this.transformCallArg(eb, arg, context)), {
|
|
588
|
+
client: this.client,
|
|
589
|
+
dialect: this.dialect,
|
|
590
|
+
model: context.modelOrType,
|
|
591
|
+
modelAlias: context.alias ?? context.modelOrType,
|
|
592
|
+
operation: context.operation
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
getFunctionImpl(functionName) {
|
|
596
|
+
let func = this.clientOptions.functions?.[functionName];
|
|
597
|
+
if (!func) {
|
|
598
|
+
for (const plugin of this.clientOptions.plugins ?? []) {
|
|
599
|
+
if (plugin.functions?.[functionName]) {
|
|
600
|
+
func = plugin.functions[functionName];
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return func;
|
|
606
|
+
}
|
|
607
|
+
transformCallArg(eb, arg, context) {
|
|
608
|
+
if (ExpressionUtils3.isLiteral(arg)) {
|
|
609
|
+
return eb.val(arg.value);
|
|
610
|
+
}
|
|
611
|
+
if (ExpressionUtils3.isField(arg)) {
|
|
612
|
+
return eb.ref(arg.field);
|
|
613
|
+
}
|
|
614
|
+
if (ExpressionUtils3.isCall(arg)) {
|
|
615
|
+
return this.transformCall(arg, context);
|
|
616
|
+
}
|
|
617
|
+
if (this.isAuthMember(arg)) {
|
|
618
|
+
const valNode = this.valueMemberAccess(this.auth, arg, this.authType);
|
|
619
|
+
return valNode ? eb.val(valNode.value) : eb.val(null);
|
|
620
|
+
}
|
|
621
|
+
throw createUnsupportedError(`Unsupported argument expression: ${arg.kind}`);
|
|
622
|
+
}
|
|
623
|
+
_member(expr2, context) {
|
|
624
|
+
if (this.isAuthCall(expr2.receiver)) {
|
|
625
|
+
return this.valueMemberAccess(this.auth, expr2, this.authType);
|
|
626
|
+
}
|
|
627
|
+
if (isBeforeInvocation(expr2.receiver)) {
|
|
628
|
+
invariant2(context.operation === "post-update", "before() can only be used in post-update policy");
|
|
629
|
+
invariant2(expr2.members.length === 1, "before() can only be followed by a scalar field access");
|
|
630
|
+
return ReferenceNode2.create(ColumnNode2.create(expr2.members[0]), TableNode2.create("$before"));
|
|
631
|
+
}
|
|
632
|
+
invariant2(ExpressionUtils3.isField(expr2.receiver) || ExpressionUtils3.isThis(expr2.receiver), 'expect receiver to be field expression or "this"');
|
|
633
|
+
let members = expr2.members;
|
|
634
|
+
let receiver;
|
|
635
|
+
const { memberFilter, memberSelect, ...restContext } = context;
|
|
636
|
+
if (ExpressionUtils3.isThis(expr2.receiver)) {
|
|
637
|
+
if (expr2.members.length === 1) {
|
|
638
|
+
return this._field(ExpressionUtils3.field(expr2.members[0]), {
|
|
639
|
+
...context,
|
|
640
|
+
alias: context.thisAlias,
|
|
641
|
+
modelOrType: context.thisType,
|
|
642
|
+
thisType: context.thisType,
|
|
643
|
+
contextValue: void 0
|
|
644
|
+
});
|
|
645
|
+
} else {
|
|
646
|
+
const firstMemberFieldDef = QueryUtils.requireField(this.schema, context.thisType, expr2.members[0]);
|
|
647
|
+
receiver = this.transformRelationAccess(expr2.members[0], firstMemberFieldDef.type, restContext);
|
|
648
|
+
members = expr2.members.slice(1);
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
receiver = this.transform(expr2.receiver, restContext);
|
|
652
|
+
}
|
|
653
|
+
invariant2(SelectQueryNode.is(receiver), "expected receiver to be select query");
|
|
654
|
+
let startType;
|
|
655
|
+
if (ExpressionUtils3.isField(expr2.receiver)) {
|
|
656
|
+
const receiverField = QueryUtils.requireField(this.schema, context.modelOrType, expr2.receiver.field);
|
|
657
|
+
startType = receiverField.type;
|
|
658
|
+
} else {
|
|
659
|
+
startType = context.thisType;
|
|
660
|
+
}
|
|
661
|
+
const memberFields = [];
|
|
662
|
+
let currType = startType;
|
|
663
|
+
for (const member of members) {
|
|
664
|
+
const fieldDef = QueryUtils.requireField(this.schema, currType, member);
|
|
665
|
+
memberFields.push({
|
|
666
|
+
fieldDef,
|
|
667
|
+
fromModel: currType
|
|
668
|
+
});
|
|
669
|
+
currType = fieldDef.type;
|
|
670
|
+
}
|
|
671
|
+
let currNode = void 0;
|
|
672
|
+
for (let i = members.length - 1; i >= 0; i--) {
|
|
673
|
+
const member = members[i];
|
|
674
|
+
const { fieldDef, fromModel } = memberFields[i];
|
|
675
|
+
if (fieldDef.relation) {
|
|
676
|
+
const relation = this.transformRelationAccess(member, fieldDef.type, {
|
|
677
|
+
...restContext,
|
|
678
|
+
modelOrType: fromModel,
|
|
679
|
+
alias: void 0
|
|
680
|
+
});
|
|
681
|
+
if (currNode) {
|
|
682
|
+
currNode = {
|
|
683
|
+
...relation,
|
|
684
|
+
selections: [
|
|
685
|
+
SelectionNode.create(AliasNode2.create(currNode, IdentifierNode.create(members[i + 1])))
|
|
686
|
+
]
|
|
687
|
+
};
|
|
688
|
+
} else {
|
|
689
|
+
currNode = {
|
|
690
|
+
...relation,
|
|
691
|
+
where: this.mergeWhere(relation.where, memberFilter),
|
|
692
|
+
selections: memberSelect ? [
|
|
693
|
+
memberSelect
|
|
694
|
+
] : relation.selections
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
invariant2(i === members.length - 1, "plain field access must be the last segment");
|
|
699
|
+
invariant2(!currNode, "plain field access must be the last segment");
|
|
700
|
+
currNode = ColumnNode2.create(member);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
...receiver,
|
|
705
|
+
selections: [
|
|
706
|
+
SelectionNode.create(AliasNode2.create(currNode, IdentifierNode.create("$t")))
|
|
707
|
+
]
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
valueMemberAccess(receiver, expr2, receiverType) {
|
|
711
|
+
if (!receiver) {
|
|
712
|
+
return ValueNode2.createImmediate(null);
|
|
713
|
+
}
|
|
714
|
+
invariant2(expr2.members.length > 0, "member expression must have at least one member");
|
|
715
|
+
let curr = receiver;
|
|
716
|
+
let currType = receiverType;
|
|
717
|
+
for (let i = 0; i < expr2.members.length; i++) {
|
|
718
|
+
const field = expr2.members[i];
|
|
719
|
+
curr = curr?.[field];
|
|
720
|
+
if (curr === void 0) {
|
|
721
|
+
curr = ValueNode2.createImmediate(null);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
currType = QueryUtils.requireField(this.schema, currType, field).type;
|
|
725
|
+
if (i === expr2.members.length - 1) {
|
|
726
|
+
curr = this.transformValue(curr, currType);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return curr;
|
|
730
|
+
}
|
|
731
|
+
transformRelationAccess(field, relationModel, context) {
|
|
732
|
+
const m2m = QueryUtils.getManyToManyRelation(this.schema, context.modelOrType, field);
|
|
733
|
+
if (m2m) {
|
|
734
|
+
return this.transformManyToManyRelationAccess(m2m, context);
|
|
735
|
+
}
|
|
736
|
+
const fromModel = context.modelOrType;
|
|
737
|
+
const relationFieldDef = QueryUtils.requireField(this.schema, fromModel, field);
|
|
738
|
+
const { keyPairs, ownedByModel } = QueryUtils.getRelationForeignKeyFieldPairs(this.schema, fromModel, field);
|
|
739
|
+
let condition;
|
|
740
|
+
if (ownedByModel) {
|
|
741
|
+
condition = conjunction(this.dialect, keyPairs.map(({ fk, pk }) => {
|
|
742
|
+
let fkRef = ReferenceNode2.create(ColumnNode2.create(fk), TableNode2.create(context.alias ?? fromModel));
|
|
743
|
+
if (relationFieldDef.originModel && relationFieldDef.originModel !== fromModel) {
|
|
744
|
+
fkRef = this.buildDelegateBaseFieldSelect(fromModel, context.alias ?? fromModel, fk, relationFieldDef.originModel);
|
|
745
|
+
}
|
|
746
|
+
return BinaryOperationNode2.create(fkRef, OperatorNode2.create("="), ReferenceNode2.create(ColumnNode2.create(pk), TableNode2.create(relationModel)));
|
|
747
|
+
}));
|
|
748
|
+
} else {
|
|
749
|
+
condition = conjunction(this.dialect, keyPairs.map(({ fk, pk }) => BinaryOperationNode2.create(ReferenceNode2.create(ColumnNode2.create(pk), TableNode2.create(context.alias ?? fromModel)), OperatorNode2.create("="), ReferenceNode2.create(ColumnNode2.create(fk), TableNode2.create(relationModel)))));
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
kind: "SelectQueryNode",
|
|
753
|
+
from: FromNode.create([
|
|
754
|
+
TableNode2.create(relationModel)
|
|
755
|
+
]),
|
|
756
|
+
where: WhereNode.create(condition)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
transformManyToManyRelationAccess(m2m, context) {
|
|
760
|
+
const eb = expressionBuilder();
|
|
761
|
+
const relationQuery = eb.selectFrom(m2m.otherModel).innerJoin(m2m.joinTable, (join) => join.onRef(`${m2m.otherModel}.${m2m.otherPKName}`, "=", `${m2m.joinTable}.${m2m.otherFkName}`).onRef(`${m2m.joinTable}.${m2m.parentFkName}`, "=", `${context.alias ?? context.modelOrType}.${m2m.parentPKName}`));
|
|
762
|
+
return relationQuery.toOperationNode();
|
|
763
|
+
}
|
|
764
|
+
createColumnRef(column, context) {
|
|
765
|
+
const tableName = context.alias ?? context.modelOrType;
|
|
766
|
+
if (context.operation === "create") {
|
|
767
|
+
return ReferenceNode2.create(ColumnNode2.create(column), TableNode2.create(tableName));
|
|
768
|
+
}
|
|
769
|
+
const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, column);
|
|
770
|
+
if (!fieldDef.originModel || fieldDef.originModel === context.modelOrType) {
|
|
771
|
+
return ReferenceNode2.create(ColumnNode2.create(column), TableNode2.create(tableName));
|
|
772
|
+
}
|
|
773
|
+
return this.buildDelegateBaseFieldSelect(context.modelOrType, tableName, column, fieldDef.originModel);
|
|
774
|
+
}
|
|
775
|
+
buildDelegateBaseFieldSelect(model, modelAlias, field, baseModel) {
|
|
776
|
+
const idFields = QueryUtils.requireIdFields(this.client.$schema, model);
|
|
777
|
+
return {
|
|
778
|
+
kind: "SelectQueryNode",
|
|
779
|
+
from: FromNode.create([
|
|
780
|
+
TableNode2.create(baseModel)
|
|
781
|
+
]),
|
|
782
|
+
selections: [
|
|
783
|
+
SelectionNode.create(ReferenceNode2.create(ColumnNode2.create(field), TableNode2.create(baseModel)))
|
|
784
|
+
],
|
|
785
|
+
where: WhereNode.create(conjunction(this.dialect, idFields.map((idField) => BinaryOperationNode2.create(ReferenceNode2.create(ColumnNode2.create(idField), TableNode2.create(baseModel)), OperatorNode2.create("="), ReferenceNode2.create(ColumnNode2.create(idField), TableNode2.create(modelAlias))))))
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
isAuthCall(value) {
|
|
789
|
+
return ExpressionUtils3.isCall(value) && value.function === "auth";
|
|
790
|
+
}
|
|
791
|
+
isAuthMember(expr2) {
|
|
792
|
+
return ExpressionUtils3.isMember(expr2) && this.isAuthCall(expr2.receiver);
|
|
793
|
+
}
|
|
794
|
+
isNullNode(node) {
|
|
795
|
+
return ValueNode2.is(node) && node.value === null;
|
|
796
|
+
}
|
|
797
|
+
buildLogicalNot(result) {
|
|
798
|
+
return ExpressionUtils3.unary("!", result);
|
|
799
|
+
}
|
|
800
|
+
buildAnd(conditions) {
|
|
801
|
+
if (conditions.length === 0) {
|
|
802
|
+
return ExpressionUtils3.literal(true);
|
|
803
|
+
} else if (conditions.length === 1) {
|
|
804
|
+
return conditions[0];
|
|
805
|
+
} else {
|
|
806
|
+
return conditions.reduce((acc, condition) => ExpressionUtils3.binary(acc, "&&", condition));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
isRelationField(expr2, model) {
|
|
810
|
+
const fieldDef = this.getFieldDefFromFieldRef(expr2, model);
|
|
811
|
+
return !!fieldDef?.relation;
|
|
812
|
+
}
|
|
813
|
+
getFieldDefFromFieldRef(expr2, model) {
|
|
814
|
+
if (ExpressionUtils3.isField(expr2)) {
|
|
815
|
+
return QueryUtils.getField(this.schema, model, expr2.field);
|
|
816
|
+
} else if (ExpressionUtils3.isMember(expr2) && expr2.members.length === 1 && ExpressionUtils3.isThis(expr2.receiver)) {
|
|
817
|
+
return QueryUtils.getField(this.schema, model, expr2.members[0]);
|
|
818
|
+
} else {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
_ts_decorate([
|
|
824
|
+
expr("literal"),
|
|
825
|
+
_ts_metadata("design:type", Function),
|
|
826
|
+
_ts_metadata("design:paramtypes", [
|
|
827
|
+
typeof LiteralExpression === "undefined" ? Object : LiteralExpression
|
|
828
|
+
]),
|
|
829
|
+
_ts_metadata("design:returntype", void 0)
|
|
830
|
+
], ExpressionTransformer.prototype, "_literal", null);
|
|
831
|
+
_ts_decorate([
|
|
832
|
+
expr("array"),
|
|
833
|
+
_ts_metadata("design:type", Function),
|
|
834
|
+
_ts_metadata("design:paramtypes", [
|
|
835
|
+
typeof ArrayExpression === "undefined" ? Object : ArrayExpression,
|
|
836
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
837
|
+
]),
|
|
838
|
+
_ts_metadata("design:returntype", void 0)
|
|
839
|
+
], ExpressionTransformer.prototype, "_array", null);
|
|
840
|
+
_ts_decorate([
|
|
841
|
+
expr("field"),
|
|
842
|
+
_ts_metadata("design:type", Function),
|
|
843
|
+
_ts_metadata("design:paramtypes", [
|
|
844
|
+
typeof FieldExpression === "undefined" ? Object : FieldExpression,
|
|
845
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
846
|
+
]),
|
|
847
|
+
_ts_metadata("design:returntype", void 0)
|
|
848
|
+
], ExpressionTransformer.prototype, "_field", null);
|
|
849
|
+
_ts_decorate([
|
|
850
|
+
expr("null"),
|
|
851
|
+
_ts_metadata("design:type", Function),
|
|
852
|
+
_ts_metadata("design:paramtypes", []),
|
|
853
|
+
_ts_metadata("design:returntype", void 0)
|
|
854
|
+
], ExpressionTransformer.prototype, "_null", null);
|
|
855
|
+
_ts_decorate([
|
|
856
|
+
expr("binary"),
|
|
857
|
+
_ts_metadata("design:type", Function),
|
|
858
|
+
_ts_metadata("design:paramtypes", [
|
|
859
|
+
typeof BinaryExpression === "undefined" ? Object : BinaryExpression,
|
|
860
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
861
|
+
]),
|
|
862
|
+
_ts_metadata("design:returntype", void 0)
|
|
863
|
+
], ExpressionTransformer.prototype, "_binary", null);
|
|
864
|
+
_ts_decorate([
|
|
865
|
+
expr("unary"),
|
|
866
|
+
_ts_metadata("design:type", Function),
|
|
867
|
+
_ts_metadata("design:paramtypes", [
|
|
868
|
+
typeof UnaryExpression === "undefined" ? Object : UnaryExpression,
|
|
869
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
870
|
+
]),
|
|
871
|
+
_ts_metadata("design:returntype", void 0)
|
|
872
|
+
], ExpressionTransformer.prototype, "_unary", null);
|
|
873
|
+
_ts_decorate([
|
|
874
|
+
expr("call"),
|
|
875
|
+
_ts_metadata("design:type", Function),
|
|
876
|
+
_ts_metadata("design:paramtypes", [
|
|
877
|
+
typeof CallExpression === "undefined" ? Object : CallExpression,
|
|
878
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
879
|
+
]),
|
|
880
|
+
_ts_metadata("design:returntype", void 0)
|
|
881
|
+
], ExpressionTransformer.prototype, "_call", null);
|
|
882
|
+
_ts_decorate([
|
|
883
|
+
expr("member"),
|
|
884
|
+
_ts_metadata("design:type", Function),
|
|
885
|
+
_ts_metadata("design:paramtypes", [
|
|
886
|
+
typeof MemberExpression === "undefined" ? Object : MemberExpression,
|
|
887
|
+
typeof ExpressionTransformerContext === "undefined" ? Object : ExpressionTransformerContext
|
|
888
|
+
]),
|
|
889
|
+
_ts_metadata("design:returntype", void 0)
|
|
890
|
+
], ExpressionTransformer.prototype, "_member", null);
|
|
891
|
+
|
|
892
|
+
// src/policy-handler.ts
|
|
893
|
+
var PolicyHandler = class extends OperationNodeTransformer {
|
|
894
|
+
static {
|
|
895
|
+
__name(this, "PolicyHandler");
|
|
896
|
+
}
|
|
897
|
+
client;
|
|
898
|
+
dialect;
|
|
899
|
+
constructor(client) {
|
|
900
|
+
super(), this.client = client;
|
|
901
|
+
this.dialect = getCrudDialect2(this.client.$schema, this.client.$options);
|
|
902
|
+
}
|
|
903
|
+
get kysely() {
|
|
904
|
+
return this.client.$qb;
|
|
905
|
+
}
|
|
906
|
+
// #region main entry point
|
|
907
|
+
async handle(node, proceed) {
|
|
908
|
+
if (!this.isCrudQueryNode(node)) {
|
|
909
|
+
throw createRejectedByPolicyError(void 0, RejectedByPolicyReason.OTHER, "non-CRUD queries are not allowed");
|
|
910
|
+
}
|
|
911
|
+
if (!this.isMutationQueryNode(node)) {
|
|
912
|
+
return proceed(this.transformNode(node));
|
|
913
|
+
}
|
|
914
|
+
const { mutationModel } = this.getMutationModel(node);
|
|
915
|
+
this.tryRejectNonexistentModel(mutationModel);
|
|
916
|
+
if (InsertQueryNode.is(node)) {
|
|
917
|
+
await this.preCreateCheck(mutationModel, node, proceed);
|
|
918
|
+
}
|
|
919
|
+
if (UpdateQueryNode.is(node)) {
|
|
920
|
+
await this.preUpdateCheck(mutationModel, node, proceed);
|
|
921
|
+
}
|
|
922
|
+
const hasPostUpdatePolicies = UpdateQueryNode.is(node) && this.hasPostUpdatePolicies(mutationModel);
|
|
923
|
+
let beforeUpdateInfo;
|
|
924
|
+
if (hasPostUpdatePolicies) {
|
|
925
|
+
beforeUpdateInfo = await this.loadBeforeUpdateEntities(mutationModel, node.where, proceed);
|
|
926
|
+
}
|
|
927
|
+
const result = await proceed(this.transformNode(node));
|
|
928
|
+
if (hasPostUpdatePolicies && result.rows.length > 0) {
|
|
929
|
+
if (beforeUpdateInfo) {
|
|
930
|
+
invariant3(beforeUpdateInfo.rows.length === result.rows.length);
|
|
931
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, mutationModel);
|
|
932
|
+
for (const postRow of result.rows) {
|
|
933
|
+
const beforeRow = beforeUpdateInfo.rows.find((r) => idFields.every((f) => r[f] === postRow[f]));
|
|
934
|
+
if (!beforeRow) {
|
|
935
|
+
throw createRejectedByPolicyError(mutationModel, RejectedByPolicyReason.OTHER, "Before-update and after-update rows do not match by id. If you have post-update policies on a model, updating id fields is not supported.");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const idConditions = this.buildIdConditions(mutationModel, result.rows);
|
|
940
|
+
const postUpdateFilter = this.buildPolicyFilter(mutationModel, void 0, "post-update");
|
|
941
|
+
const eb = expressionBuilder2();
|
|
942
|
+
const beforeUpdateTable = beforeUpdateInfo ? {
|
|
943
|
+
kind: "SelectQueryNode",
|
|
944
|
+
from: FromNode2.create([
|
|
945
|
+
ParensNode2.create(ValuesNode.create(beforeUpdateInfo.rows.map((r) => PrimitiveValueListNode.create(beforeUpdateInfo.fields.map((f) => r[f])))))
|
|
946
|
+
]),
|
|
947
|
+
selections: beforeUpdateInfo.fields.map((name, index) => {
|
|
948
|
+
const def = QueryUtils2.requireField(this.client.$schema, mutationModel, name);
|
|
949
|
+
const castedColumnRef = sql`CAST(${eb.ref(`column${index + 1}`)} as ${sql.raw(this.dialect.getFieldSqlType(def))})`.as(name);
|
|
950
|
+
return SelectionNode2.create(castedColumnRef.toOperationNode());
|
|
951
|
+
})
|
|
952
|
+
} : void 0;
|
|
953
|
+
const postUpdateQuery = eb.selectFrom(mutationModel).select(() => [
|
|
954
|
+
eb(eb.fn("COUNT", [
|
|
955
|
+
eb.lit(1)
|
|
956
|
+
]), "=", result.rows.length).as("$condition")
|
|
957
|
+
]).where(() => new ExpressionWrapper(conjunction(this.dialect, [
|
|
958
|
+
idConditions,
|
|
959
|
+
postUpdateFilter
|
|
960
|
+
]))).$if(!!beforeUpdateInfo, (qb) => qb.leftJoin(() => new ExpressionWrapper(beforeUpdateTable).as("$before"), (join) => {
|
|
961
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, mutationModel);
|
|
962
|
+
return idFields.reduce((acc, f) => acc.onRef(`${mutationModel}.${f}`, "=", `$before.${f}`), join);
|
|
963
|
+
}));
|
|
964
|
+
const postUpdateResult = await proceed(postUpdateQuery.toOperationNode());
|
|
965
|
+
if (!postUpdateResult.rows[0]?.$condition) {
|
|
966
|
+
throw createRejectedByPolicyError(mutationModel, RejectedByPolicyReason.NO_ACCESS, "some or all updated rows failed to pass post-update policy check");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (!node.returning || this.onlyReturningId(node)) {
|
|
970
|
+
return this.postProcessMutationResult(result, node);
|
|
971
|
+
} else {
|
|
972
|
+
const readBackResult = await this.processReadBack(node, result, proceed);
|
|
973
|
+
if (readBackResult.rows.length !== result.rows.length) {
|
|
974
|
+
throw createRejectedByPolicyError(mutationModel, RejectedByPolicyReason.CANNOT_READ_BACK, "result is not allowed to be read back");
|
|
975
|
+
}
|
|
976
|
+
return readBackResult;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async preCreateCheck(mutationModel, node, proceed) {
|
|
980
|
+
const isManyToManyJoinTable = this.isManyToManyJoinTable(mutationModel);
|
|
981
|
+
let needCheckPreCreate = true;
|
|
982
|
+
if (!isManyToManyJoinTable) {
|
|
983
|
+
const constCondition = this.tryGetConstantPolicy(mutationModel, "create");
|
|
984
|
+
if (constCondition === true) {
|
|
985
|
+
needCheckPreCreate = false;
|
|
986
|
+
} else if (constCondition === false) {
|
|
987
|
+
throw createRejectedByPolicyError(mutationModel, RejectedByPolicyReason.NO_ACCESS);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (needCheckPreCreate) {
|
|
991
|
+
await this.enforcePreCreatePolicy(node, mutationModel, isManyToManyJoinTable, proceed);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async preUpdateCheck(mutationModel, node, proceed) {
|
|
995
|
+
const fieldsToUpdate = node.updates?.map((u) => ColumnNode3.is(u.column) ? u.column.column.name : void 0).filter((f) => !!f) ?? [];
|
|
996
|
+
const fieldUpdatePolicies = fieldsToUpdate.map((f) => this.buildFieldPolicyFilter(mutationModel, f, "update"));
|
|
997
|
+
const fieldLevelFilter = conjunction(this.dialect, fieldUpdatePolicies);
|
|
998
|
+
if (isTrueNode(fieldLevelFilter)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const modelLevelFilter = this.buildPolicyFilter(mutationModel, void 0, "update");
|
|
1002
|
+
const updateFilter = conjunction(this.dialect, [
|
|
1003
|
+
modelLevelFilter,
|
|
1004
|
+
node.where?.where ?? trueNode(this.dialect)
|
|
1005
|
+
]);
|
|
1006
|
+
const preUpdateCheckQuery = expressionBuilder2().selectFrom(mutationModel).select((eb) => eb.fn.coalesce(eb.fn.sum(eb.cast(new ExpressionWrapper(logicalNot(this.dialect, fieldLevelFilter)), "integer")), eb.lit(0)).as("$filteredCount")).where(() => new ExpressionWrapper(updateFilter));
|
|
1007
|
+
const preUpdateResult = await proceed(preUpdateCheckQuery.toOperationNode());
|
|
1008
|
+
if (preUpdateResult.rows[0].$filteredCount > 0) {
|
|
1009
|
+
throw createRejectedByPolicyError(mutationModel, RejectedByPolicyReason.NO_ACCESS, "some rows cannot be updated due to field policies");
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// #endregion
|
|
1013
|
+
// #region Transformations
|
|
1014
|
+
transformSelectQuery(node) {
|
|
1015
|
+
if (!node.from) {
|
|
1016
|
+
return super.transformSelectQuery(node);
|
|
1017
|
+
}
|
|
1018
|
+
this.tryRejectNonexistingTables(node.from.froms);
|
|
1019
|
+
let result = super.transformSelectQuery(node);
|
|
1020
|
+
const hasFieldLevelPolicies = node.from.froms.some((table) => {
|
|
1021
|
+
const extractedTable = this.extractTableName(table);
|
|
1022
|
+
if (extractedTable) {
|
|
1023
|
+
return this.hasFieldLevelPolicies(extractedTable.model, "read");
|
|
1024
|
+
} else {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
if (hasFieldLevelPolicies) {
|
|
1029
|
+
const updatedFroms = [];
|
|
1030
|
+
for (const table of result.from.froms) {
|
|
1031
|
+
const extractedTable = this.extractTableName(table);
|
|
1032
|
+
if (extractedTable?.model && QueryUtils2.getModel(this.client.$schema, extractedTable.model)) {
|
|
1033
|
+
const { query } = this.createSelectAllFieldsWithPolicies(extractedTable.model, extractedTable.alias, "read");
|
|
1034
|
+
updatedFroms.push(query);
|
|
1035
|
+
} else {
|
|
1036
|
+
updatedFroms.push(table);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
result = {
|
|
1040
|
+
...result,
|
|
1041
|
+
from: FromNode2.create(updatedFroms)
|
|
1042
|
+
};
|
|
1043
|
+
} else {
|
|
1044
|
+
let whereNode = result.where;
|
|
1045
|
+
const policyFilter = this.createPolicyFilterForFrom(result.from);
|
|
1046
|
+
if (policyFilter && !isTrueNode(policyFilter)) {
|
|
1047
|
+
whereNode = WhereNode2.create(whereNode?.where ? conjunction(this.dialect, [
|
|
1048
|
+
whereNode.where,
|
|
1049
|
+
policyFilter
|
|
1050
|
+
]) : policyFilter);
|
|
1051
|
+
}
|
|
1052
|
+
result = {
|
|
1053
|
+
...result,
|
|
1054
|
+
where: whereNode
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return result;
|
|
1058
|
+
}
|
|
1059
|
+
transformJoin(node) {
|
|
1060
|
+
const table = this.extractTableName(node.table);
|
|
1061
|
+
if (!table) {
|
|
1062
|
+
return super.transformJoin(node);
|
|
1063
|
+
}
|
|
1064
|
+
this.tryRejectNonexistentModel(table.model);
|
|
1065
|
+
if (!QueryUtils2.getModel(this.client.$schema, table.model)) {
|
|
1066
|
+
return super.transformJoin(node);
|
|
1067
|
+
}
|
|
1068
|
+
const result = super.transformJoin(node);
|
|
1069
|
+
const { hasPolicies, query: nestedQuery } = this.createSelectAllFieldsWithPolicies(table.model, table.alias, "read");
|
|
1070
|
+
if (!hasPolicies) {
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
...result,
|
|
1075
|
+
table: nestedQuery
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
transformInsertQuery(node) {
|
|
1079
|
+
let onConflict = node.onConflict;
|
|
1080
|
+
if (onConflict?.updates) {
|
|
1081
|
+
const { mutationModel, alias } = this.getMutationModel(node);
|
|
1082
|
+
const filter = this.buildPolicyFilter(mutationModel, alias, "update");
|
|
1083
|
+
if (onConflict.updateWhere) {
|
|
1084
|
+
onConflict = {
|
|
1085
|
+
...onConflict,
|
|
1086
|
+
updateWhere: WhereNode2.create(conjunction(this.dialect, [
|
|
1087
|
+
onConflict.updateWhere.where,
|
|
1088
|
+
filter
|
|
1089
|
+
]))
|
|
1090
|
+
};
|
|
1091
|
+
} else {
|
|
1092
|
+
onConflict = {
|
|
1093
|
+
...onConflict,
|
|
1094
|
+
updateWhere: WhereNode2.create(filter)
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const processedNode = onConflict ? {
|
|
1099
|
+
...node,
|
|
1100
|
+
onConflict
|
|
1101
|
+
} : node;
|
|
1102
|
+
const result = super.transformInsertQuery(processedNode);
|
|
1103
|
+
let returning = result.returning;
|
|
1104
|
+
if (returning) {
|
|
1105
|
+
const { mutationModel } = this.getMutationModel(node);
|
|
1106
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, mutationModel);
|
|
1107
|
+
returning = ReturningNode.create(idFields.map((f) => SelectionNode2.create(ColumnNode3.create(f))));
|
|
1108
|
+
}
|
|
1109
|
+
return {
|
|
1110
|
+
...result,
|
|
1111
|
+
returning
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
transformUpdateQuery(node) {
|
|
1115
|
+
const result = super.transformUpdateQuery(node);
|
|
1116
|
+
const { mutationModel, alias } = this.getMutationModel(node);
|
|
1117
|
+
let filter = this.buildPolicyFilter(mutationModel, alias, "update");
|
|
1118
|
+
if (node.from) {
|
|
1119
|
+
this.tryRejectNonexistingTables(node.from.froms);
|
|
1120
|
+
const joinFilter = this.createPolicyFilterForFrom(node.from);
|
|
1121
|
+
if (joinFilter) {
|
|
1122
|
+
filter = conjunction(this.dialect, [
|
|
1123
|
+
filter,
|
|
1124
|
+
joinFilter
|
|
1125
|
+
]);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
let returning = result.returning;
|
|
1129
|
+
if (returning || this.hasPostUpdatePolicies(mutationModel)) {
|
|
1130
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, mutationModel);
|
|
1131
|
+
returning = ReturningNode.create(idFields.map((f) => SelectionNode2.create(ColumnNode3.create(f))));
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
...result,
|
|
1135
|
+
where: WhereNode2.create(result.where ? conjunction(this.dialect, [
|
|
1136
|
+
result.where.where,
|
|
1137
|
+
filter
|
|
1138
|
+
]) : filter),
|
|
1139
|
+
returning
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
transformDeleteQuery(node) {
|
|
1143
|
+
const result = super.transformDeleteQuery(node);
|
|
1144
|
+
const { mutationModel, alias } = this.getMutationModel(node);
|
|
1145
|
+
let filter = this.buildPolicyFilter(mutationModel, alias, "delete");
|
|
1146
|
+
if (node.using) {
|
|
1147
|
+
this.tryRejectNonexistingTables(node.using.tables);
|
|
1148
|
+
const joinFilter = this.createPolicyFilterForTables(node.using.tables);
|
|
1149
|
+
if (joinFilter) {
|
|
1150
|
+
filter = conjunction(this.dialect, [
|
|
1151
|
+
filter,
|
|
1152
|
+
joinFilter
|
|
1153
|
+
]);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
...result,
|
|
1158
|
+
where: WhereNode2.create(result.where ? conjunction(this.dialect, [
|
|
1159
|
+
result.where.where,
|
|
1160
|
+
filter
|
|
1161
|
+
]) : filter)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
// #endregion
|
|
1165
|
+
// #region post-update
|
|
1166
|
+
async loadBeforeUpdateEntities(model, where, proceed) {
|
|
1167
|
+
const beforeUpdateAccessFields = this.getFieldsAccessForBeforeUpdatePolicies(model);
|
|
1168
|
+
if (!beforeUpdateAccessFields || beforeUpdateAccessFields.length === 0) {
|
|
1169
|
+
return void 0;
|
|
1170
|
+
}
|
|
1171
|
+
const policyFilter = this.buildPolicyFilter(model, model, "update");
|
|
1172
|
+
const combinedFilter = where ? conjunction(this.dialect, [
|
|
1173
|
+
where.where,
|
|
1174
|
+
policyFilter
|
|
1175
|
+
]) : policyFilter;
|
|
1176
|
+
const query = {
|
|
1177
|
+
kind: "SelectQueryNode",
|
|
1178
|
+
from: FromNode2.create([
|
|
1179
|
+
TableNode3.create(model)
|
|
1180
|
+
]),
|
|
1181
|
+
where: WhereNode2.create(combinedFilter),
|
|
1182
|
+
selections: [
|
|
1183
|
+
...beforeUpdateAccessFields.map((f) => SelectionNode2.create(ColumnNode3.create(f)))
|
|
1184
|
+
]
|
|
1185
|
+
};
|
|
1186
|
+
const result = await proceed(query);
|
|
1187
|
+
return {
|
|
1188
|
+
fields: beforeUpdateAccessFields,
|
|
1189
|
+
rows: result.rows
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
getFieldsAccessForBeforeUpdatePolicies(model) {
|
|
1193
|
+
const policies = this.getModelPolicies(model, "post-update");
|
|
1194
|
+
if (policies.length === 0) {
|
|
1195
|
+
return void 0;
|
|
1196
|
+
}
|
|
1197
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1198
|
+
const fieldCollector = new class extends SchemaUtils2.ExpressionVisitor {
|
|
1199
|
+
visitMember(e) {
|
|
1200
|
+
if (isBeforeInvocation(e.receiver)) {
|
|
1201
|
+
invariant3(e.members.length === 1, "before() can only be followed by a scalar field access");
|
|
1202
|
+
fields.add(e.members[0]);
|
|
1203
|
+
}
|
|
1204
|
+
super.visitMember(e);
|
|
1205
|
+
}
|
|
1206
|
+
}();
|
|
1207
|
+
for (const policy of policies) {
|
|
1208
|
+
fieldCollector.visit(policy.condition);
|
|
1209
|
+
}
|
|
1210
|
+
if (fields.size === 0) {
|
|
1211
|
+
return void 0;
|
|
1212
|
+
}
|
|
1213
|
+
QueryUtils2.requireIdFields(this.client.$schema, model).forEach((f) => fields.add(f));
|
|
1214
|
+
return Array.from(fields).sort();
|
|
1215
|
+
}
|
|
1216
|
+
hasPostUpdatePolicies(model) {
|
|
1217
|
+
const policies = this.getModelPolicies(model, "post-update");
|
|
1218
|
+
return policies.length > 0;
|
|
1219
|
+
}
|
|
1220
|
+
// #endregion
|
|
1221
|
+
// #region field-level policies
|
|
1222
|
+
createSelectAllFieldsWithPolicies(model, alias, operation) {
|
|
1223
|
+
let hasPolicies = false;
|
|
1224
|
+
const modelDef = QueryUtils2.requireModel(this.client.$schema, model);
|
|
1225
|
+
let selections = [];
|
|
1226
|
+
for (const fieldDef of Object.values(modelDef.fields).filter(
|
|
1227
|
+
// exclude relation/computed/inherited fields
|
|
1228
|
+
(f) => !f.relation && !f.computed && !f.originModel
|
|
1229
|
+
)) {
|
|
1230
|
+
const { hasPolicies: fieldHasPolicies, selection } = this.createFieldSelectionWithPolicy(model, fieldDef.name, operation);
|
|
1231
|
+
hasPolicies = hasPolicies || fieldHasPolicies;
|
|
1232
|
+
selections.push(selection);
|
|
1233
|
+
}
|
|
1234
|
+
if (!hasPolicies) {
|
|
1235
|
+
selections = [
|
|
1236
|
+
SelectionNode2.create(SelectAllNode.create())
|
|
1237
|
+
];
|
|
1238
|
+
}
|
|
1239
|
+
const modelPolicyFilter = this.buildPolicyFilter(model, alias, operation);
|
|
1240
|
+
if (!isTrueNode(modelPolicyFilter)) {
|
|
1241
|
+
hasPolicies = true;
|
|
1242
|
+
}
|
|
1243
|
+
const nestedQuery = {
|
|
1244
|
+
kind: "SelectQueryNode",
|
|
1245
|
+
from: FromNode2.create([
|
|
1246
|
+
TableNode3.create(model)
|
|
1247
|
+
]),
|
|
1248
|
+
where: isTrueNode(modelPolicyFilter) ? void 0 : WhereNode2.create(modelPolicyFilter),
|
|
1249
|
+
selections
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
hasPolicies,
|
|
1253
|
+
query: AliasNode3.create(ParensNode2.create(nestedQuery), IdentifierNode2.create(alias ?? model))
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
createFieldSelectionWithPolicy(model, field, operation) {
|
|
1257
|
+
const filter = this.buildFieldPolicyFilter(model, field, operation);
|
|
1258
|
+
if (isTrueNode(filter)) {
|
|
1259
|
+
return {
|
|
1260
|
+
hasPolicies: false,
|
|
1261
|
+
selection: SelectionNode2.create(ColumnNode3.create(field))
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const eb = expressionBuilder2();
|
|
1265
|
+
const selection = eb.case().when(new ExpressionWrapper(filter)).then(eb.ref(field)).else(null).end().as(field).toOperationNode();
|
|
1266
|
+
return {
|
|
1267
|
+
hasPolicies: true,
|
|
1268
|
+
selection: SelectionNode2.create(selection)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
hasFieldLevelPolicies(model, operation) {
|
|
1272
|
+
const modelDef = QueryUtils2.getModel(this.client.$schema, model);
|
|
1273
|
+
if (!modelDef) {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
return Object.keys(modelDef.fields).some((field) => this.getFieldPolicies(model, field, operation).length > 0);
|
|
1277
|
+
}
|
|
1278
|
+
buildFieldPolicyFilter(model, field, operation) {
|
|
1279
|
+
const policies = this.getFieldPolicies(model, field, operation);
|
|
1280
|
+
const allows = policies.filter((policy) => policy.kind === "allow").map((policy) => this.compilePolicyCondition(model, model, operation, policy));
|
|
1281
|
+
const denies = policies.filter((policy) => policy.kind === "deny").map((policy) => this.compilePolicyCondition(model, model, operation, policy));
|
|
1282
|
+
let combinedPolicy;
|
|
1283
|
+
if (allows.length === 0) {
|
|
1284
|
+
combinedPolicy = trueNode(this.dialect);
|
|
1285
|
+
} else {
|
|
1286
|
+
combinedPolicy = disjunction(this.dialect, allows);
|
|
1287
|
+
}
|
|
1288
|
+
if (denies.length !== 0) {
|
|
1289
|
+
const combinedDenies = conjunction(this.dialect, denies.map((d) => buildIsFalse(d, this.dialect)));
|
|
1290
|
+
combinedPolicy = conjunction(this.dialect, [
|
|
1291
|
+
combinedPolicy,
|
|
1292
|
+
combinedDenies
|
|
1293
|
+
]);
|
|
1294
|
+
}
|
|
1295
|
+
return combinedPolicy;
|
|
1296
|
+
}
|
|
1297
|
+
// #endregion
|
|
1298
|
+
// #region helpers
|
|
1299
|
+
onlyReturningId(node) {
|
|
1300
|
+
if (!node.returning) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
const { mutationModel } = this.getMutationModel(node);
|
|
1304
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, mutationModel);
|
|
1305
|
+
if (node.returning.selections.some((s) => SelectAllNode.is(s.selection))) {
|
|
1306
|
+
const modelDef = QueryUtils2.requireModel(this.client.$schema, mutationModel);
|
|
1307
|
+
if (Object.keys(modelDef.fields).some((f) => !idFields.includes(f))) {
|
|
1308
|
+
return false;
|
|
1309
|
+
} else {
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const collector = new ColumnCollector();
|
|
1314
|
+
const selectedColumns = collector.collect(node.returning);
|
|
1315
|
+
return selectedColumns.every((c) => idFields.includes(c));
|
|
1316
|
+
}
|
|
1317
|
+
async enforcePreCreatePolicy(node, mutationModel, isManyToManyJoinTable, proceed) {
|
|
1318
|
+
const fields = node.columns?.map((c) => c.column.name) ?? [];
|
|
1319
|
+
const valueRows = node.values ? this.unwrapCreateValueRows(node.values, mutationModel, fields, isManyToManyJoinTable) : [
|
|
1320
|
+
[]
|
|
1321
|
+
];
|
|
1322
|
+
for (const values of valueRows) {
|
|
1323
|
+
if (isManyToManyJoinTable) {
|
|
1324
|
+
await this.enforcePreCreatePolicyForManyToManyJoinTable(mutationModel, fields, values.map((v) => v.node), proceed);
|
|
1325
|
+
} else {
|
|
1326
|
+
await this.enforcePreCreatePolicyForOne(mutationModel, fields, values.map((v) => v.node), proceed);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async enforcePreCreatePolicyForManyToManyJoinTable(tableName, fields, values, proceed) {
|
|
1331
|
+
const m2m = this.resolveManyToManyJoinTable(tableName);
|
|
1332
|
+
invariant3(m2m);
|
|
1333
|
+
invariant3(fields.includes("A") && fields.includes("B"), "many-to-many join table must have A and B fk fields");
|
|
1334
|
+
const aIndex = fields.indexOf("A");
|
|
1335
|
+
const aNode = values[aIndex];
|
|
1336
|
+
const bIndex = fields.indexOf("B");
|
|
1337
|
+
const bNode = values[bIndex];
|
|
1338
|
+
invariant3(ValueNode3.is(aNode) && ValueNode3.is(bNode), "A and B values must be ValueNode");
|
|
1339
|
+
const aValue = aNode.value;
|
|
1340
|
+
const bValue = bNode.value;
|
|
1341
|
+
invariant3(aValue !== null && aValue !== void 0, "A value cannot be null or undefined");
|
|
1342
|
+
invariant3(bValue !== null && bValue !== void 0, "B value cannot be null or undefined");
|
|
1343
|
+
const eb = expressionBuilder2();
|
|
1344
|
+
const filterA = this.buildPolicyFilter(m2m.firstModel, void 0, "update");
|
|
1345
|
+
const queryA = eb.selectFrom(m2m.firstModel).where(eb(eb.ref(`${m2m.firstModel}.${m2m.firstIdField}`), "=", aValue)).select(() => new ExpressionWrapper(filterA).as("$t"));
|
|
1346
|
+
const filterB = this.buildPolicyFilter(m2m.secondModel, void 0, "update");
|
|
1347
|
+
const queryB = eb.selectFrom(m2m.secondModel).where(eb(eb.ref(`${m2m.secondModel}.${m2m.secondIdField}`), "=", bValue)).select(() => new ExpressionWrapper(filterB).as("$t"));
|
|
1348
|
+
const queryNode = {
|
|
1349
|
+
kind: "SelectQueryNode",
|
|
1350
|
+
selections: [
|
|
1351
|
+
SelectionNode2.create(AliasNode3.create(queryA.toOperationNode(), IdentifierNode2.create("$conditionA"))),
|
|
1352
|
+
SelectionNode2.create(AliasNode3.create(queryB.toOperationNode(), IdentifierNode2.create("$conditionB")))
|
|
1353
|
+
]
|
|
1354
|
+
};
|
|
1355
|
+
const result = await proceed(queryNode);
|
|
1356
|
+
if (!result.rows[0]?.$conditionA) {
|
|
1357
|
+
throw createRejectedByPolicyError(m2m.firstModel, RejectedByPolicyReason.CANNOT_READ_BACK, `many-to-many relation participant model "${m2m.firstModel}" not updatable`);
|
|
1358
|
+
}
|
|
1359
|
+
if (!result.rows[0]?.$conditionB) {
|
|
1360
|
+
throw createRejectedByPolicyError(m2m.secondModel, RejectedByPolicyReason.NO_ACCESS, `many-to-many relation participant model "${m2m.secondModel}" not updatable`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async enforcePreCreatePolicyForOne(model, fields, values, proceed) {
|
|
1364
|
+
const allFields = Object.entries(QueryUtils2.requireModel(this.client.$schema, model).fields).filter(([, def]) => !def.relation);
|
|
1365
|
+
const allValues = [];
|
|
1366
|
+
for (const [name, _def] of allFields) {
|
|
1367
|
+
const index = fields.indexOf(name);
|
|
1368
|
+
if (index >= 0) {
|
|
1369
|
+
allValues.push(values[index]);
|
|
1370
|
+
} else {
|
|
1371
|
+
allValues.push(ValueNode3.createImmediate(null));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const eb = expressionBuilder2();
|
|
1375
|
+
const constTable = {
|
|
1376
|
+
kind: "SelectQueryNode",
|
|
1377
|
+
from: FromNode2.create([
|
|
1378
|
+
AliasNode3.create(ParensNode2.create(ValuesNode.create([
|
|
1379
|
+
ValueListNode2.create(allValues)
|
|
1380
|
+
])), IdentifierNode2.create("$t"))
|
|
1381
|
+
]),
|
|
1382
|
+
selections: allFields.map(([name, def], index) => {
|
|
1383
|
+
const castedColumnRef = sql`CAST(${eb.ref(`column${index + 1}`)} as ${sql.raw(this.dialect.getFieldSqlType(def))})`.as(name);
|
|
1384
|
+
return SelectionNode2.create(castedColumnRef.toOperationNode());
|
|
1385
|
+
})
|
|
1386
|
+
};
|
|
1387
|
+
const filter = this.buildPolicyFilter(model, void 0, "create");
|
|
1388
|
+
const preCreateCheck = {
|
|
1389
|
+
kind: "SelectQueryNode",
|
|
1390
|
+
from: FromNode2.create([
|
|
1391
|
+
AliasNode3.create(constTable, IdentifierNode2.create(model))
|
|
1392
|
+
]),
|
|
1393
|
+
selections: [
|
|
1394
|
+
SelectionNode2.create(AliasNode3.create(BinaryOperationNode3.create(FunctionNode3.create("COUNT", [
|
|
1395
|
+
ValueNode3.createImmediate(1)
|
|
1396
|
+
]), OperatorNode3.create(">"), ValueNode3.createImmediate(0)), IdentifierNode2.create("$condition")))
|
|
1397
|
+
],
|
|
1398
|
+
where: WhereNode2.create(filter)
|
|
1399
|
+
};
|
|
1400
|
+
const result = await proceed(preCreateCheck);
|
|
1401
|
+
if (!result.rows[0]?.$condition) {
|
|
1402
|
+
throw createRejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
unwrapCreateValueRows(node, model, fields, isManyToManyJoinTable) {
|
|
1406
|
+
if (ValuesNode.is(node)) {
|
|
1407
|
+
return node.values.map((v) => this.unwrapCreateValueRow(v.values, model, fields, isManyToManyJoinTable));
|
|
1408
|
+
} else if (PrimitiveValueListNode.is(node)) {
|
|
1409
|
+
return [
|
|
1410
|
+
this.unwrapCreateValueRow(node.values, model, fields, isManyToManyJoinTable)
|
|
1411
|
+
];
|
|
1412
|
+
} else {
|
|
1413
|
+
invariant3(false, `Unexpected node kind: ${node.kind} for unwrapping create values`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
unwrapCreateValueRow(data, model, fields, isImplicitManyToManyJoinTable) {
|
|
1417
|
+
invariant3(data.length === fields.length, "data length must match fields length");
|
|
1418
|
+
const result = [];
|
|
1419
|
+
for (let i = 0; i < data.length; i++) {
|
|
1420
|
+
const item = data[i];
|
|
1421
|
+
if (typeof item === "object" && item && "kind" in item) {
|
|
1422
|
+
const fieldDef = QueryUtils2.requireField(this.client.$schema, model, fields[i]);
|
|
1423
|
+
invariant3(item.kind === "ValueNode", "expecting a ValueNode");
|
|
1424
|
+
result.push({
|
|
1425
|
+
node: ValueNode3.create(this.dialect.transformPrimitive(item.value, fieldDef.type, !!fieldDef.array)),
|
|
1426
|
+
raw: item.value
|
|
1427
|
+
});
|
|
1428
|
+
} else {
|
|
1429
|
+
let value = item;
|
|
1430
|
+
if (!isImplicitManyToManyJoinTable) {
|
|
1431
|
+
const fieldDef = QueryUtils2.requireField(this.client.$schema, model, fields[i]);
|
|
1432
|
+
value = this.dialect.transformPrimitive(item, fieldDef.type, !!fieldDef.array);
|
|
1433
|
+
}
|
|
1434
|
+
if (Array.isArray(value)) {
|
|
1435
|
+
result.push({
|
|
1436
|
+
node: RawNode.createWithSql(this.dialect.buildArrayLiteralSQL(value)),
|
|
1437
|
+
raw: value
|
|
1438
|
+
});
|
|
1439
|
+
} else {
|
|
1440
|
+
result.push({
|
|
1441
|
+
node: ValueNode3.create(value),
|
|
1442
|
+
raw: value
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return result;
|
|
1448
|
+
}
|
|
1449
|
+
tryGetConstantPolicy(model, operation) {
|
|
1450
|
+
const policies = this.getModelPolicies(model, operation);
|
|
1451
|
+
if (!policies.some((p) => p.kind === "allow")) {
|
|
1452
|
+
return false;
|
|
1453
|
+
} else if (
|
|
1454
|
+
// unconditional deny
|
|
1455
|
+
policies.some((p) => p.kind === "deny" && this.isTrueExpr(p.condition))
|
|
1456
|
+
) {
|
|
1457
|
+
return false;
|
|
1458
|
+
} else if (
|
|
1459
|
+
// unconditional allow
|
|
1460
|
+
!policies.some((p) => p.kind === "deny") && policies.some((p) => p.kind === "allow" && this.isTrueExpr(p.condition))
|
|
1461
|
+
) {
|
|
1462
|
+
return true;
|
|
1463
|
+
} else {
|
|
1464
|
+
return void 0;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
isTrueExpr(expr2) {
|
|
1468
|
+
return ExpressionUtils4.isLiteral(expr2) && expr2.value === true;
|
|
1469
|
+
}
|
|
1470
|
+
async processReadBack(node, result, proceed) {
|
|
1471
|
+
if (result.rows.length === 0) {
|
|
1472
|
+
return result;
|
|
1473
|
+
}
|
|
1474
|
+
if (!this.isMutationQueryNode(node) || !node.returning) {
|
|
1475
|
+
return result;
|
|
1476
|
+
}
|
|
1477
|
+
const { mutationModel } = this.getMutationModel(node);
|
|
1478
|
+
const idConditions = this.buildIdConditions(mutationModel, result.rows);
|
|
1479
|
+
const policyFilter = this.buildPolicyFilter(mutationModel, void 0, "read");
|
|
1480
|
+
const select = {
|
|
1481
|
+
kind: "SelectQueryNode",
|
|
1482
|
+
from: FromNode2.create([
|
|
1483
|
+
TableNode3.create(mutationModel)
|
|
1484
|
+
]),
|
|
1485
|
+
where: WhereNode2.create(conjunction(this.dialect, [
|
|
1486
|
+
idConditions,
|
|
1487
|
+
policyFilter
|
|
1488
|
+
])),
|
|
1489
|
+
selections: node.returning.selections
|
|
1490
|
+
};
|
|
1491
|
+
const selectResult = await proceed(select);
|
|
1492
|
+
return selectResult;
|
|
1493
|
+
}
|
|
1494
|
+
buildIdConditions(table, rows) {
|
|
1495
|
+
const idFields = QueryUtils2.requireIdFields(this.client.$schema, table);
|
|
1496
|
+
return disjunction(this.dialect, rows.map((row) => conjunction(this.dialect, idFields.map((field) => BinaryOperationNode3.create(ReferenceNode3.create(ColumnNode3.create(field), TableNode3.create(table)), OperatorNode3.create("="), ValueNode3.create(row[field]))))));
|
|
1497
|
+
}
|
|
1498
|
+
getMutationModel(node) {
|
|
1499
|
+
const r = match3(node).when(InsertQueryNode.is, (node2) => ({
|
|
1500
|
+
mutationModel: getTableName(node2.into),
|
|
1501
|
+
alias: void 0
|
|
1502
|
+
})).when(UpdateQueryNode.is, (node2) => {
|
|
1503
|
+
if (!node2.table) {
|
|
1504
|
+
invariant3(false, "Update query must have a table");
|
|
1505
|
+
}
|
|
1506
|
+
const r2 = this.extractTableName(node2.table);
|
|
1507
|
+
return r2 ? {
|
|
1508
|
+
mutationModel: r2.model,
|
|
1509
|
+
alias: r2.alias
|
|
1510
|
+
} : void 0;
|
|
1511
|
+
}).when(DeleteQueryNode.is, (node2) => {
|
|
1512
|
+
if (node2.from.froms.length !== 1) {
|
|
1513
|
+
throw createUnsupportedError("Only one from table is supported for delete");
|
|
1514
|
+
}
|
|
1515
|
+
const r2 = this.extractTableName(node2.from.froms[0]);
|
|
1516
|
+
return r2 ? {
|
|
1517
|
+
mutationModel: r2.model,
|
|
1518
|
+
alias: r2.alias
|
|
1519
|
+
} : void 0;
|
|
1520
|
+
}).exhaustive();
|
|
1521
|
+
if (!r) {
|
|
1522
|
+
invariant3(false, `Unable to get table name for query node: ${node}`);
|
|
1523
|
+
}
|
|
1524
|
+
return r;
|
|
1525
|
+
}
|
|
1526
|
+
isCrudQueryNode(node) {
|
|
1527
|
+
return SelectQueryNode2.is(node) || InsertQueryNode.is(node) || UpdateQueryNode.is(node) || DeleteQueryNode.is(node);
|
|
1528
|
+
}
|
|
1529
|
+
isMutationQueryNode(node) {
|
|
1530
|
+
return InsertQueryNode.is(node) || UpdateQueryNode.is(node) || DeleteQueryNode.is(node);
|
|
1531
|
+
}
|
|
1532
|
+
buildPolicyFilter(model, alias, operation) {
|
|
1533
|
+
const m2mFilter = this.getModelPolicyFilterForManyToManyJoinTable(model, alias, operation);
|
|
1534
|
+
if (m2mFilter) {
|
|
1535
|
+
return m2mFilter;
|
|
1536
|
+
}
|
|
1537
|
+
const policies = this.getModelPolicies(model, operation);
|
|
1538
|
+
const allows = policies.filter((policy) => policy.kind === "allow").map((policy) => this.compilePolicyCondition(model, alias, operation, policy));
|
|
1539
|
+
const denies = policies.filter((policy) => policy.kind === "deny").map((policy) => this.compilePolicyCondition(model, alias, operation, policy));
|
|
1540
|
+
let combinedPolicy;
|
|
1541
|
+
if (allows.length === 0) {
|
|
1542
|
+
if (operation === "post-update") {
|
|
1543
|
+
combinedPolicy = trueNode(this.dialect);
|
|
1544
|
+
} else {
|
|
1545
|
+
combinedPolicy = falseNode(this.dialect);
|
|
1546
|
+
}
|
|
1547
|
+
} else {
|
|
1548
|
+
combinedPolicy = disjunction(this.dialect, allows);
|
|
1549
|
+
}
|
|
1550
|
+
if (denies.length !== 0) {
|
|
1551
|
+
const combinedDenies = conjunction(this.dialect, denies.map((d) => buildIsFalse(d, this.dialect)));
|
|
1552
|
+
combinedPolicy = conjunction(this.dialect, [
|
|
1553
|
+
combinedPolicy,
|
|
1554
|
+
combinedDenies
|
|
1555
|
+
]);
|
|
1556
|
+
}
|
|
1557
|
+
return combinedPolicy;
|
|
1558
|
+
}
|
|
1559
|
+
extractTableName(node) {
|
|
1560
|
+
if (TableNode3.is(node)) {
|
|
1561
|
+
return {
|
|
1562
|
+
model: node.table.identifier.name
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
if (AliasNode3.is(node)) {
|
|
1566
|
+
const inner = this.extractTableName(node.node);
|
|
1567
|
+
if (!inner) {
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
model: inner.model,
|
|
1572
|
+
alias: IdentifierNode2.is(node.alias) ? node.alias.name : void 0
|
|
1573
|
+
};
|
|
1574
|
+
} else {
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
createPolicyFilterForFrom(node) {
|
|
1579
|
+
if (!node) {
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1582
|
+
return this.createPolicyFilterForTables(node.froms);
|
|
1583
|
+
}
|
|
1584
|
+
createPolicyFilterForTables(tables) {
|
|
1585
|
+
return tables.reduce((acc, table) => {
|
|
1586
|
+
const extractResult = this.extractTableName(table);
|
|
1587
|
+
if (extractResult) {
|
|
1588
|
+
const { model, alias } = extractResult;
|
|
1589
|
+
const filter = this.buildPolicyFilter(model, alias, "read");
|
|
1590
|
+
return acc ? conjunction(this.dialect, [
|
|
1591
|
+
acc,
|
|
1592
|
+
filter
|
|
1593
|
+
]) : filter;
|
|
1594
|
+
}
|
|
1595
|
+
return acc;
|
|
1596
|
+
}, void 0);
|
|
1597
|
+
}
|
|
1598
|
+
compilePolicyCondition(model, alias, operation, policy) {
|
|
1599
|
+
return new ExpressionTransformer(this.client).transform(policy.condition, {
|
|
1600
|
+
modelOrType: model,
|
|
1601
|
+
thisType: model,
|
|
1602
|
+
thisAlias: alias,
|
|
1603
|
+
alias,
|
|
1604
|
+
operation
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
getModelPolicies(model, operation) {
|
|
1608
|
+
const modelDef = QueryUtils2.requireModel(this.client.$schema, model);
|
|
1609
|
+
const result = [];
|
|
1610
|
+
const extractOperations = /* @__PURE__ */ __name((expr2) => {
|
|
1611
|
+
invariant3(ExpressionUtils4.isLiteral(expr2), "expecting a literal");
|
|
1612
|
+
invariant3(typeof expr2.value === "string", "expecting a string literal");
|
|
1613
|
+
return expr2.value.split(",").filter((v) => !!v).map((v) => v.trim());
|
|
1614
|
+
}, "extractOperations");
|
|
1615
|
+
if (modelDef.attributes) {
|
|
1616
|
+
result.push(...modelDef.attributes.filter((attr) => attr.name === "@@allow" || attr.name === "@@deny").map((attr) => ({
|
|
1617
|
+
kind: attr.name === "@@allow" ? "allow" : "deny",
|
|
1618
|
+
operations: extractOperations(attr.args[0].value),
|
|
1619
|
+
condition: attr.args[1].value
|
|
1620
|
+
})).filter((policy) => operation !== "post-update" && policy.operations.includes("all") || policy.operations.includes(operation)));
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
getFieldPolicies(model, field, operation) {
|
|
1625
|
+
const fieldDef = QueryUtils2.requireField(this.client.$schema, model, field);
|
|
1626
|
+
const result = [];
|
|
1627
|
+
const extractOperations = /* @__PURE__ */ __name((expr2) => {
|
|
1628
|
+
invariant3(ExpressionUtils4.isLiteral(expr2), "expecting a literal");
|
|
1629
|
+
invariant3(typeof expr2.value === "string", "expecting a string literal");
|
|
1630
|
+
return expr2.value.split(",").filter((v) => !!v).map((v) => v.trim());
|
|
1631
|
+
}, "extractOperations");
|
|
1632
|
+
if (fieldDef.attributes) {
|
|
1633
|
+
result.push(...fieldDef.attributes.filter((attr) => attr.name === "@allow" || attr.name === "@deny").map((attr) => ({
|
|
1634
|
+
kind: attr.name === "@allow" ? "allow" : "deny",
|
|
1635
|
+
operations: extractOperations(attr.args[0].value),
|
|
1636
|
+
condition: attr.args[1].value
|
|
1637
|
+
})).filter((policy) => policy.operations.includes("all") || policy.operations.includes(operation)));
|
|
1638
|
+
}
|
|
1639
|
+
return result;
|
|
1640
|
+
}
|
|
1641
|
+
resolveManyToManyJoinTable(tableName) {
|
|
1642
|
+
for (const model of Object.values(this.client.$schema.models)) {
|
|
1643
|
+
for (const field of Object.values(model.fields)) {
|
|
1644
|
+
const m2m = QueryUtils2.getManyToManyRelation(this.client.$schema, model.name, field.name);
|
|
1645
|
+
if (m2m?.joinTable === tableName) {
|
|
1646
|
+
const sortedRecord = [
|
|
1647
|
+
{
|
|
1648
|
+
model: model.name,
|
|
1649
|
+
field: field.name
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
model: m2m.otherModel,
|
|
1653
|
+
field: m2m.otherField
|
|
1654
|
+
}
|
|
1655
|
+
].sort(this.manyToManySorter);
|
|
1656
|
+
const firstIdFields = QueryUtils2.requireIdFields(this.client.$schema, sortedRecord[0].model);
|
|
1657
|
+
const secondIdFields = QueryUtils2.requireIdFields(this.client.$schema, sortedRecord[1].model);
|
|
1658
|
+
invariant3(firstIdFields.length === 1 && secondIdFields.length === 1, "only single-field id is supported for implicit many-to-many join table");
|
|
1659
|
+
return {
|
|
1660
|
+
firstModel: sortedRecord[0].model,
|
|
1661
|
+
firstField: sortedRecord[0].field,
|
|
1662
|
+
firstIdField: firstIdFields[0],
|
|
1663
|
+
secondModel: sortedRecord[1].model,
|
|
1664
|
+
secondField: sortedRecord[1].field,
|
|
1665
|
+
secondIdField: secondIdFields[0]
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return void 0;
|
|
1671
|
+
}
|
|
1672
|
+
manyToManySorter(a, b) {
|
|
1673
|
+
return a.model !== b.model ? a.model.localeCompare(b.model) : a.field.localeCompare(b.field);
|
|
1674
|
+
}
|
|
1675
|
+
isManyToManyJoinTable(tableName) {
|
|
1676
|
+
return !!this.resolveManyToManyJoinTable(tableName);
|
|
1677
|
+
}
|
|
1678
|
+
getModelPolicyFilterForManyToManyJoinTable(tableName, alias, operation) {
|
|
1679
|
+
const m2m = this.resolveManyToManyJoinTable(tableName);
|
|
1680
|
+
if (!m2m) {
|
|
1681
|
+
return void 0;
|
|
1682
|
+
}
|
|
1683
|
+
const checkForOperation = operation === "read" ? "read" : "update";
|
|
1684
|
+
const eb = expressionBuilder2();
|
|
1685
|
+
const joinTable = alias ?? tableName;
|
|
1686
|
+
const aQuery = eb.selectFrom(m2m.firstModel).whereRef(`${m2m.firstModel}.${m2m.firstIdField}`, "=", `${joinTable}.A`).select(() => new ExpressionWrapper(this.buildPolicyFilter(m2m.firstModel, void 0, checkForOperation)).as("$conditionA"));
|
|
1687
|
+
const bQuery = eb.selectFrom(m2m.secondModel).whereRef(`${m2m.secondModel}.${m2m.secondIdField}`, "=", `${joinTable}.B`).select(() => new ExpressionWrapper(this.buildPolicyFilter(m2m.secondModel, void 0, checkForOperation)).as("$conditionB"));
|
|
1688
|
+
return eb.and([
|
|
1689
|
+
aQuery,
|
|
1690
|
+
bQuery
|
|
1691
|
+
]).toOperationNode();
|
|
1692
|
+
}
|
|
1693
|
+
tryRejectNonexistentModel(model) {
|
|
1694
|
+
if (!QueryUtils2.hasModel(this.client.$schema, model) && !this.isManyToManyJoinTable(model)) {
|
|
1695
|
+
throw createRejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
tryRejectNonexistingTables(tables) {
|
|
1699
|
+
for (const table of tables) {
|
|
1700
|
+
const extractResult = this.extractTableName(table);
|
|
1701
|
+
if (extractResult) {
|
|
1702
|
+
this.tryRejectNonexistentModel(extractResult.model);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
// correction to kysely mutation result may be needed because we might have added
|
|
1707
|
+
// returning clause to the query and caused changes to the result shape
|
|
1708
|
+
postProcessMutationResult(result, node) {
|
|
1709
|
+
if (node.returning) {
|
|
1710
|
+
return result;
|
|
1711
|
+
} else {
|
|
1712
|
+
return {
|
|
1713
|
+
...result,
|
|
1714
|
+
rows: [],
|
|
1715
|
+
numAffectedRows: result.numAffectedRows ?? BigInt(result.rows.length)
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// src/functions.ts
|
|
1722
|
+
var check = /* @__PURE__ */ __name((eb, args, { client, model, modelAlias, operation }) => {
|
|
1723
|
+
invariant4(args.length === 1 || args.length === 2, '"check" function requires 1 or 2 arguments');
|
|
1724
|
+
const arg1Node = args[0].toOperationNode();
|
|
1725
|
+
const arg2Node = args.length === 2 ? args[1].toOperationNode() : void 0;
|
|
1726
|
+
if (arg2Node) {
|
|
1727
|
+
invariant4(ValueNode4.is(arg2Node) && typeof arg2Node.value === "string", '"operation" parameter must be a string literal when provided');
|
|
1728
|
+
invariant4(CRUD.includes(arg2Node.value), '"operation" parameter must be one of "create", "read", "update", "delete"');
|
|
1729
|
+
}
|
|
1730
|
+
const fieldName = QueryUtils3.extractFieldName(arg1Node);
|
|
1731
|
+
invariant4(fieldName, 'Failed to extract field name from the first argument of "check" function');
|
|
1732
|
+
const fieldDef = QueryUtils3.requireField(client.$schema, model, fieldName);
|
|
1733
|
+
invariant4(fieldDef.relation, `Field "${fieldName}" is not a relation field in model "${model}"`);
|
|
1734
|
+
invariant4(!fieldDef.array, `Field "${fieldName}" is a to-many relation, which is not supported by "check"`);
|
|
1735
|
+
const relationModel = fieldDef.type;
|
|
1736
|
+
const joinConditions = [];
|
|
1737
|
+
const fkInfo = QueryUtils3.getRelationForeignKeyFieldPairs(client.$schema, model, fieldName);
|
|
1738
|
+
const idFields = QueryUtils3.requireIdFields(client.$schema, model);
|
|
1739
|
+
const buildBaseSelect = /* @__PURE__ */ __name((baseModel, field) => {
|
|
1740
|
+
return eb.selectFrom(baseModel).select(field).where(eb.and(idFields.map((idField) => eb(eb.ref(`${fieldDef.originModel}.${idField}`), "=", eb.ref(`${modelAlias}.${idField}`)))));
|
|
1741
|
+
}, "buildBaseSelect");
|
|
1742
|
+
if (fkInfo.ownedByModel) {
|
|
1743
|
+
joinConditions.push(...fkInfo.keyPairs.map(({ fk, pk }) => {
|
|
1744
|
+
let fkRef;
|
|
1745
|
+
if (fieldDef.originModel && fieldDef.originModel !== model) {
|
|
1746
|
+
fkRef = buildBaseSelect(fieldDef.originModel, fk);
|
|
1747
|
+
} else {
|
|
1748
|
+
fkRef = eb.ref(`${modelAlias}.${fk}`);
|
|
1749
|
+
}
|
|
1750
|
+
return eb(fkRef, "=", eb.ref(`${relationModel}.${pk}`));
|
|
1751
|
+
}));
|
|
1752
|
+
} else {
|
|
1753
|
+
joinConditions.push(...fkInfo.keyPairs.map(({ fk, pk }) => {
|
|
1754
|
+
let pkRef;
|
|
1755
|
+
if (fieldDef.originModel && fieldDef.originModel !== model) {
|
|
1756
|
+
pkRef = buildBaseSelect(fieldDef.originModel, pk);
|
|
1757
|
+
} else {
|
|
1758
|
+
pkRef = eb.ref(`${modelAlias}.${pk}`);
|
|
1759
|
+
}
|
|
1760
|
+
return eb(pkRef, "=", eb.ref(`${relationModel}.${fk}`));
|
|
1761
|
+
}));
|
|
1762
|
+
}
|
|
1763
|
+
const joinCondition = joinConditions.length === 1 ? joinConditions[0] : eb.and(joinConditions);
|
|
1764
|
+
const policyHandler = new PolicyHandler(client);
|
|
1765
|
+
const op = arg2Node ? arg2Node.value : operation;
|
|
1766
|
+
const policyCondition = policyHandler.buildPolicyFilter(relationModel, void 0, op);
|
|
1767
|
+
const result = eb.selectFrom(relationModel).where(joinCondition).select(new ExpressionWrapper2(policyCondition).as("$condition"));
|
|
1768
|
+
return result;
|
|
1769
|
+
}, "check");
|
|
1770
|
+
|
|
1771
|
+
// src/plugin.ts
|
|
1772
|
+
var PolicyPlugin = class {
|
|
1773
|
+
static {
|
|
1774
|
+
__name(this, "PolicyPlugin");
|
|
1775
|
+
}
|
|
1776
|
+
get id() {
|
|
1777
|
+
return "policy";
|
|
1778
|
+
}
|
|
1779
|
+
get name() {
|
|
1780
|
+
return "Access Policy";
|
|
1781
|
+
}
|
|
1782
|
+
get description() {
|
|
1783
|
+
return "Enforces access policies defined in the schema.";
|
|
1784
|
+
}
|
|
1785
|
+
get functions() {
|
|
1786
|
+
return {
|
|
1787
|
+
check
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
onKyselyQuery({ query, client, proceed }) {
|
|
1791
|
+
const handler = new PolicyHandler(client);
|
|
1792
|
+
return handler.handle(query, proceed);
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
export {
|
|
1796
|
+
PolicyPlugin
|
|
1797
|
+
};
|
|
1798
|
+
//# sourceMappingURL=index.mjs.map
|