@cerbos/orm-prisma 2.0.1 → 3.1.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/README.md +59 -2
- package/lib/{types/index.d.ts → index.d.ts} +3 -9
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +629 -0
- package/lib/index.js.map +1 -0
- package/package.json +47 -32
- package/src/generated/prisma/browser.ts +59 -0
- package/src/generated/prisma/client.ts +79 -0
- package/src/generated/prisma/commonInputTypes.ts +248 -0
- package/src/generated/prisma/enums.ts +15 -0
- package/src/generated/prisma/internal/class.ts +260 -0
- package/src/generated/prisma/internal/prismaNamespace.ts +1333 -0
- package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +168 -0
- package/src/generated/prisma/models/Category.ts +1407 -0
- package/src/generated/prisma/models/Label.ts +1271 -0
- package/src/generated/prisma/models/NestedResource.ts +1557 -0
- package/src/generated/prisma/models/NextLevelNestedResource.ts +1332 -0
- package/src/generated/prisma/models/Resource.ts +2131 -0
- package/src/generated/prisma/models/SubCategory.ts +1407 -0
- package/src/generated/prisma/models/Tag.ts +1271 -0
- package/src/generated/prisma/models/User.ts +1498 -0
- package/src/generated/prisma/models.ts +19 -0
- package/src/generated/prisma-v6/client.d.ts +1 -0
- package/src/generated/prisma-v6/default.d.ts +1 -0
- package/src/generated/prisma-v6/edge.d.ts +1 -0
- package/src/generated/prisma-v6/index.d.ts +13639 -0
- package/src/generated/prisma-v6/runtime/index-browser.d.ts +370 -0
- package/src/generated/prisma-v6/runtime/library.d.ts +3982 -0
- package/src/generated/prisma-v6/wasm.d.ts +1 -0
- package/src/index.test.ts +4869 -0
- package/src/index.ts +954 -0
- package/src/test-setup.v6.ts +5 -0
- package/src/test-setup.v7.ts +7 -0
- package/lib/cjs/index.js +0 -517
- package/lib/cjs/index.js.map +0 -1
- package/lib/esm/index.js +0 -513
- package/lib/esm/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
|
@@ -0,0 +1,4869 @@
|
|
|
1
|
+
import { queryPlanToPrisma, PlanKind, QueryPlanToPrismaResult } from ".";
|
|
2
|
+
import {
|
|
3
|
+
beforeAll,
|
|
4
|
+
beforeEach,
|
|
5
|
+
afterEach,
|
|
6
|
+
describe,
|
|
7
|
+
test,
|
|
8
|
+
expect,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
PlanExpression,
|
|
12
|
+
PlanExpressionOperand,
|
|
13
|
+
PlanResourcesConditionalResponse,
|
|
14
|
+
PlanResourcesResponse,
|
|
15
|
+
} from "@cerbos/core";
|
|
16
|
+
import { GRPC as Cerbos } from "@cerbos/grpc";
|
|
17
|
+
import { prisma, Prisma } from "./test-setup";
|
|
18
|
+
|
|
19
|
+
const cerbos = new Cerbos("127.0.0.1:3593", { tls: false });
|
|
20
|
+
|
|
21
|
+
function createConditionalPlan(
|
|
22
|
+
condition: PlanExpressionOperand
|
|
23
|
+
): PlanResourcesConditionalResponse {
|
|
24
|
+
return {
|
|
25
|
+
kind: PlanKind.CONDITIONAL,
|
|
26
|
+
condition,
|
|
27
|
+
cerbosCallId: "",
|
|
28
|
+
requestId: "",
|
|
29
|
+
validationErrors: [],
|
|
30
|
+
metadata: undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getExpressionOperand(
|
|
35
|
+
expression: PlanExpression,
|
|
36
|
+
index: number
|
|
37
|
+
): PlanExpressionOperand {
|
|
38
|
+
const operand = expression.operands[index];
|
|
39
|
+
if (!operand) {
|
|
40
|
+
throw new Error(`Missing operand at index ${index}`);
|
|
41
|
+
}
|
|
42
|
+
return operand;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const fixtureUsers: Prisma.UserCreateInput[] = [
|
|
46
|
+
{
|
|
47
|
+
id: "user1",
|
|
48
|
+
aBool: true,
|
|
49
|
+
aNumber: 1,
|
|
50
|
+
aString: "string",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "user2",
|
|
54
|
+
aBool: true,
|
|
55
|
+
aNumber: 2,
|
|
56
|
+
aString: "string",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const fixtureNextLevelResources: Prisma.NextLevelNestedResourceCreateInput[] = [
|
|
61
|
+
{
|
|
62
|
+
id: "nextLevel1",
|
|
63
|
+
aBool: true,
|
|
64
|
+
aNumber: 1,
|
|
65
|
+
aString: "string",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "nextLevel2",
|
|
69
|
+
aBool: false,
|
|
70
|
+
aNumber: 1,
|
|
71
|
+
aString: "string",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "nextLevel3",
|
|
75
|
+
aBool: true,
|
|
76
|
+
aNumber: 1,
|
|
77
|
+
aString: "string",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const fixtureTags: Prisma.TagCreateInput[] = [
|
|
82
|
+
{
|
|
83
|
+
id: "tag1",
|
|
84
|
+
name: "public",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "tag2",
|
|
88
|
+
name: "private",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "tag3",
|
|
92
|
+
name: "draft",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const fixtureNestedResources: Prisma.NestedResourceCreateInput[] = [
|
|
97
|
+
{
|
|
98
|
+
id: "nested1",
|
|
99
|
+
aBool: true,
|
|
100
|
+
aNumber: 1,
|
|
101
|
+
aString: "string",
|
|
102
|
+
nextlevel: {
|
|
103
|
+
connect: {
|
|
104
|
+
id: "nextLevel1",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "nested2",
|
|
110
|
+
aBool: false,
|
|
111
|
+
aNumber: 1,
|
|
112
|
+
aString: "string",
|
|
113
|
+
nextlevel: {
|
|
114
|
+
connect: {
|
|
115
|
+
id: "nextLevel2",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "nested3",
|
|
121
|
+
aBool: true,
|
|
122
|
+
aNumber: 1,
|
|
123
|
+
aString: "string",
|
|
124
|
+
nextlevel: {
|
|
125
|
+
connect: {
|
|
126
|
+
id: "nextLevel3",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const fixtureLabels: Prisma.LabelCreateInput[] = [
|
|
133
|
+
{ id: "label1", name: "important" },
|
|
134
|
+
{ id: "label2", name: "archived" },
|
|
135
|
+
{ id: "label3", name: "flagged" },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const fixtureSubCategories: Prisma.SubCategoryCreateInput[] = [
|
|
139
|
+
{
|
|
140
|
+
id: "sub1",
|
|
141
|
+
name: "finance",
|
|
142
|
+
labels: {
|
|
143
|
+
connect: [{ id: "label1" }, { id: "label2" }],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "sub2",
|
|
148
|
+
name: "tech",
|
|
149
|
+
labels: {
|
|
150
|
+
connect: [{ id: "label2" }, { id: "label3" }],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const fixtureCategories: Prisma.CategoryCreateInput[] = [
|
|
156
|
+
{
|
|
157
|
+
id: "cat1",
|
|
158
|
+
name: "business",
|
|
159
|
+
subCategories: {
|
|
160
|
+
connect: [{ id: "sub1" }],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "cat2",
|
|
165
|
+
name: "development",
|
|
166
|
+
subCategories: {
|
|
167
|
+
connect: [{ id: "sub2" }],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const fixtureResources: Prisma.ResourceCreateInput[] = [
|
|
173
|
+
{
|
|
174
|
+
id: "resource1",
|
|
175
|
+
aBool: true,
|
|
176
|
+
aNumber: 1,
|
|
177
|
+
aString: "string",
|
|
178
|
+
aOptionalString: "optionalString",
|
|
179
|
+
createdBy: {
|
|
180
|
+
connect: {
|
|
181
|
+
id: "user1",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
ownedBy: {
|
|
185
|
+
connect: [
|
|
186
|
+
{
|
|
187
|
+
id: "user1",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
nested: {
|
|
192
|
+
connect: {
|
|
193
|
+
id: "nested1",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
tags: {
|
|
197
|
+
connect: [
|
|
198
|
+
{ id: "tag1" }, // public
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
categories: {
|
|
202
|
+
connect: [{ id: "cat1" }],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "resource2",
|
|
207
|
+
aBool: false,
|
|
208
|
+
aNumber: 2,
|
|
209
|
+
aString: "string2",
|
|
210
|
+
createdBy: {
|
|
211
|
+
connect: {
|
|
212
|
+
id: "user2",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
ownedBy: {
|
|
216
|
+
connect: [
|
|
217
|
+
{
|
|
218
|
+
id: "user2",
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
nested: {
|
|
223
|
+
connect: {
|
|
224
|
+
id: "nested3",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
tags: {
|
|
228
|
+
connect: [
|
|
229
|
+
{ id: "tag2" }, // private
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
categories: {
|
|
233
|
+
connect: [{ id: "cat2" }],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "resource3",
|
|
238
|
+
aBool: false,
|
|
239
|
+
aNumber: 3,
|
|
240
|
+
aString: "string3",
|
|
241
|
+
createdBy: {
|
|
242
|
+
connect: {
|
|
243
|
+
id: "user1",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
ownedBy: {
|
|
247
|
+
connect: [
|
|
248
|
+
{
|
|
249
|
+
id: "user1",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "user2",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
nested: {
|
|
257
|
+
connect: {
|
|
258
|
+
id: "nested3",
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
tags: {
|
|
262
|
+
connect: [
|
|
263
|
+
{ id: "tag1" }, // public
|
|
264
|
+
{ id: "tag3" }, // draft
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
categories: {
|
|
268
|
+
connect: [{ id: "cat1" }, { id: "cat2" }],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
beforeAll(async () => {
|
|
274
|
+
await prisma.resource.deleteMany();
|
|
275
|
+
await prisma.nextLevelNestedResource.deleteMany();
|
|
276
|
+
await prisma.nestedResource.deleteMany();
|
|
277
|
+
await prisma.tag.deleteMany();
|
|
278
|
+
await prisma.user.deleteMany();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
beforeEach(async () => {
|
|
282
|
+
for (const tag of fixtureTags) {
|
|
283
|
+
await prisma.tag.create({ data: tag });
|
|
284
|
+
}
|
|
285
|
+
for (const user of fixtureUsers) {
|
|
286
|
+
await prisma.user.create({ data: user });
|
|
287
|
+
}
|
|
288
|
+
for (const resource of fixtureNextLevelResources) {
|
|
289
|
+
await prisma.nextLevelNestedResource.create({ data: resource });
|
|
290
|
+
}
|
|
291
|
+
for (const resource of fixtureNestedResources) {
|
|
292
|
+
await prisma.nestedResource.create({ data: resource });
|
|
293
|
+
}
|
|
294
|
+
for (const label of fixtureLabels) {
|
|
295
|
+
await prisma.label.create({ data: label });
|
|
296
|
+
}
|
|
297
|
+
for (const subCategory of fixtureSubCategories) {
|
|
298
|
+
await prisma.subCategory.create({ data: subCategory });
|
|
299
|
+
}
|
|
300
|
+
for (const category of fixtureCategories) {
|
|
301
|
+
await prisma.category.create({ data: category });
|
|
302
|
+
}
|
|
303
|
+
for (const resource of fixtureResources) {
|
|
304
|
+
await prisma.resource.create({ data: resource });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterEach(async () => {
|
|
309
|
+
await prisma.resource.deleteMany();
|
|
310
|
+
await prisma.nestedResource.deleteMany();
|
|
311
|
+
await prisma.nextLevelNestedResource.deleteMany();
|
|
312
|
+
await prisma.tag.deleteMany();
|
|
313
|
+
await prisma.user.deleteMany();
|
|
314
|
+
await prisma.category.deleteMany();
|
|
315
|
+
await prisma.subCategory.deleteMany();
|
|
316
|
+
await prisma.label.deleteMany();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Core Functionality
|
|
320
|
+
describe("Basic Plan Types", () => {
|
|
321
|
+
test("always allowed", async () => {
|
|
322
|
+
const queryPlan = await cerbos.planResources({
|
|
323
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
324
|
+
resource: { kind: "resource" },
|
|
325
|
+
action: "always-allow",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(queryPlan.kind).toEqual(PlanKind.ALWAYS_ALLOWED);
|
|
329
|
+
|
|
330
|
+
const result = queryPlanToPrisma({
|
|
331
|
+
queryPlan,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(result).toStrictEqual({
|
|
335
|
+
kind: PlanKind.ALWAYS_ALLOWED,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const query = await prisma.resource.findMany({});
|
|
339
|
+
expect(query.length).toEqual(fixtureResources.length);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("always denied", async () => {
|
|
343
|
+
const queryPlan = await cerbos.planResources({
|
|
344
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
345
|
+
resource: { kind: "resource" },
|
|
346
|
+
action: "always-deny",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(queryPlan.kind).toEqual(PlanKind.ALWAYS_DENIED);
|
|
350
|
+
|
|
351
|
+
const result = queryPlanToPrisma({
|
|
352
|
+
queryPlan,
|
|
353
|
+
mapper: {},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result).toEqual({
|
|
357
|
+
kind: PlanKind.ALWAYS_DENIED,
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Field Operations
|
|
363
|
+
describe("Field Operations", () => {
|
|
364
|
+
describe("Basic Field Tests", () => {
|
|
365
|
+
test("conditional - eq", async () => {
|
|
366
|
+
const queryPlan = await cerbos.planResources({
|
|
367
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
368
|
+
resource: { kind: "resource" },
|
|
369
|
+
action: "equal",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
373
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
374
|
+
{
|
|
375
|
+
operator: "eq",
|
|
376
|
+
operands: [{ name: "request.resource.attr.aBool" }, { value: true }],
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const result = queryPlanToPrisma({
|
|
381
|
+
queryPlan,
|
|
382
|
+
mapper: {
|
|
383
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(result).toStrictEqual({
|
|
388
|
+
kind: PlanKind.CONDITIONAL,
|
|
389
|
+
filters: { aBool: { equals: true } },
|
|
390
|
+
});
|
|
391
|
+
const query = await prisma.resource.findMany({
|
|
392
|
+
where:
|
|
393
|
+
result.kind === PlanKind.CONDITIONAL ? { ...result.filters } : {},
|
|
394
|
+
});
|
|
395
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
396
|
+
fixtureResources.filter((a) => a.aBool).map((r) => r.id)
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("conditional - eq - inverted order", async () => {
|
|
401
|
+
const queryPlan = await cerbos.planResources({
|
|
402
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
403
|
+
resource: { kind: "resource" },
|
|
404
|
+
action: "equal",
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
408
|
+
|
|
409
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
410
|
+
{
|
|
411
|
+
operator: "eq",
|
|
412
|
+
operands: [{ name: "request.resource.attr.aBool" }, { value: true }],
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const typeQp = queryPlan as PlanResourcesConditionalResponse;
|
|
417
|
+
|
|
418
|
+
const invertedQueryPlan: PlanResourcesConditionalResponse = {
|
|
419
|
+
...typeQp,
|
|
420
|
+
condition: {
|
|
421
|
+
...typeQp.condition,
|
|
422
|
+
operands: [
|
|
423
|
+
getExpressionOperand(typeQp.condition as PlanExpression, 1),
|
|
424
|
+
getExpressionOperand(typeQp.condition as PlanExpression, 0),
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const result = queryPlanToPrisma({
|
|
430
|
+
queryPlan: invertedQueryPlan,
|
|
431
|
+
mapper: {
|
|
432
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result).toStrictEqual({
|
|
437
|
+
kind: PlanKind.CONDITIONAL,
|
|
438
|
+
filters: { aBool: { equals: true } },
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
442
|
+
throw new Error("Expected CONDITIONAL result");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const query = await prisma.resource.findMany({
|
|
446
|
+
where: { ...result.filters },
|
|
447
|
+
});
|
|
448
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
449
|
+
fixtureResources.filter((a) => a.aBool).map((r) => r.id)
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("conditional - ne", async () => {
|
|
454
|
+
const queryPlan = await cerbos.planResources({
|
|
455
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
456
|
+
resource: { kind: "resource" },
|
|
457
|
+
action: "ne",
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
461
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
462
|
+
{
|
|
463
|
+
operator: "ne",
|
|
464
|
+
operands: [
|
|
465
|
+
{ name: "request.resource.attr.aString" },
|
|
466
|
+
{ value: "string" },
|
|
467
|
+
],
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const result = queryPlanToPrisma({
|
|
472
|
+
queryPlan,
|
|
473
|
+
mapper: {
|
|
474
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(result).toStrictEqual({
|
|
479
|
+
kind: PlanKind.CONDITIONAL,
|
|
480
|
+
filters: { aString: { not: "string" } },
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
484
|
+
throw new Error("Expected CONDITIONAL result");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const query = await prisma.resource.findMany({
|
|
488
|
+
where: { ...result.filters },
|
|
489
|
+
});
|
|
490
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
491
|
+
fixtureResources.filter((a) => a.aString != "string").map((r) => r.id)
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("conditional - explicit-deny", async () => {
|
|
496
|
+
const queryPlan = await cerbos.planResources({
|
|
497
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
498
|
+
resource: { kind: "resource" },
|
|
499
|
+
action: "explicit-deny",
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
503
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
504
|
+
{
|
|
505
|
+
operator: "not",
|
|
506
|
+
operands: [
|
|
507
|
+
{
|
|
508
|
+
operator: "eq",
|
|
509
|
+
operands: [
|
|
510
|
+
{ name: "request.resource.attr.aBool" },
|
|
511
|
+
{ value: true },
|
|
512
|
+
],
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const result = queryPlanToPrisma({
|
|
519
|
+
queryPlan,
|
|
520
|
+
mapper: {
|
|
521
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(result).toStrictEqual({
|
|
526
|
+
kind: PlanKind.CONDITIONAL,
|
|
527
|
+
filters: { NOT: { aBool: { equals: true } } },
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
531
|
+
throw new Error("Expected CONDITIONAL result");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const query = await prisma.resource.findMany({
|
|
535
|
+
where: { ...result.filters },
|
|
536
|
+
});
|
|
537
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
538
|
+
fixtureResources.filter((a) => !a.aBool).map((r) => r.id)
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe("Comparison Tests", () => {
|
|
544
|
+
test("conditional - gt", async () => {
|
|
545
|
+
const queryPlan = await cerbos.planResources({
|
|
546
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
547
|
+
resource: { kind: "resource" },
|
|
548
|
+
action: "gt",
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
552
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
553
|
+
{
|
|
554
|
+
operator: "gt",
|
|
555
|
+
operands: [{ name: "request.resource.attr.aNumber" }, { value: 1 }],
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const result = queryPlanToPrisma({
|
|
560
|
+
queryPlan,
|
|
561
|
+
mapper: {
|
|
562
|
+
"request.resource.attr.aNumber": { field: "aNumber" },
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
expect(result).toStrictEqual({
|
|
567
|
+
kind: PlanKind.CONDITIONAL,
|
|
568
|
+
filters: {
|
|
569
|
+
aNumber: { gt: 1 },
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
574
|
+
throw new Error("Expected CONDITIONAL result");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const query = await prisma.resource.findMany({
|
|
578
|
+
where: { ...result.filters },
|
|
579
|
+
});
|
|
580
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
581
|
+
fixtureResources
|
|
582
|
+
.filter((r) => {
|
|
583
|
+
return r.aNumber > 1;
|
|
584
|
+
})
|
|
585
|
+
.map((r) => r.id)
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("conditional - lt", async () => {
|
|
590
|
+
const queryPlan = await cerbos.planResources({
|
|
591
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
592
|
+
resource: { kind: "resource" },
|
|
593
|
+
action: "lt",
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
597
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
598
|
+
{
|
|
599
|
+
operator: "lt",
|
|
600
|
+
operands: [{ name: "request.resource.attr.aNumber" }, { value: 2 }],
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const result = queryPlanToPrisma({
|
|
605
|
+
queryPlan,
|
|
606
|
+
mapper: {
|
|
607
|
+
"request.resource.attr.aNumber": { field: "aNumber" },
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
expect(result).toStrictEqual({
|
|
612
|
+
kind: PlanKind.CONDITIONAL,
|
|
613
|
+
filters: {
|
|
614
|
+
aNumber: { lt: 2 },
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
619
|
+
throw new Error("Expected CONDITIONAL result");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const query = await prisma.resource.findMany({
|
|
623
|
+
where: { ...result.filters },
|
|
624
|
+
});
|
|
625
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
626
|
+
fixtureResources
|
|
627
|
+
.filter((r) => {
|
|
628
|
+
return r.aNumber < 2;
|
|
629
|
+
})
|
|
630
|
+
.map((r) => r.id)
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("conditional - gte", async () => {
|
|
635
|
+
const queryPlan = await cerbos.planResources({
|
|
636
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
637
|
+
resource: { kind: "resource" },
|
|
638
|
+
action: "gte",
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
642
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
643
|
+
{
|
|
644
|
+
operator: "ge",
|
|
645
|
+
operands: [{ name: "request.resource.attr.aNumber" }, { value: 1 }],
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
const result = queryPlanToPrisma({
|
|
650
|
+
queryPlan,
|
|
651
|
+
mapper: {
|
|
652
|
+
"request.resource.attr.aNumber": { field: "aNumber" },
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(result).toStrictEqual({
|
|
657
|
+
kind: PlanKind.CONDITIONAL,
|
|
658
|
+
filters: {
|
|
659
|
+
aNumber: { gte: 1 },
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
664
|
+
throw new Error("Expected CONDITIONAL result");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const query = await prisma.resource.findMany({
|
|
668
|
+
where: { ...result.filters },
|
|
669
|
+
});
|
|
670
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
671
|
+
fixtureResources
|
|
672
|
+
.filter((r) => {
|
|
673
|
+
return r.aNumber >= 1;
|
|
674
|
+
})
|
|
675
|
+
.map((r) => r.id)
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("conditional - lte", async () => {
|
|
680
|
+
const queryPlan = await cerbos.planResources({
|
|
681
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
682
|
+
resource: { kind: "resource" },
|
|
683
|
+
action: "lte",
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
687
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
688
|
+
{
|
|
689
|
+
operator: "le",
|
|
690
|
+
operands: [{ name: "request.resource.attr.aNumber" }, { value: 2 }],
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const result = queryPlanToPrisma({
|
|
695
|
+
queryPlan,
|
|
696
|
+
mapper: {
|
|
697
|
+
"request.resource.attr.aNumber": { field: "aNumber" },
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
expect(result).toStrictEqual({
|
|
702
|
+
kind: PlanKind.CONDITIONAL,
|
|
703
|
+
filters: {
|
|
704
|
+
aNumber: { lte: 2 },
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
709
|
+
throw new Error("Expected CONDITIONAL result");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const query = await prisma.resource.findMany({
|
|
713
|
+
where: { ...result.filters },
|
|
714
|
+
});
|
|
715
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
716
|
+
fixtureResources
|
|
717
|
+
.filter((r) => {
|
|
718
|
+
return r.aNumber <= 2;
|
|
719
|
+
})
|
|
720
|
+
.map((r) => r.id)
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
describe("String Tests", () => {
|
|
726
|
+
test("conditional - contains", async () => {
|
|
727
|
+
const queryPlan = await cerbos.planResources({
|
|
728
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
729
|
+
resource: { kind: "resource" },
|
|
730
|
+
action: "contains",
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
734
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
735
|
+
{
|
|
736
|
+
operands: [
|
|
737
|
+
{
|
|
738
|
+
name: "request.resource.attr.aString",
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
value: "str",
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
operator: "contains",
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
const result = queryPlanToPrisma({
|
|
749
|
+
queryPlan,
|
|
750
|
+
mapper: {
|
|
751
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
expect(result).toStrictEqual({
|
|
756
|
+
kind: PlanKind.CONDITIONAL,
|
|
757
|
+
filters: { aString: { contains: "str" } },
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
761
|
+
throw new Error("Expected CONDITIONAL result");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const query = await prisma.resource.findMany({
|
|
765
|
+
where: { ...result.filters },
|
|
766
|
+
});
|
|
767
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
768
|
+
fixtureResources
|
|
769
|
+
.filter((a) => a.aString.includes("str"))
|
|
770
|
+
.map((r) => r.id)
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("conditional - startsWith", async () => {
|
|
775
|
+
const queryPlan = await cerbos.planResources({
|
|
776
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
777
|
+
resource: { kind: "resource" },
|
|
778
|
+
action: "starts-with",
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
782
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
783
|
+
{
|
|
784
|
+
operands: [
|
|
785
|
+
{
|
|
786
|
+
name: "request.resource.attr.aString",
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
value: "str",
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
operator: "startsWith",
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
const result = queryPlanToPrisma({
|
|
797
|
+
queryPlan,
|
|
798
|
+
mapper: {
|
|
799
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
expect(result).toStrictEqual({
|
|
804
|
+
kind: PlanKind.CONDITIONAL,
|
|
805
|
+
filters: { aString: { startsWith: "str" } },
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
809
|
+
throw new Error("Expected CONDITIONAL result");
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const query = await prisma.resource.findMany({
|
|
813
|
+
where: { ...result.filters },
|
|
814
|
+
});
|
|
815
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
816
|
+
fixtureResources
|
|
817
|
+
.filter((a) => a.aString.startsWith("str"))
|
|
818
|
+
.map((r) => r.id)
|
|
819
|
+
);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("conditional - endsWith", async () => {
|
|
823
|
+
const queryPlan = await cerbos.planResources({
|
|
824
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
825
|
+
resource: { kind: "resource" },
|
|
826
|
+
action: "ends-with",
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
830
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
831
|
+
{
|
|
832
|
+
operands: [
|
|
833
|
+
{
|
|
834
|
+
name: "request.resource.attr.aString",
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
value: "ing",
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
operator: "endsWith",
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
const result = queryPlanToPrisma({
|
|
845
|
+
queryPlan,
|
|
846
|
+
mapper: {
|
|
847
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
expect(result).toStrictEqual({
|
|
852
|
+
kind: PlanKind.CONDITIONAL,
|
|
853
|
+
filters: { aString: { endsWith: "ing" } },
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
857
|
+
throw new Error("Expected CONDITIONAL result");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const query = await prisma.resource.findMany({
|
|
861
|
+
where: { ...result.filters },
|
|
862
|
+
});
|
|
863
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
864
|
+
fixtureResources
|
|
865
|
+
.filter((a) => a.aString.endsWith("ing"))
|
|
866
|
+
.map((r) => r.id)
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("conditional - isSet", async () => {
|
|
871
|
+
const queryPlan = await cerbos.planResources({
|
|
872
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
873
|
+
resource: { kind: "resource" },
|
|
874
|
+
action: "is-set",
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
878
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
879
|
+
{
|
|
880
|
+
operands: [
|
|
881
|
+
{
|
|
882
|
+
name: "request.resource.attr.aOptionalString",
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
value: null,
|
|
886
|
+
},
|
|
887
|
+
],
|
|
888
|
+
operator: "ne",
|
|
889
|
+
}
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const result = queryPlanToPrisma({
|
|
893
|
+
queryPlan,
|
|
894
|
+
mapper: {
|
|
895
|
+
"request.resource.attr.aOptionalString": { field: "aOptionalString" },
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
expect(result).toStrictEqual({
|
|
900
|
+
kind: PlanKind.CONDITIONAL,
|
|
901
|
+
filters: { aOptionalString: { not: null } },
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
905
|
+
throw new Error("Expected CONDITIONAL result");
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const query = await prisma.resource.findMany({
|
|
909
|
+
where: { ...result.filters },
|
|
910
|
+
});
|
|
911
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
912
|
+
fixtureResources.filter((a) => a.aOptionalString).map((r) => r.id)
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// Collection Operations
|
|
919
|
+
describe("Collection Operations", () => {
|
|
920
|
+
describe("Basic Collections", () => {
|
|
921
|
+
test("conditional - in", async () => {
|
|
922
|
+
const queryPlan = await cerbos.planResources({
|
|
923
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
924
|
+
resource: { kind: "resource" },
|
|
925
|
+
action: "in",
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
929
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
930
|
+
{
|
|
931
|
+
operator: "in",
|
|
932
|
+
operands: [
|
|
933
|
+
{ name: "request.resource.attr.aString" },
|
|
934
|
+
{ value: ["string", "anotherString"] },
|
|
935
|
+
],
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
const result = queryPlanToPrisma({
|
|
940
|
+
queryPlan,
|
|
941
|
+
mapper: {
|
|
942
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
expect(result).toStrictEqual({
|
|
947
|
+
kind: PlanKind.CONDITIONAL,
|
|
948
|
+
filters: {
|
|
949
|
+
aString: { in: ["string", "anotherString"] },
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
954
|
+
throw new Error("Expected CONDITIONAL result");
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const query = await prisma.resource.findMany({
|
|
958
|
+
where: { ...result.filters },
|
|
959
|
+
});
|
|
960
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
961
|
+
fixtureResources
|
|
962
|
+
.filter((r) => {
|
|
963
|
+
return ["string", "anotherString"].includes(r.aString);
|
|
964
|
+
})
|
|
965
|
+
.map((r) => r.id)
|
|
966
|
+
);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test("conditional - in - scalar value", async () => {
|
|
970
|
+
const queryPlan = createConditionalPlan({
|
|
971
|
+
operator: "in",
|
|
972
|
+
operands: [
|
|
973
|
+
{ name: "request.resource.attr.aString" },
|
|
974
|
+
{ value: "string" },
|
|
975
|
+
],
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
const result = queryPlanToPrisma({
|
|
979
|
+
queryPlan,
|
|
980
|
+
mapper: {
|
|
981
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
expect(result).toStrictEqual({
|
|
986
|
+
kind: PlanKind.CONDITIONAL,
|
|
987
|
+
filters: { aString: "string" },
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
991
|
+
throw new Error("Expected CONDITIONAL result");
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const query = await prisma.resource.findMany({
|
|
995
|
+
where: { ...result.filters },
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
expect(query.map((r) => r.id).sort()).toEqual(
|
|
999
|
+
fixtureResources
|
|
1000
|
+
.filter((r) => r.aString === "string")
|
|
1001
|
+
.map((r) => r.id)
|
|
1002
|
+
.sort()
|
|
1003
|
+
);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
test("conditional - relation in", async () => {
|
|
1007
|
+
const queryPlan = createConditionalPlan({
|
|
1008
|
+
operator: "in",
|
|
1009
|
+
operands: [
|
|
1010
|
+
{ name: "request.resource.attr.categories.name" },
|
|
1011
|
+
{ value: ["business"] },
|
|
1012
|
+
],
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
const result = queryPlanToPrisma({
|
|
1016
|
+
queryPlan,
|
|
1017
|
+
mapper: {
|
|
1018
|
+
"request.resource.attr.categories": {
|
|
1019
|
+
relation: {
|
|
1020
|
+
name: "categories",
|
|
1021
|
+
type: "many",
|
|
1022
|
+
fields: {
|
|
1023
|
+
name: { field: "name" },
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
expect(result).toStrictEqual({
|
|
1031
|
+
kind: PlanKind.CONDITIONAL,
|
|
1032
|
+
filters: {
|
|
1033
|
+
categories: {
|
|
1034
|
+
some: {
|
|
1035
|
+
name: "business",
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1042
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const query = await prisma.resource.findMany({
|
|
1046
|
+
where: { ...result.filters },
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
expect(query.map((r) => r.id).sort()).toEqual(
|
|
1050
|
+
fixtureResources
|
|
1051
|
+
.filter((resource) => {
|
|
1052
|
+
const categoryRefs =
|
|
1053
|
+
(resource.categories?.connect as Prisma.CategoryWhereUniqueInput[]) ??
|
|
1054
|
+
[];
|
|
1055
|
+
return categoryRefs.some((categoryRef) => {
|
|
1056
|
+
const category = fixtureCategories.find(
|
|
1057
|
+
(fc) => fc.id === categoryRef.id
|
|
1058
|
+
);
|
|
1059
|
+
return category?.name === "business";
|
|
1060
|
+
});
|
|
1061
|
+
})
|
|
1062
|
+
.map((r) => r.id)
|
|
1063
|
+
.sort()
|
|
1064
|
+
);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
test("conditional - relation in multiple values", async () => {
|
|
1068
|
+
const queryPlan = createConditionalPlan({
|
|
1069
|
+
operator: "in",
|
|
1070
|
+
operands: [
|
|
1071
|
+
{ name: "request.resource.attr.categories.name" },
|
|
1072
|
+
{ value: ["business", "development"] },
|
|
1073
|
+
],
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
const result = queryPlanToPrisma({
|
|
1077
|
+
queryPlan,
|
|
1078
|
+
mapper: {
|
|
1079
|
+
"request.resource.attr.categories": {
|
|
1080
|
+
relation: {
|
|
1081
|
+
name: "categories",
|
|
1082
|
+
type: "many",
|
|
1083
|
+
fields: {
|
|
1084
|
+
name: { field: "name" },
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
expect(result).toStrictEqual({
|
|
1092
|
+
kind: PlanKind.CONDITIONAL,
|
|
1093
|
+
filters: {
|
|
1094
|
+
categories: {
|
|
1095
|
+
some: {
|
|
1096
|
+
name: { in: ["business", "development"] },
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1103
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const query = await prisma.resource.findMany({
|
|
1107
|
+
where: { ...result.filters },
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
expect(query.map((r) => r.id).sort()).toEqual(
|
|
1111
|
+
fixtureResources
|
|
1112
|
+
.filter((resource) => {
|
|
1113
|
+
const categoryRefs =
|
|
1114
|
+
(resource.categories?.connect as Prisma.CategoryWhereUniqueInput[]) ??
|
|
1115
|
+
[];
|
|
1116
|
+
return categoryRefs.some((categoryRef) => {
|
|
1117
|
+
const category = fixtureCategories.find(
|
|
1118
|
+
(fc) => fc.id === categoryRef.id
|
|
1119
|
+
);
|
|
1120
|
+
return ["business", "development"].includes(category?.name ?? "");
|
|
1121
|
+
});
|
|
1122
|
+
})
|
|
1123
|
+
.map((r) => r.id)
|
|
1124
|
+
.sort()
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("conditional - except relation subset", async () => {
|
|
1129
|
+
const queryPlan = createConditionalPlan({
|
|
1130
|
+
operator: "except",
|
|
1131
|
+
operands: [
|
|
1132
|
+
{ name: "request.resource.attr.categories" },
|
|
1133
|
+
{
|
|
1134
|
+
operator: "lambda",
|
|
1135
|
+
operands: [
|
|
1136
|
+
{
|
|
1137
|
+
operator: "eq",
|
|
1138
|
+
operands: [
|
|
1139
|
+
{ name: "cat.name" },
|
|
1140
|
+
{ value: "business" },
|
|
1141
|
+
],
|
|
1142
|
+
},
|
|
1143
|
+
{ name: "cat" },
|
|
1144
|
+
],
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
const result = queryPlanToPrisma({
|
|
1150
|
+
queryPlan,
|
|
1151
|
+
mapper: {
|
|
1152
|
+
"request.resource.attr.categories": {
|
|
1153
|
+
relation: {
|
|
1154
|
+
name: "categories",
|
|
1155
|
+
type: "many",
|
|
1156
|
+
fields: {
|
|
1157
|
+
name: { field: "name" },
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(result).toStrictEqual({
|
|
1165
|
+
kind: PlanKind.CONDITIONAL,
|
|
1166
|
+
filters: {
|
|
1167
|
+
categories: {
|
|
1168
|
+
some: {
|
|
1169
|
+
NOT: {
|
|
1170
|
+
name: { equals: "business" },
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1178
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const query = await prisma.resource.findMany({
|
|
1182
|
+
where: { ...result.filters },
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
expect(query.map((r) => r.id).sort()).toEqual(
|
|
1186
|
+
fixtureResources
|
|
1187
|
+
.filter((resource) => {
|
|
1188
|
+
const categoryRefs =
|
|
1189
|
+
(resource.categories?.connect as Prisma.CategoryWhereUniqueInput[]) ??
|
|
1190
|
+
[];
|
|
1191
|
+
return categoryRefs.some((categoryRef) => {
|
|
1192
|
+
const category = fixtureCategories.find(
|
|
1193
|
+
(fc) => fc.id === categoryRef.id
|
|
1194
|
+
);
|
|
1195
|
+
return category?.name !== "business";
|
|
1196
|
+
});
|
|
1197
|
+
})
|
|
1198
|
+
.map((r) => r.id)
|
|
1199
|
+
.sort()
|
|
1200
|
+
);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
test("conditional - except nested relation subset", async () => {
|
|
1204
|
+
const queryPlan = createConditionalPlan({
|
|
1205
|
+
operator: "except",
|
|
1206
|
+
operands: [
|
|
1207
|
+
{ name: "request.resource.attr.categories" },
|
|
1208
|
+
{
|
|
1209
|
+
operator: "lambda",
|
|
1210
|
+
operands: [
|
|
1211
|
+
{
|
|
1212
|
+
operator: "exists",
|
|
1213
|
+
operands: [
|
|
1214
|
+
{ name: "cat.subCategories" },
|
|
1215
|
+
{
|
|
1216
|
+
operator: "lambda",
|
|
1217
|
+
operands: [
|
|
1218
|
+
{
|
|
1219
|
+
operator: "eq",
|
|
1220
|
+
operands: [
|
|
1221
|
+
{ name: "sub.name" },
|
|
1222
|
+
{ value: "finance" },
|
|
1223
|
+
],
|
|
1224
|
+
},
|
|
1225
|
+
{ name: "sub" },
|
|
1226
|
+
],
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
},
|
|
1230
|
+
{ name: "cat" },
|
|
1231
|
+
],
|
|
1232
|
+
},
|
|
1233
|
+
],
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
const result = queryPlanToPrisma({
|
|
1237
|
+
queryPlan,
|
|
1238
|
+
mapper: {
|
|
1239
|
+
"request.resource.attr.categories": {
|
|
1240
|
+
relation: {
|
|
1241
|
+
name: "categories",
|
|
1242
|
+
type: "many",
|
|
1243
|
+
fields: {
|
|
1244
|
+
subCategories: {
|
|
1245
|
+
relation: {
|
|
1246
|
+
name: "subCategories",
|
|
1247
|
+
type: "many",
|
|
1248
|
+
fields: {
|
|
1249
|
+
name: { field: "name" },
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
expect(result).toStrictEqual({
|
|
1260
|
+
kind: PlanKind.CONDITIONAL,
|
|
1261
|
+
filters: {
|
|
1262
|
+
categories: {
|
|
1263
|
+
some: {
|
|
1264
|
+
NOT: {
|
|
1265
|
+
subCategories: {
|
|
1266
|
+
some: {
|
|
1267
|
+
name: { equals: "finance" },
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1277
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const query = await prisma.resource.findMany({
|
|
1281
|
+
where: { ...result.filters },
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
expect(query.map((r) => r.id).sort()).toEqual(
|
|
1285
|
+
fixtureResources
|
|
1286
|
+
.filter((resource) => {
|
|
1287
|
+
const categoryRefs =
|
|
1288
|
+
(resource.categories?.connect as Prisma.CategoryWhereUniqueInput[]) ??
|
|
1289
|
+
[];
|
|
1290
|
+
return categoryRefs.some((categoryRef) => {
|
|
1291
|
+
const category = fixtureCategories.find(
|
|
1292
|
+
(fc) => fc.id === categoryRef.id
|
|
1293
|
+
);
|
|
1294
|
+
if (!category) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
const subCategoryRefs =
|
|
1298
|
+
(category.subCategories?.connect as Prisma.SubCategoryWhereUniqueInput[]) ??
|
|
1299
|
+
[];
|
|
1300
|
+
return !subCategoryRefs.some((subCategoryRef) => {
|
|
1301
|
+
const subCategory = fixtureSubCategories.find(
|
|
1302
|
+
(fsc) => fsc.id === subCategoryRef.id
|
|
1303
|
+
);
|
|
1304
|
+
return subCategory?.name === "finance";
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
})
|
|
1308
|
+
.map((r) => r.id)
|
|
1309
|
+
.sort()
|
|
1310
|
+
);
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
test("conditional - exists single", async () => {
|
|
1314
|
+
const queryPlan = await cerbos.planResources({
|
|
1315
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1316
|
+
resource: { kind: "resource" },
|
|
1317
|
+
action: "exists-single",
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1321
|
+
|
|
1322
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1323
|
+
{
|
|
1324
|
+
operator: "exists",
|
|
1325
|
+
operands: [
|
|
1326
|
+
{
|
|
1327
|
+
name: "request.resource.attr.tags",
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
operator: "lambda",
|
|
1331
|
+
operands: [
|
|
1332
|
+
{
|
|
1333
|
+
operator: "eq",
|
|
1334
|
+
operands: [
|
|
1335
|
+
{
|
|
1336
|
+
name: "tag.id",
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
value: "tag1",
|
|
1340
|
+
},
|
|
1341
|
+
],
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: "tag",
|
|
1345
|
+
},
|
|
1346
|
+
],
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
}
|
|
1350
|
+
);
|
|
1351
|
+
|
|
1352
|
+
const result = queryPlanToPrisma({
|
|
1353
|
+
queryPlan,
|
|
1354
|
+
mapper: {
|
|
1355
|
+
"request.resource.attr.tags": {
|
|
1356
|
+
relation: {
|
|
1357
|
+
name: "tags",
|
|
1358
|
+
type: "many",
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
expect(result).toStrictEqual({
|
|
1365
|
+
kind: PlanKind.CONDITIONAL,
|
|
1366
|
+
filters: {
|
|
1367
|
+
tags: {
|
|
1368
|
+
some: {
|
|
1369
|
+
id: {
|
|
1370
|
+
equals: "tag1",
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1378
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const query = await prisma.resource.findMany({
|
|
1382
|
+
where: { ...result.filters },
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1386
|
+
fixtureResources
|
|
1387
|
+
.filter(
|
|
1388
|
+
(a) =>
|
|
1389
|
+
Array.isArray(a.tags?.connect) &&
|
|
1390
|
+
a.tags?.connect
|
|
1391
|
+
.map((t) => {
|
|
1392
|
+
return fixtureTags.find((f) => f.id === t.id);
|
|
1393
|
+
})
|
|
1394
|
+
.filter((t) => t?.id == "tag1").length > 0
|
|
1395
|
+
)
|
|
1396
|
+
.map((r) => r.id)
|
|
1397
|
+
);
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
test("conditional - exists multiple", async () => {
|
|
1401
|
+
const queryPlan = await cerbos.planResources({
|
|
1402
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1403
|
+
resource: { kind: "resource" },
|
|
1404
|
+
action: "exists-multiple",
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1408
|
+
|
|
1409
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1410
|
+
{
|
|
1411
|
+
operator: "exists",
|
|
1412
|
+
operands: [
|
|
1413
|
+
{
|
|
1414
|
+
name: "request.resource.attr.tags",
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
operator: "lambda",
|
|
1418
|
+
operands: [
|
|
1419
|
+
{
|
|
1420
|
+
operator: "and",
|
|
1421
|
+
operands: [
|
|
1422
|
+
{
|
|
1423
|
+
operator: "eq",
|
|
1424
|
+
operands: [
|
|
1425
|
+
{
|
|
1426
|
+
name: "tag.id",
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
value: "tag1",
|
|
1430
|
+
},
|
|
1431
|
+
],
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
operator: "eq",
|
|
1435
|
+
operands: [
|
|
1436
|
+
{
|
|
1437
|
+
name: "tag.name",
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
value: "public",
|
|
1441
|
+
},
|
|
1442
|
+
],
|
|
1443
|
+
},
|
|
1444
|
+
],
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
name: "tag",
|
|
1448
|
+
},
|
|
1449
|
+
],
|
|
1450
|
+
},
|
|
1451
|
+
],
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
|
|
1455
|
+
const result = queryPlanToPrisma({
|
|
1456
|
+
queryPlan,
|
|
1457
|
+
mapper: {
|
|
1458
|
+
"request.resource.attr.tags": {
|
|
1459
|
+
relation: {
|
|
1460
|
+
name: "tags",
|
|
1461
|
+
type: "many",
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
expect(result).toStrictEqual({
|
|
1468
|
+
kind: PlanKind.CONDITIONAL,
|
|
1469
|
+
filters: {
|
|
1470
|
+
tags: {
|
|
1471
|
+
some: {
|
|
1472
|
+
AND: [
|
|
1473
|
+
{
|
|
1474
|
+
id: {
|
|
1475
|
+
equals: "tag1",
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
name: {
|
|
1480
|
+
equals: "public",
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
],
|
|
1484
|
+
},
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1490
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const query = await prisma.resource.findMany({
|
|
1494
|
+
where: { ...result.filters },
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1498
|
+
fixtureResources
|
|
1499
|
+
.filter(
|
|
1500
|
+
(a) =>
|
|
1501
|
+
Array.isArray(a.tags?.connect) &&
|
|
1502
|
+
a.tags?.connect
|
|
1503
|
+
.map((t) => {
|
|
1504
|
+
return fixtureTags.find((f) => f.id === t.id);
|
|
1505
|
+
})
|
|
1506
|
+
.filter((t) => t?.id === "tag1" && t?.name === "public")
|
|
1507
|
+
.length > 0
|
|
1508
|
+
)
|
|
1509
|
+
.map((r) => r.id)
|
|
1510
|
+
);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test("conditional - exists_one", async () => {
|
|
1514
|
+
const queryPlan = await cerbos.planResources({
|
|
1515
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1516
|
+
resource: { kind: "resource" },
|
|
1517
|
+
action: "exists-one",
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1521
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1522
|
+
{
|
|
1523
|
+
operator: "exists_one",
|
|
1524
|
+
operands: [
|
|
1525
|
+
{
|
|
1526
|
+
name: "request.resource.attr.tags",
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
operator: "lambda",
|
|
1530
|
+
operands: [
|
|
1531
|
+
{
|
|
1532
|
+
operator: "eq",
|
|
1533
|
+
operands: [
|
|
1534
|
+
{
|
|
1535
|
+
name: "tag.name",
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
value: "public",
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
name: "tag",
|
|
1544
|
+
},
|
|
1545
|
+
],
|
|
1546
|
+
},
|
|
1547
|
+
],
|
|
1548
|
+
}
|
|
1549
|
+
);
|
|
1550
|
+
|
|
1551
|
+
const result = queryPlanToPrisma({
|
|
1552
|
+
queryPlan,
|
|
1553
|
+
mapper: {
|
|
1554
|
+
"request.resource.attr.tags": {
|
|
1555
|
+
relation: {
|
|
1556
|
+
name: "tags",
|
|
1557
|
+
type: "many",
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
expect(result).toStrictEqual({
|
|
1564
|
+
kind: PlanKind.CONDITIONAL,
|
|
1565
|
+
filters: {
|
|
1566
|
+
tags: {
|
|
1567
|
+
some: {
|
|
1568
|
+
name: { equals: "public" },
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
AND: [
|
|
1572
|
+
{
|
|
1573
|
+
tags: {
|
|
1574
|
+
every: {
|
|
1575
|
+
OR: [
|
|
1576
|
+
{ name: { equals: "public" } },
|
|
1577
|
+
{ NOT: { name: { equals: "public" } } },
|
|
1578
|
+
],
|
|
1579
|
+
},
|
|
1580
|
+
},
|
|
1581
|
+
},
|
|
1582
|
+
],
|
|
1583
|
+
},
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1587
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const query = await prisma.resource.findMany({
|
|
1591
|
+
where: { ...result.filters },
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1595
|
+
fixtureResources
|
|
1596
|
+
.filter(
|
|
1597
|
+
(a) =>
|
|
1598
|
+
Array.isArray(a.tags?.connect) &&
|
|
1599
|
+
a.tags?.connect
|
|
1600
|
+
.map((t) => {
|
|
1601
|
+
return fixtureTags.find((f) => f.id === t.id);
|
|
1602
|
+
})
|
|
1603
|
+
.filter((t) => t?.id === "tag1" && t?.name === "public")
|
|
1604
|
+
.length > 0
|
|
1605
|
+
)
|
|
1606
|
+
.map((r) => r.id)
|
|
1607
|
+
);
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
test("conditional - all", async () => {
|
|
1611
|
+
const queryPlan = await cerbos.planResources({
|
|
1612
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1613
|
+
resource: { kind: "resource" },
|
|
1614
|
+
action: "all",
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1618
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1619
|
+
{
|
|
1620
|
+
operator: "all",
|
|
1621
|
+
operands: [
|
|
1622
|
+
{
|
|
1623
|
+
name: "request.resource.attr.tags",
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
operator: "lambda",
|
|
1627
|
+
operands: [
|
|
1628
|
+
{
|
|
1629
|
+
operator: "eq",
|
|
1630
|
+
operands: [
|
|
1631
|
+
{
|
|
1632
|
+
name: "tag.name",
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
value: "public",
|
|
1636
|
+
},
|
|
1637
|
+
],
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
name: "tag",
|
|
1641
|
+
},
|
|
1642
|
+
],
|
|
1643
|
+
},
|
|
1644
|
+
],
|
|
1645
|
+
}
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
const result = queryPlanToPrisma({
|
|
1649
|
+
queryPlan,
|
|
1650
|
+
mapper: {
|
|
1651
|
+
"request.resource.attr.tags": {
|
|
1652
|
+
relation: {
|
|
1653
|
+
name: "tags",
|
|
1654
|
+
type: "many",
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
expect(result).toStrictEqual({
|
|
1661
|
+
kind: PlanKind.CONDITIONAL,
|
|
1662
|
+
filters: {
|
|
1663
|
+
tags: {
|
|
1664
|
+
every: {
|
|
1665
|
+
name: { equals: "public" },
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1672
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const query = await prisma.resource.findMany({
|
|
1676
|
+
where: { ...result.filters },
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1680
|
+
fixtureResources
|
|
1681
|
+
.filter(
|
|
1682
|
+
(a) =>
|
|
1683
|
+
Array.isArray(a.tags?.connect) &&
|
|
1684
|
+
a.tags?.connect
|
|
1685
|
+
.map((t) => {
|
|
1686
|
+
return fixtureTags.find((f) => f.id === t.id);
|
|
1687
|
+
})
|
|
1688
|
+
.every((t) => t?.name === "public")
|
|
1689
|
+
)
|
|
1690
|
+
.map((r) => r.id)
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("conditional - filter", async () => {
|
|
1695
|
+
const queryPlan = await cerbos.planResources({
|
|
1696
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1697
|
+
resource: { kind: "resource" },
|
|
1698
|
+
action: "filter",
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1702
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1703
|
+
{
|
|
1704
|
+
operator: "filter",
|
|
1705
|
+
operands: [
|
|
1706
|
+
{
|
|
1707
|
+
name: "request.resource.attr.tags",
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
operator: "lambda",
|
|
1711
|
+
operands: [
|
|
1712
|
+
{
|
|
1713
|
+
operator: "eq",
|
|
1714
|
+
operands: [
|
|
1715
|
+
{
|
|
1716
|
+
name: "tag.name",
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
value: "public",
|
|
1720
|
+
},
|
|
1721
|
+
],
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
name: "tag",
|
|
1725
|
+
},
|
|
1726
|
+
],
|
|
1727
|
+
},
|
|
1728
|
+
],
|
|
1729
|
+
}
|
|
1730
|
+
);
|
|
1731
|
+
|
|
1732
|
+
const result = queryPlanToPrisma({
|
|
1733
|
+
queryPlan,
|
|
1734
|
+
mapper: {
|
|
1735
|
+
"request.resource.attr.tags": {
|
|
1736
|
+
relation: {
|
|
1737
|
+
name: "tags",
|
|
1738
|
+
type: "many",
|
|
1739
|
+
},
|
|
1740
|
+
},
|
|
1741
|
+
},
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
expect(result).toStrictEqual({
|
|
1745
|
+
kind: PlanKind.CONDITIONAL,
|
|
1746
|
+
filters: {
|
|
1747
|
+
tags: {
|
|
1748
|
+
some: {
|
|
1749
|
+
name: { equals: "public" },
|
|
1750
|
+
},
|
|
1751
|
+
},
|
|
1752
|
+
},
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1756
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const query = await prisma.resource.findMany({
|
|
1760
|
+
where: { ...result.filters },
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1764
|
+
fixtureResources
|
|
1765
|
+
.filter(
|
|
1766
|
+
(a) =>
|
|
1767
|
+
Array.isArray(a.tags?.connect) &&
|
|
1768
|
+
a.tags?.connect
|
|
1769
|
+
.map((t) => {
|
|
1770
|
+
return fixtureTags.find((f) => f.id === t.id);
|
|
1771
|
+
})
|
|
1772
|
+
.filter((t) => t?.name === "public").length > 0
|
|
1773
|
+
)
|
|
1774
|
+
.map((r) => r.id)
|
|
1775
|
+
);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
test("conditional - map collection", async () => {
|
|
1779
|
+
const queryPlan = await cerbos.planResources({
|
|
1780
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1781
|
+
resource: { kind: "resource" },
|
|
1782
|
+
action: "map-collection",
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1786
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
1787
|
+
{
|
|
1788
|
+
operator: "hasIntersection",
|
|
1789
|
+
operands: [
|
|
1790
|
+
{
|
|
1791
|
+
operator: "map",
|
|
1792
|
+
operands: [
|
|
1793
|
+
{ name: "request.resource.attr.tags" },
|
|
1794
|
+
{
|
|
1795
|
+
operator: "lambda",
|
|
1796
|
+
operands: [{ name: "tag.name" }, { name: "tag" }],
|
|
1797
|
+
},
|
|
1798
|
+
],
|
|
1799
|
+
},
|
|
1800
|
+
{ value: ["public", "private"] },
|
|
1801
|
+
],
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
|
|
1805
|
+
const result = queryPlanToPrisma({
|
|
1806
|
+
queryPlan,
|
|
1807
|
+
mapper: {
|
|
1808
|
+
"request.resource.attr.tags": {
|
|
1809
|
+
relation: {
|
|
1810
|
+
name: "tags",
|
|
1811
|
+
type: "many",
|
|
1812
|
+
fields: {
|
|
1813
|
+
name: { field: "name" },
|
|
1814
|
+
},
|
|
1815
|
+
},
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
expect(result).toStrictEqual({
|
|
1821
|
+
kind: PlanKind.CONDITIONAL,
|
|
1822
|
+
filters: {
|
|
1823
|
+
tags: {
|
|
1824
|
+
some: {
|
|
1825
|
+
name: { in: ["public", "private"] },
|
|
1826
|
+
},
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1832
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const query = await prisma.resource.findMany({
|
|
1836
|
+
where: { ...result.filters },
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
// Should return resources that have either "public" or "private" tags
|
|
1840
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1841
|
+
fixtureResources
|
|
1842
|
+
.filter((r) => {
|
|
1843
|
+
const tagNames = Array.isArray(r.tags?.connect)
|
|
1844
|
+
? r.tags.connect.map(
|
|
1845
|
+
(t) => fixtureTags.find((ft) => ft.id === t.id)?.name
|
|
1846
|
+
) || []
|
|
1847
|
+
: [];
|
|
1848
|
+
return tagNames.some((name) =>
|
|
1849
|
+
["public", "private"].includes(name || "")
|
|
1850
|
+
);
|
|
1851
|
+
})
|
|
1852
|
+
.map((r) => r.id)
|
|
1853
|
+
);
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// Relations
|
|
1859
|
+
describe("Relations", () => {
|
|
1860
|
+
describe("Simple Relations", () => {
|
|
1861
|
+
describe("One-to-One", () => {
|
|
1862
|
+
test("conditional - relation is", async () => {
|
|
1863
|
+
const queryPlan = await cerbos.planResources({
|
|
1864
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1865
|
+
resource: { kind: "resource" },
|
|
1866
|
+
action: "relation-is",
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1870
|
+
|
|
1871
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
1872
|
+
.condition;
|
|
1873
|
+
|
|
1874
|
+
expect(conditions).toEqual({
|
|
1875
|
+
operator: "eq",
|
|
1876
|
+
operands: [
|
|
1877
|
+
{ name: "request.resource.attr.createdBy" },
|
|
1878
|
+
{ value: "user1" },
|
|
1879
|
+
],
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
const result = queryPlanToPrisma({
|
|
1883
|
+
queryPlan,
|
|
1884
|
+
mapper: {
|
|
1885
|
+
"request.resource.attr.createdBy": {
|
|
1886
|
+
relation: {
|
|
1887
|
+
name: "createdBy",
|
|
1888
|
+
type: "one",
|
|
1889
|
+
field: "id",
|
|
1890
|
+
},
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
expect(result).toStrictEqual({
|
|
1896
|
+
kind: PlanKind.CONDITIONAL,
|
|
1897
|
+
filters: {
|
|
1898
|
+
createdBy: {
|
|
1899
|
+
is: {
|
|
1900
|
+
id: {
|
|
1901
|
+
equals: "user1",
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
},
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1909
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const query = await prisma.resource.findMany({
|
|
1913
|
+
where: { ...result.filters },
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1917
|
+
fixtureResources
|
|
1918
|
+
.filter((r) => {
|
|
1919
|
+
if (!r.createdBy?.connect) return false;
|
|
1920
|
+
return (r.createdBy.connect as { id: string }).id == "user1";
|
|
1921
|
+
})
|
|
1922
|
+
.map((r) => r.id)
|
|
1923
|
+
);
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
test("conditional - relation is not", async () => {
|
|
1927
|
+
const queryPlan = await cerbos.planResources({
|
|
1928
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
1929
|
+
resource: { kind: "resource" },
|
|
1930
|
+
action: "relation-is-not",
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
1934
|
+
|
|
1935
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
1936
|
+
.condition;
|
|
1937
|
+
|
|
1938
|
+
expect(conditions).toEqual({
|
|
1939
|
+
operator: "not",
|
|
1940
|
+
operands: [
|
|
1941
|
+
{
|
|
1942
|
+
operator: "eq",
|
|
1943
|
+
operands: [
|
|
1944
|
+
{ name: "request.resource.attr.createdBy" },
|
|
1945
|
+
{ value: "user1" },
|
|
1946
|
+
],
|
|
1947
|
+
},
|
|
1948
|
+
],
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
const result = queryPlanToPrisma({
|
|
1952
|
+
queryPlan,
|
|
1953
|
+
mapper: {
|
|
1954
|
+
"request.resource.attr.createdBy": {
|
|
1955
|
+
relation: {
|
|
1956
|
+
name: "createdBy",
|
|
1957
|
+
type: "one",
|
|
1958
|
+
field: "id",
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
},
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
expect(result).toStrictEqual({
|
|
1965
|
+
kind: PlanKind.CONDITIONAL,
|
|
1966
|
+
filters: {
|
|
1967
|
+
NOT: {
|
|
1968
|
+
createdBy: {
|
|
1969
|
+
is: {
|
|
1970
|
+
id: {
|
|
1971
|
+
equals: "user1",
|
|
1972
|
+
},
|
|
1973
|
+
},
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1976
|
+
},
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
1980
|
+
throw new Error("Expected CONDITIONAL result");
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const query = await prisma.resource.findMany({
|
|
1984
|
+
where: { ...result.filters },
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
1988
|
+
fixtureResources
|
|
1989
|
+
.filter((r) => {
|
|
1990
|
+
if (!r.createdBy?.connect) return false;
|
|
1991
|
+
return (r.createdBy.connect as { id: string }).id != "user1";
|
|
1992
|
+
})
|
|
1993
|
+
.map((r) => r.id)
|
|
1994
|
+
);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
test("conditional - relation is without field", async () => {
|
|
1998
|
+
const queryPlan = await cerbos.planResources({
|
|
1999
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2000
|
+
resource: { kind: "resource" },
|
|
2001
|
+
action: "relation-is",
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2005
|
+
|
|
2006
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2007
|
+
.condition;
|
|
2008
|
+
|
|
2009
|
+
expect(conditions).toEqual({
|
|
2010
|
+
operator: "eq",
|
|
2011
|
+
operands: [
|
|
2012
|
+
{ name: "request.resource.attr.createdBy" },
|
|
2013
|
+
{ value: "user1" },
|
|
2014
|
+
],
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
const result = queryPlanToPrisma({
|
|
2018
|
+
queryPlan,
|
|
2019
|
+
mapper: {
|
|
2020
|
+
"request.resource.attr.createdBy": {
|
|
2021
|
+
relation: {
|
|
2022
|
+
name: "createdBy",
|
|
2023
|
+
type: "one",
|
|
2024
|
+
field: "id",
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
},
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
expect(result).toStrictEqual({
|
|
2031
|
+
kind: PlanKind.CONDITIONAL,
|
|
2032
|
+
filters: {
|
|
2033
|
+
createdBy: {
|
|
2034
|
+
is: {
|
|
2035
|
+
id: { equals: "user1" },
|
|
2036
|
+
},
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2042
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const query = await prisma.resource.findMany({
|
|
2046
|
+
where: { ...result.filters },
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2050
|
+
fixtureResources
|
|
2051
|
+
.filter((r) => {
|
|
2052
|
+
if (!r.createdBy?.connect) return false;
|
|
2053
|
+
return (r.createdBy.connect as { id: string }).id == "user1";
|
|
2054
|
+
})
|
|
2055
|
+
.map((r) => r.id)
|
|
2056
|
+
);
|
|
2057
|
+
});
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
describe("One-to-Many/Many-to-Many", () => {
|
|
2061
|
+
test("conditional - relation some", async () => {
|
|
2062
|
+
const queryPlan = await cerbos.planResources({
|
|
2063
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2064
|
+
resource: { kind: "resource" },
|
|
2065
|
+
action: "relation-some",
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2069
|
+
|
|
2070
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2071
|
+
.condition;
|
|
2072
|
+
|
|
2073
|
+
expect(conditions).toEqual({
|
|
2074
|
+
operator: "in",
|
|
2075
|
+
operands: [
|
|
2076
|
+
{ value: "user1" },
|
|
2077
|
+
{ name: "request.resource.attr.ownedBy" },
|
|
2078
|
+
],
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
const result = queryPlanToPrisma({
|
|
2082
|
+
queryPlan,
|
|
2083
|
+
mapper: {
|
|
2084
|
+
"request.resource.attr.ownedBy": {
|
|
2085
|
+
relation: {
|
|
2086
|
+
name: "ownedBy",
|
|
2087
|
+
type: "many",
|
|
2088
|
+
field: "id",
|
|
2089
|
+
},
|
|
2090
|
+
},
|
|
2091
|
+
},
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
expect(result).toStrictEqual({
|
|
2095
|
+
kind: PlanKind.CONDITIONAL,
|
|
2096
|
+
filters: {
|
|
2097
|
+
ownedBy: {
|
|
2098
|
+
some: {
|
|
2099
|
+
id: "user1",
|
|
2100
|
+
},
|
|
2101
|
+
},
|
|
2102
|
+
},
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2106
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const query = await prisma.resource.findMany({
|
|
2110
|
+
where: { ...result.filters },
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2114
|
+
fixtureResources
|
|
2115
|
+
.filter((r) => {
|
|
2116
|
+
if (!r.ownedBy?.connect) return false;
|
|
2117
|
+
return (
|
|
2118
|
+
(r.ownedBy.connect as { id: string }[]).filter(
|
|
2119
|
+
(o) => o.id == "user1"
|
|
2120
|
+
).length > 0
|
|
2121
|
+
);
|
|
2122
|
+
})
|
|
2123
|
+
.map((r) => r.id)
|
|
2124
|
+
);
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test("conditional - relation none", async () => {
|
|
2128
|
+
const queryPlan = await cerbos.planResources({
|
|
2129
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2130
|
+
resource: { kind: "resource" },
|
|
2131
|
+
action: "relation-none",
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2135
|
+
|
|
2136
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2137
|
+
.condition;
|
|
2138
|
+
|
|
2139
|
+
expect(conditions).toEqual({
|
|
2140
|
+
operator: "not",
|
|
2141
|
+
operands: [
|
|
2142
|
+
{
|
|
2143
|
+
operator: "in",
|
|
2144
|
+
operands: [
|
|
2145
|
+
{ value: "user1" },
|
|
2146
|
+
{ name: "request.resource.attr.ownedBy" },
|
|
2147
|
+
],
|
|
2148
|
+
},
|
|
2149
|
+
],
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
const result = queryPlanToPrisma({
|
|
2153
|
+
queryPlan,
|
|
2154
|
+
mapper: {
|
|
2155
|
+
"request.resource.attr.ownedBy": {
|
|
2156
|
+
relation: {
|
|
2157
|
+
name: "ownedBy",
|
|
2158
|
+
type: "many",
|
|
2159
|
+
field: "id",
|
|
2160
|
+
},
|
|
2161
|
+
},
|
|
2162
|
+
},
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
expect(result).toStrictEqual({
|
|
2166
|
+
kind: PlanKind.CONDITIONAL,
|
|
2167
|
+
filters: {
|
|
2168
|
+
NOT: {
|
|
2169
|
+
ownedBy: {
|
|
2170
|
+
some: {
|
|
2171
|
+
id: "user1",
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2174
|
+
},
|
|
2175
|
+
},
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2179
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
const query = await prisma.resource.findMany({
|
|
2183
|
+
where: { ...result.filters },
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2187
|
+
fixtureResources
|
|
2188
|
+
.filter((r) => {
|
|
2189
|
+
if (!r.ownedBy?.connect) return false;
|
|
2190
|
+
return (
|
|
2191
|
+
(r.ownedBy.connect as { id: string }[]).filter(
|
|
2192
|
+
(o) => o.id == "user1"
|
|
2193
|
+
).length == 0
|
|
2194
|
+
);
|
|
2195
|
+
})
|
|
2196
|
+
.map((r) => r.id)
|
|
2197
|
+
);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
test("conditional - has-tag", async () => {
|
|
2201
|
+
const queryPlan = await cerbos.planResources({
|
|
2202
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2203
|
+
resource: { kind: "resource" },
|
|
2204
|
+
action: "has-tag",
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2208
|
+
expect(
|
|
2209
|
+
(queryPlan as PlanResourcesConditionalResponse).condition
|
|
2210
|
+
).toEqual({
|
|
2211
|
+
operator: "in",
|
|
2212
|
+
operands: [
|
|
2213
|
+
{ value: "public" },
|
|
2214
|
+
{ name: "request.resource.attr.tags" },
|
|
2215
|
+
],
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
const result = queryPlanToPrisma({
|
|
2219
|
+
queryPlan,
|
|
2220
|
+
mapper: {
|
|
2221
|
+
"request.resource.attr.tags": {
|
|
2222
|
+
relation: {
|
|
2223
|
+
name: "tags",
|
|
2224
|
+
type: "many", // This indicates a many-to-many relationship
|
|
2225
|
+
field: "name",
|
|
2226
|
+
},
|
|
2227
|
+
},
|
|
2228
|
+
},
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
expect(result).toStrictEqual({
|
|
2232
|
+
kind: PlanKind.CONDITIONAL,
|
|
2233
|
+
filters: {
|
|
2234
|
+
tags: {
|
|
2235
|
+
some: {
|
|
2236
|
+
name: "public",
|
|
2237
|
+
},
|
|
2238
|
+
},
|
|
2239
|
+
},
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2243
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
const query = await prisma.resource.findMany({
|
|
2247
|
+
where: { ...result.filters },
|
|
2248
|
+
include: { tags: true },
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
const expectedIds = fixtureResources
|
|
2252
|
+
.filter((resource) => {
|
|
2253
|
+
const tagRefs =
|
|
2254
|
+
(resource.tags?.connect as Prisma.TagWhereUniqueInput[]) ?? [];
|
|
2255
|
+
return tagRefs.some((tagRef) => {
|
|
2256
|
+
const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
|
|
2257
|
+
return tag?.name === "public";
|
|
2258
|
+
});
|
|
2259
|
+
})
|
|
2260
|
+
.map((r) => r.id)
|
|
2261
|
+
.sort();
|
|
2262
|
+
|
|
2263
|
+
expect(query.map((r) => r.id).sort()).toEqual(expectedIds);
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
test("conditional - has-no-tag", async () => {
|
|
2267
|
+
const queryPlan = await cerbos.planResources({
|
|
2268
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2269
|
+
resource: { kind: "resource" },
|
|
2270
|
+
action: "has-no-tag",
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2274
|
+
expect(
|
|
2275
|
+
(queryPlan as PlanResourcesConditionalResponse).condition
|
|
2276
|
+
).toEqual({
|
|
2277
|
+
operator: "not",
|
|
2278
|
+
operands: [
|
|
2279
|
+
{
|
|
2280
|
+
operator: "in",
|
|
2281
|
+
operands: [
|
|
2282
|
+
{ value: "private" },
|
|
2283
|
+
{ name: "request.resource.attr.tags" },
|
|
2284
|
+
],
|
|
2285
|
+
},
|
|
2286
|
+
],
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
const result = queryPlanToPrisma({
|
|
2290
|
+
queryPlan,
|
|
2291
|
+
mapper: {
|
|
2292
|
+
"request.resource.attr.tags": {
|
|
2293
|
+
relation: {
|
|
2294
|
+
name: "tags",
|
|
2295
|
+
type: "many",
|
|
2296
|
+
field: "name",
|
|
2297
|
+
},
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2300
|
+
});
|
|
2301
|
+
|
|
2302
|
+
expect(result).toStrictEqual({
|
|
2303
|
+
kind: PlanKind.CONDITIONAL,
|
|
2304
|
+
filters: {
|
|
2305
|
+
NOT: {
|
|
2306
|
+
tags: {
|
|
2307
|
+
some: {
|
|
2308
|
+
name: "private",
|
|
2309
|
+
},
|
|
2310
|
+
},
|
|
2311
|
+
},
|
|
2312
|
+
},
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2316
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
const query = await prisma.resource.findMany({
|
|
2320
|
+
where: { ...result.filters },
|
|
2321
|
+
include: { tags: true },
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
const expectedIds = fixtureResources
|
|
2325
|
+
.filter((resource) => {
|
|
2326
|
+
const tagRefs =
|
|
2327
|
+
(resource.tags?.connect as Prisma.TagWhereUniqueInput[]) ?? [];
|
|
2328
|
+
return tagRefs.every((tagRef) => {
|
|
2329
|
+
const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
|
|
2330
|
+
return tag?.name !== "private";
|
|
2331
|
+
});
|
|
2332
|
+
})
|
|
2333
|
+
.map((r) => r.id)
|
|
2334
|
+
.sort();
|
|
2335
|
+
|
|
2336
|
+
expect(query.map((r) => r.id).sort()).toEqual(expectedIds);
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
test("conditional - has-intersection", async () => {
|
|
2340
|
+
const queryPlan = await cerbos.planResources({
|
|
2341
|
+
principal: {
|
|
2342
|
+
id: "user1",
|
|
2343
|
+
roles: ["USER"],
|
|
2344
|
+
attr: { tags: ["public", "draft"] },
|
|
2345
|
+
},
|
|
2346
|
+
resource: { kind: "resource" },
|
|
2347
|
+
action: "has-intersection",
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2351
|
+
// expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual({
|
|
2352
|
+
// operator: "hasIntersection",
|
|
2353
|
+
// operands: [
|
|
2354
|
+
// { name: "request.resource.attr.tags" },
|
|
2355
|
+
// { value: ["public", "draft"] },
|
|
2356
|
+
// ],
|
|
2357
|
+
// });
|
|
2358
|
+
|
|
2359
|
+
const result = queryPlanToPrisma({
|
|
2360
|
+
queryPlan,
|
|
2361
|
+
mapper: {
|
|
2362
|
+
"request.resource.attr.tags": {
|
|
2363
|
+
relation: {
|
|
2364
|
+
name: "tags",
|
|
2365
|
+
type: "many",
|
|
2366
|
+
},
|
|
2367
|
+
},
|
|
2368
|
+
},
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
expect(result).toStrictEqual({
|
|
2372
|
+
kind: PlanKind.CONDITIONAL,
|
|
2373
|
+
filters: {
|
|
2374
|
+
tags: {
|
|
2375
|
+
some: {
|
|
2376
|
+
name: { in: ["public", "draft"] },
|
|
2377
|
+
},
|
|
2378
|
+
},
|
|
2379
|
+
},
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2383
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const query = await prisma.resource.findMany({
|
|
2387
|
+
where: { ...result.filters },
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
const expectedIds = fixtureResources
|
|
2391
|
+
.filter((resource) => {
|
|
2392
|
+
const tagRefs =
|
|
2393
|
+
(resource.tags?.connect as Prisma.TagWhereUniqueInput[]) ?? [];
|
|
2394
|
+
return tagRefs.some((tagRef) => {
|
|
2395
|
+
const tag = fixtureTags.find((ft) => ft.id === tagRef.id);
|
|
2396
|
+
return ["public", "draft"].includes(tag?.name ?? "");
|
|
2397
|
+
});
|
|
2398
|
+
})
|
|
2399
|
+
.map((r) => r.id)
|
|
2400
|
+
.sort();
|
|
2401
|
+
|
|
2402
|
+
expect(query.map((r) => r.id).sort()).toEqual(expectedIds);
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
test("conditional - hasIntersection with direct value", async () => {
|
|
2406
|
+
const queryPlan = await cerbos.planResources({
|
|
2407
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2408
|
+
resource: { kind: "resource" },
|
|
2409
|
+
action: "has-intersection-direct",
|
|
2410
|
+
});
|
|
2411
|
+
|
|
2412
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2413
|
+
expect(
|
|
2414
|
+
(queryPlan as PlanResourcesConditionalResponse).condition
|
|
2415
|
+
).toEqual({
|
|
2416
|
+
operator: "hasIntersection",
|
|
2417
|
+
operands: [
|
|
2418
|
+
{ name: "request.resource.attr.tags" },
|
|
2419
|
+
{ value: ["public", "draft"] },
|
|
2420
|
+
],
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
const result = queryPlanToPrisma({
|
|
2424
|
+
queryPlan,
|
|
2425
|
+
mapper: {
|
|
2426
|
+
"request.resource.attr.tags": {
|
|
2427
|
+
relation: {
|
|
2428
|
+
name: "tags",
|
|
2429
|
+
type: "many",
|
|
2430
|
+
field: "name",
|
|
2431
|
+
},
|
|
2432
|
+
},
|
|
2433
|
+
},
|
|
2434
|
+
});
|
|
2435
|
+
|
|
2436
|
+
expect(result).toStrictEqual({
|
|
2437
|
+
kind: PlanKind.CONDITIONAL,
|
|
2438
|
+
filters: {
|
|
2439
|
+
tags: {
|
|
2440
|
+
some: {
|
|
2441
|
+
name: { in: ["public", "draft"] },
|
|
2442
|
+
},
|
|
2443
|
+
},
|
|
2444
|
+
},
|
|
2445
|
+
});
|
|
2446
|
+
|
|
2447
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2448
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
const query = await prisma.resource.findMany({
|
|
2452
|
+
where: { ...result.filters },
|
|
2453
|
+
});
|
|
2454
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2455
|
+
fixtureResources
|
|
2456
|
+
.filter(
|
|
2457
|
+
(r) =>
|
|
2458
|
+
Array.isArray(r.tags?.connect) &&
|
|
2459
|
+
r.tags?.connect
|
|
2460
|
+
.map((t) => fixtureTags.find((ft) => ft.id === t.id)?.name)
|
|
2461
|
+
.some((name) => ["public", "draft"].includes(name ?? ""))
|
|
2462
|
+
)
|
|
2463
|
+
.map((r) => r.id)
|
|
2464
|
+
);
|
|
2465
|
+
});
|
|
2466
|
+
});
|
|
2467
|
+
});
|
|
2468
|
+
|
|
2469
|
+
describe("Nested Relations", () => {
|
|
2470
|
+
describe("Single Level", () => {
|
|
2471
|
+
test("conditional - nested eq", async () => {
|
|
2472
|
+
const queryPlan = await cerbos.planResources({
|
|
2473
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2474
|
+
resource: { kind: "resource" },
|
|
2475
|
+
action: "equal-nested",
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2479
|
+
|
|
2480
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2481
|
+
.condition;
|
|
2482
|
+
|
|
2483
|
+
expect(conditions).toEqual({
|
|
2484
|
+
operator: "eq",
|
|
2485
|
+
operands: [
|
|
2486
|
+
{ name: "request.resource.attr.nested.aBool" },
|
|
2487
|
+
{ value: true },
|
|
2488
|
+
],
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
const result = queryPlanToPrisma({
|
|
2492
|
+
queryPlan,
|
|
2493
|
+
mapper: {
|
|
2494
|
+
"request.resource.attr.nested": {
|
|
2495
|
+
relation: {
|
|
2496
|
+
name: "nested",
|
|
2497
|
+
type: "one",
|
|
2498
|
+
fields: {
|
|
2499
|
+
aBool: { field: "aBool" },
|
|
2500
|
+
},
|
|
2501
|
+
},
|
|
2502
|
+
},
|
|
2503
|
+
},
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2506
|
+
expect(result).toStrictEqual({
|
|
2507
|
+
kind: PlanKind.CONDITIONAL,
|
|
2508
|
+
filters: { nested: { is: { aBool: { equals: true } } } },
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2512
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
const query = await prisma.resource.findMany({
|
|
2516
|
+
where: { ...result.filters },
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2520
|
+
fixtureResources
|
|
2521
|
+
.filter(
|
|
2522
|
+
(a) =>
|
|
2523
|
+
fixtureNestedResources.find(
|
|
2524
|
+
(f) => f.id === a.nested.connect?.id
|
|
2525
|
+
)?.aBool
|
|
2526
|
+
)
|
|
2527
|
+
.map((r) => r.id)
|
|
2528
|
+
);
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
test("conditional - relation eq with number", async () => {
|
|
2532
|
+
const queryPlan = await cerbos.planResources({
|
|
2533
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2534
|
+
resource: { kind: "resource" },
|
|
2535
|
+
action: "relation-eq-number",
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2539
|
+
|
|
2540
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2541
|
+
.condition;
|
|
2542
|
+
|
|
2543
|
+
expect(conditions).toEqual({
|
|
2544
|
+
operator: "eq",
|
|
2545
|
+
operands: [
|
|
2546
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2547
|
+
{ value: 1 },
|
|
2548
|
+
],
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
const result = queryPlanToPrisma({
|
|
2552
|
+
queryPlan,
|
|
2553
|
+
mapper: {
|
|
2554
|
+
"request.resource.attr.nested": {
|
|
2555
|
+
relation: {
|
|
2556
|
+
name: "nested",
|
|
2557
|
+
type: "one",
|
|
2558
|
+
},
|
|
2559
|
+
},
|
|
2560
|
+
},
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
expect(result).toStrictEqual({
|
|
2564
|
+
kind: PlanKind.CONDITIONAL,
|
|
2565
|
+
filters: {
|
|
2566
|
+
nested: {
|
|
2567
|
+
is: {
|
|
2568
|
+
aNumber: { equals: 1 },
|
|
2569
|
+
},
|
|
2570
|
+
},
|
|
2571
|
+
},
|
|
2572
|
+
});
|
|
2573
|
+
|
|
2574
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2575
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
const query = await prisma.resource.findMany({
|
|
2579
|
+
where: { ...result.filters },
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2583
|
+
fixtureResources
|
|
2584
|
+
.filter((r) => {
|
|
2585
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2586
|
+
(n) => n.id === r.nested.connect?.id
|
|
2587
|
+
);
|
|
2588
|
+
return (nestedResource?.aNumber ?? 0) == 1;
|
|
2589
|
+
})
|
|
2590
|
+
.map((r) => r.id)
|
|
2591
|
+
);
|
|
2592
|
+
});
|
|
2593
|
+
|
|
2594
|
+
test("conditional - relation eq with number without field", async () => {
|
|
2595
|
+
const queryPlan = await cerbos.planResources({
|
|
2596
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2597
|
+
resource: { kind: "resource" },
|
|
2598
|
+
action: "relation-eq-number",
|
|
2599
|
+
});
|
|
2600
|
+
|
|
2601
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2602
|
+
|
|
2603
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2604
|
+
.condition;
|
|
2605
|
+
|
|
2606
|
+
expect(conditions).toEqual({
|
|
2607
|
+
operator: "eq",
|
|
2608
|
+
operands: [
|
|
2609
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2610
|
+
{ value: 1 },
|
|
2611
|
+
],
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
const result = queryPlanToPrisma({
|
|
2615
|
+
queryPlan,
|
|
2616
|
+
mapper: {
|
|
2617
|
+
"request.resource.attr.nested": {
|
|
2618
|
+
relation: {
|
|
2619
|
+
name: "nested",
|
|
2620
|
+
type: "one",
|
|
2621
|
+
},
|
|
2622
|
+
},
|
|
2623
|
+
},
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
expect(result).toStrictEqual({
|
|
2627
|
+
kind: PlanKind.CONDITIONAL,
|
|
2628
|
+
filters: {
|
|
2629
|
+
nested: {
|
|
2630
|
+
is: {
|
|
2631
|
+
aNumber: {
|
|
2632
|
+
equals: 1,
|
|
2633
|
+
},
|
|
2634
|
+
},
|
|
2635
|
+
},
|
|
2636
|
+
},
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2640
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const query = await prisma.resource.findMany({
|
|
2644
|
+
where: { ...result.filters },
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2648
|
+
fixtureResources
|
|
2649
|
+
.filter((r) => {
|
|
2650
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2651
|
+
(n) => n.id === r.nested.connect?.id
|
|
2652
|
+
);
|
|
2653
|
+
return nestedResource && nestedResource.aNumber === 1;
|
|
2654
|
+
})
|
|
2655
|
+
.map((r) => r.id)
|
|
2656
|
+
);
|
|
2657
|
+
});
|
|
2658
|
+
|
|
2659
|
+
test("conditional - relation lt with number", async () => {
|
|
2660
|
+
const queryPlan = await cerbos.planResources({
|
|
2661
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2662
|
+
resource: { kind: "resource" },
|
|
2663
|
+
action: "relation-lt-number",
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2667
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2668
|
+
.condition;
|
|
2669
|
+
expect(conditions).toEqual({
|
|
2670
|
+
operator: "lt",
|
|
2671
|
+
operands: [
|
|
2672
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2673
|
+
{ value: 2 },
|
|
2674
|
+
],
|
|
2675
|
+
});
|
|
2676
|
+
|
|
2677
|
+
const result = queryPlanToPrisma({
|
|
2678
|
+
queryPlan,
|
|
2679
|
+
mapper: {
|
|
2680
|
+
"request.resource.attr.nested": {
|
|
2681
|
+
relation: {
|
|
2682
|
+
name: "nested",
|
|
2683
|
+
type: "one",
|
|
2684
|
+
},
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
expect(result).toStrictEqual({
|
|
2690
|
+
kind: PlanKind.CONDITIONAL,
|
|
2691
|
+
filters: {
|
|
2692
|
+
nested: {
|
|
2693
|
+
is: {
|
|
2694
|
+
aNumber: { lt: 2 },
|
|
2695
|
+
},
|
|
2696
|
+
},
|
|
2697
|
+
},
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2701
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
const query = await prisma.resource.findMany({
|
|
2705
|
+
where: { ...result.filters },
|
|
2706
|
+
});
|
|
2707
|
+
|
|
2708
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2709
|
+
fixtureResources
|
|
2710
|
+
.filter((r) => {
|
|
2711
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2712
|
+
(n) => n.id === r.nested.connect?.id
|
|
2713
|
+
);
|
|
2714
|
+
return (nestedResource?.aNumber ?? 0) < 2;
|
|
2715
|
+
})
|
|
2716
|
+
.map((r) => r.id)
|
|
2717
|
+
);
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
test("conditional - relation lte with number", async () => {
|
|
2721
|
+
const queryPlan = await cerbos.planResources({
|
|
2722
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2723
|
+
resource: { kind: "resource" },
|
|
2724
|
+
action: "relation-lte-number",
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2728
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2729
|
+
.condition;
|
|
2730
|
+
expect(conditions).toEqual({
|
|
2731
|
+
operator: "le",
|
|
2732
|
+
operands: [
|
|
2733
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2734
|
+
{ value: 2 },
|
|
2735
|
+
],
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
const result = queryPlanToPrisma({
|
|
2739
|
+
queryPlan,
|
|
2740
|
+
mapper: {
|
|
2741
|
+
"request.resource.attr.nested": {
|
|
2742
|
+
relation: {
|
|
2743
|
+
name: "nested",
|
|
2744
|
+
type: "one",
|
|
2745
|
+
},
|
|
2746
|
+
},
|
|
2747
|
+
},
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2750
|
+
expect(result).toStrictEqual({
|
|
2751
|
+
kind: PlanKind.CONDITIONAL,
|
|
2752
|
+
filters: {
|
|
2753
|
+
nested: {
|
|
2754
|
+
is: {
|
|
2755
|
+
aNumber: { lte: 2 },
|
|
2756
|
+
},
|
|
2757
|
+
},
|
|
2758
|
+
},
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2761
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2762
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
const query = await prisma.resource.findMany({
|
|
2766
|
+
where: { ...result.filters },
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2770
|
+
fixtureResources
|
|
2771
|
+
.filter((r) => {
|
|
2772
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2773
|
+
(n) => n.id === r.nested.connect?.id
|
|
2774
|
+
);
|
|
2775
|
+
return (nestedResource?.aNumber ?? 0) <= 2;
|
|
2776
|
+
})
|
|
2777
|
+
.map((r) => r.id)
|
|
2778
|
+
);
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
test("conditional - relation gte with number", async () => {
|
|
2782
|
+
const queryPlan = await cerbos.planResources({
|
|
2783
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2784
|
+
resource: { kind: "resource" },
|
|
2785
|
+
action: "relation-gte-number",
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2789
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2790
|
+
.condition;
|
|
2791
|
+
expect(conditions).toEqual({
|
|
2792
|
+
operator: "ge",
|
|
2793
|
+
operands: [
|
|
2794
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2795
|
+
{ value: 1 },
|
|
2796
|
+
],
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
const result = queryPlanToPrisma({
|
|
2800
|
+
queryPlan,
|
|
2801
|
+
mapper: {
|
|
2802
|
+
"request.resource.attr.nested": {
|
|
2803
|
+
relation: {
|
|
2804
|
+
name: "nested",
|
|
2805
|
+
type: "one",
|
|
2806
|
+
},
|
|
2807
|
+
},
|
|
2808
|
+
},
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
expect(result).toStrictEqual({
|
|
2812
|
+
kind: PlanKind.CONDITIONAL,
|
|
2813
|
+
filters: {
|
|
2814
|
+
nested: {
|
|
2815
|
+
is: {
|
|
2816
|
+
aNumber: { gte: 1 },
|
|
2817
|
+
},
|
|
2818
|
+
},
|
|
2819
|
+
},
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2823
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
const query = await prisma.resource.findMany({
|
|
2827
|
+
where: { ...result.filters },
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2831
|
+
fixtureResources
|
|
2832
|
+
.filter((r) => {
|
|
2833
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2834
|
+
(n) => n.id === r.nested.connect?.id
|
|
2835
|
+
);
|
|
2836
|
+
return (nestedResource?.aNumber ?? 0) >= 1;
|
|
2837
|
+
})
|
|
2838
|
+
.map((r) => r.id)
|
|
2839
|
+
);
|
|
2840
|
+
});
|
|
2841
|
+
|
|
2842
|
+
test("conditional - relation-gt-number", async () => {
|
|
2843
|
+
const queryPlan = await cerbos.planResources({
|
|
2844
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2845
|
+
resource: { kind: "resource" },
|
|
2846
|
+
action: "relation-gt-number",
|
|
2847
|
+
});
|
|
2848
|
+
|
|
2849
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2850
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2851
|
+
.condition;
|
|
2852
|
+
expect(conditions).toEqual({
|
|
2853
|
+
operator: "gt",
|
|
2854
|
+
operands: [
|
|
2855
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
2856
|
+
{ value: 1 },
|
|
2857
|
+
],
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
const result = queryPlanToPrisma({
|
|
2861
|
+
queryPlan,
|
|
2862
|
+
mapper: {
|
|
2863
|
+
"request.resource.attr.nested": {
|
|
2864
|
+
relation: {
|
|
2865
|
+
name: "nested",
|
|
2866
|
+
type: "one",
|
|
2867
|
+
},
|
|
2868
|
+
},
|
|
2869
|
+
},
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
expect(result).toStrictEqual({
|
|
2873
|
+
kind: PlanKind.CONDITIONAL,
|
|
2874
|
+
filters: {
|
|
2875
|
+
nested: {
|
|
2876
|
+
is: {
|
|
2877
|
+
aNumber: { gt: 1 },
|
|
2878
|
+
},
|
|
2879
|
+
},
|
|
2880
|
+
},
|
|
2881
|
+
});
|
|
2882
|
+
|
|
2883
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2884
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
const query = await prisma.resource.findMany({
|
|
2888
|
+
where: { ...result.filters },
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2892
|
+
fixtureResources
|
|
2893
|
+
.filter((r) => {
|
|
2894
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2895
|
+
(n) => n.id === r.nested.connect?.id
|
|
2896
|
+
);
|
|
2897
|
+
return (nestedResource?.aNumber ?? 0) > 1;
|
|
2898
|
+
})
|
|
2899
|
+
.map((r) => r.id)
|
|
2900
|
+
);
|
|
2901
|
+
});
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
describe("Deep Nesting", () => {
|
|
2905
|
+
test("conditional - deeply nested eq", async () => {
|
|
2906
|
+
const queryPlan = await cerbos.planResources({
|
|
2907
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2908
|
+
resource: { kind: "resource" },
|
|
2909
|
+
action: "equal-deeply-nested",
|
|
2910
|
+
});
|
|
2911
|
+
|
|
2912
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2913
|
+
|
|
2914
|
+
const conditions = (queryPlan as PlanResourcesConditionalResponse)
|
|
2915
|
+
.condition;
|
|
2916
|
+
|
|
2917
|
+
expect(conditions).toEqual({
|
|
2918
|
+
operator: "eq",
|
|
2919
|
+
operands: [
|
|
2920
|
+
{ name: "request.resource.attr.nested.nextlevel.aBool" },
|
|
2921
|
+
{ value: true },
|
|
2922
|
+
],
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
const result = queryPlanToPrisma({
|
|
2926
|
+
queryPlan,
|
|
2927
|
+
mapper: {
|
|
2928
|
+
"request.resource.attr.nested": {
|
|
2929
|
+
relation: {
|
|
2930
|
+
name: "nested",
|
|
2931
|
+
type: "one",
|
|
2932
|
+
fields: {
|
|
2933
|
+
nextlevel: {
|
|
2934
|
+
relation: {
|
|
2935
|
+
name: "nextlevel",
|
|
2936
|
+
type: "one",
|
|
2937
|
+
fields: {
|
|
2938
|
+
aBool: { field: "aBool" },
|
|
2939
|
+
},
|
|
2940
|
+
},
|
|
2941
|
+
},
|
|
2942
|
+
},
|
|
2943
|
+
},
|
|
2944
|
+
},
|
|
2945
|
+
},
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
expect(result).toStrictEqual({
|
|
2949
|
+
kind: PlanKind.CONDITIONAL,
|
|
2950
|
+
filters: {
|
|
2951
|
+
nested: {
|
|
2952
|
+
is: {
|
|
2953
|
+
nextlevel: {
|
|
2954
|
+
is: {
|
|
2955
|
+
aBool: {
|
|
2956
|
+
equals: true,
|
|
2957
|
+
},
|
|
2958
|
+
},
|
|
2959
|
+
},
|
|
2960
|
+
},
|
|
2961
|
+
},
|
|
2962
|
+
},
|
|
2963
|
+
});
|
|
2964
|
+
|
|
2965
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
2966
|
+
throw new Error("Expected CONDITIONAL result");
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
const query = await prisma.resource.findMany({
|
|
2970
|
+
where: { ...result.filters },
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
2974
|
+
fixtureResources
|
|
2975
|
+
.filter((a) => {
|
|
2976
|
+
const nestedResource = fixtureNestedResources.find(
|
|
2977
|
+
(f) => f.id === a.nested.connect?.id
|
|
2978
|
+
);
|
|
2979
|
+
const nextLevelResource = fixtureNextLevelResources.find(
|
|
2980
|
+
(f) => f.id === nestedResource?.nextlevel.connect?.id
|
|
2981
|
+
);
|
|
2982
|
+
return nextLevelResource?.aBool === true;
|
|
2983
|
+
})
|
|
2984
|
+
.map((r) => r.id)
|
|
2985
|
+
);
|
|
2986
|
+
});
|
|
2987
|
+
|
|
2988
|
+
test("conditional - deeply nested many to many category label", async () => {
|
|
2989
|
+
const queryPlan = await cerbos.planResources({
|
|
2990
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
2991
|
+
resource: { kind: "resource" },
|
|
2992
|
+
action: "deep-nested-category-label",
|
|
2993
|
+
});
|
|
2994
|
+
|
|
2995
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
2996
|
+
|
|
2997
|
+
expect(
|
|
2998
|
+
(queryPlan as PlanResourcesConditionalResponse).condition
|
|
2999
|
+
).toEqual({
|
|
3000
|
+
operator: "exists",
|
|
3001
|
+
operands: [
|
|
3002
|
+
{
|
|
3003
|
+
name: "request.resource.attr.categories",
|
|
3004
|
+
},
|
|
3005
|
+
{
|
|
3006
|
+
operator: "lambda",
|
|
3007
|
+
operands: [
|
|
3008
|
+
{
|
|
3009
|
+
operator: "exists",
|
|
3010
|
+
operands: [
|
|
3011
|
+
{
|
|
3012
|
+
name: "cat.subCategories",
|
|
3013
|
+
},
|
|
3014
|
+
{
|
|
3015
|
+
operator: "lambda",
|
|
3016
|
+
operands: [
|
|
3017
|
+
{
|
|
3018
|
+
operator: "exists",
|
|
3019
|
+
operands: [
|
|
3020
|
+
{
|
|
3021
|
+
name: "sub.labels",
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
operator: "lambda",
|
|
3025
|
+
operands: [
|
|
3026
|
+
{
|
|
3027
|
+
operator: "eq",
|
|
3028
|
+
operands: [
|
|
3029
|
+
{
|
|
3030
|
+
name: "label.name",
|
|
3031
|
+
},
|
|
3032
|
+
{
|
|
3033
|
+
value: "important",
|
|
3034
|
+
},
|
|
3035
|
+
],
|
|
3036
|
+
},
|
|
3037
|
+
{
|
|
3038
|
+
name: "label",
|
|
3039
|
+
},
|
|
3040
|
+
],
|
|
3041
|
+
},
|
|
3042
|
+
],
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
name: "sub",
|
|
3046
|
+
},
|
|
3047
|
+
],
|
|
3048
|
+
},
|
|
3049
|
+
],
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
name: "cat",
|
|
3053
|
+
},
|
|
3054
|
+
],
|
|
3055
|
+
},
|
|
3056
|
+
],
|
|
3057
|
+
});
|
|
3058
|
+
|
|
3059
|
+
const result = queryPlanToPrisma({
|
|
3060
|
+
queryPlan,
|
|
3061
|
+
mapper: {
|
|
3062
|
+
"request.resource.attr.categories": {
|
|
3063
|
+
relation: {
|
|
3064
|
+
name: "categories",
|
|
3065
|
+
type: "many",
|
|
3066
|
+
fields: {
|
|
3067
|
+
subCategories: {
|
|
3068
|
+
relation: {
|
|
3069
|
+
name: "subCategories",
|
|
3070
|
+
type: "many",
|
|
3071
|
+
fields: {
|
|
3072
|
+
labels: {
|
|
3073
|
+
relation: {
|
|
3074
|
+
name: "labels",
|
|
3075
|
+
type: "many",
|
|
3076
|
+
fields: {
|
|
3077
|
+
name: { field: "name" },
|
|
3078
|
+
},
|
|
3079
|
+
},
|
|
3080
|
+
},
|
|
3081
|
+
},
|
|
3082
|
+
},
|
|
3083
|
+
},
|
|
3084
|
+
},
|
|
3085
|
+
},
|
|
3086
|
+
},
|
|
3087
|
+
},
|
|
3088
|
+
});
|
|
3089
|
+
|
|
3090
|
+
expect(result).toStrictEqual({
|
|
3091
|
+
kind: PlanKind.CONDITIONAL,
|
|
3092
|
+
filters: {
|
|
3093
|
+
categories: {
|
|
3094
|
+
some: {
|
|
3095
|
+
subCategories: {
|
|
3096
|
+
some: {
|
|
3097
|
+
labels: {
|
|
3098
|
+
some: {
|
|
3099
|
+
name: { equals: "important" },
|
|
3100
|
+
},
|
|
3101
|
+
},
|
|
3102
|
+
},
|
|
3103
|
+
},
|
|
3104
|
+
},
|
|
3105
|
+
},
|
|
3106
|
+
},
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3110
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
const query = await prisma.resource.findMany({
|
|
3114
|
+
where: { ...result.filters },
|
|
3115
|
+
include: {
|
|
3116
|
+
categories: {
|
|
3117
|
+
include: {
|
|
3118
|
+
subCategories: {
|
|
3119
|
+
include: {
|
|
3120
|
+
labels: true,
|
|
3121
|
+
},
|
|
3122
|
+
},
|
|
3123
|
+
},
|
|
3124
|
+
},
|
|
3125
|
+
},
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3129
|
+
fixtureResources
|
|
3130
|
+
.filter((r) => {
|
|
3131
|
+
return (
|
|
3132
|
+
r.categories?.connect as Prisma.CategoryWhereUniqueInput[]
|
|
3133
|
+
)
|
|
3134
|
+
.map((c) => {
|
|
3135
|
+
return fixtureCategories.find((fc) => fc.id === c.id);
|
|
3136
|
+
})
|
|
3137
|
+
.some((c) => {
|
|
3138
|
+
return (
|
|
3139
|
+
c?.subCategories
|
|
3140
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
3141
|
+
)
|
|
3142
|
+
.map((sc) => {
|
|
3143
|
+
return fixtureSubCategories.find(
|
|
3144
|
+
(fsc) => fsc.id === sc.id
|
|
3145
|
+
);
|
|
3146
|
+
})
|
|
3147
|
+
.some((sc) => {
|
|
3148
|
+
return (
|
|
3149
|
+
sc?.labels?.connect as Prisma.LabelWhereUniqueInput[]
|
|
3150
|
+
)
|
|
3151
|
+
.map((l) => {
|
|
3152
|
+
return fixtureLabels.find((fl) => fl.id === l.id);
|
|
3153
|
+
})
|
|
3154
|
+
.some((l) => l?.name === "important");
|
|
3155
|
+
});
|
|
3156
|
+
});
|
|
3157
|
+
})
|
|
3158
|
+
.map((r) => r.id)
|
|
3159
|
+
);
|
|
3160
|
+
});
|
|
3161
|
+
|
|
3162
|
+
test("conditional - deep nested exists with multiple conditions", async () => {
|
|
3163
|
+
const queryPlan = await cerbos.planResources({
|
|
3164
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3165
|
+
resource: { kind: "resource" },
|
|
3166
|
+
action: "deep-nested-exists",
|
|
3167
|
+
});
|
|
3168
|
+
|
|
3169
|
+
const result = queryPlanToPrisma({
|
|
3170
|
+
queryPlan,
|
|
3171
|
+
mapper: {
|
|
3172
|
+
"request.resource.attr.categories": {
|
|
3173
|
+
relation: {
|
|
3174
|
+
name: "categories",
|
|
3175
|
+
type: "many",
|
|
3176
|
+
fields: {
|
|
3177
|
+
subCategories: {
|
|
3178
|
+
relation: {
|
|
3179
|
+
name: "subCategories",
|
|
3180
|
+
type: "many",
|
|
3181
|
+
field: "name",
|
|
3182
|
+
},
|
|
3183
|
+
},
|
|
3184
|
+
},
|
|
3185
|
+
},
|
|
3186
|
+
},
|
|
3187
|
+
},
|
|
3188
|
+
});
|
|
3189
|
+
|
|
3190
|
+
expect(result).toStrictEqual({
|
|
3191
|
+
kind: PlanKind.CONDITIONAL,
|
|
3192
|
+
filters: {
|
|
3193
|
+
categories: {
|
|
3194
|
+
some: {
|
|
3195
|
+
AND: [
|
|
3196
|
+
{ name: { equals: "business" } },
|
|
3197
|
+
{
|
|
3198
|
+
subCategories: {
|
|
3199
|
+
some: {
|
|
3200
|
+
name: { equals: "finance" },
|
|
3201
|
+
},
|
|
3202
|
+
},
|
|
3203
|
+
},
|
|
3204
|
+
],
|
|
3205
|
+
},
|
|
3206
|
+
},
|
|
3207
|
+
},
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3211
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
const query = await prisma.resource.findMany({
|
|
3215
|
+
where: { ...result.filters },
|
|
3216
|
+
});
|
|
3217
|
+
|
|
3218
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3219
|
+
fixtureResources
|
|
3220
|
+
.filter((r) => {
|
|
3221
|
+
const categoryConnections = r.categories
|
|
3222
|
+
?.connect as Prisma.CategoryWhereUniqueInput[];
|
|
3223
|
+
return (
|
|
3224
|
+
categoryConnections?.some((categoryConnection) => {
|
|
3225
|
+
const category = fixtureCategories.find(
|
|
3226
|
+
(fc) => fc.id === categoryConnection.id
|
|
3227
|
+
);
|
|
3228
|
+
return (
|
|
3229
|
+
category?.name === "business" &&
|
|
3230
|
+
(
|
|
3231
|
+
category?.subCategories
|
|
3232
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
3233
|
+
).some(
|
|
3234
|
+
(sc) =>
|
|
3235
|
+
fixtureSubCategories.find((fsc) => fsc.id === sc.id)
|
|
3236
|
+
?.name === "finance"
|
|
3237
|
+
)
|
|
3238
|
+
);
|
|
3239
|
+
}) ?? false
|
|
3240
|
+
);
|
|
3241
|
+
})
|
|
3242
|
+
.map((r) => r.id)
|
|
3243
|
+
);
|
|
3244
|
+
});
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
describe("Nested String Operations", () => {
|
|
3248
|
+
test("conditional - nested contains", async () => {
|
|
3249
|
+
const queryPlan = await cerbos.planResources({
|
|
3250
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3251
|
+
resource: { kind: "resource" },
|
|
3252
|
+
action: "nested-contains",
|
|
3253
|
+
});
|
|
3254
|
+
|
|
3255
|
+
const result = queryPlanToPrisma({
|
|
3256
|
+
queryPlan,
|
|
3257
|
+
mapper: {
|
|
3258
|
+
"request.resource.attr.nested": {
|
|
3259
|
+
relation: {
|
|
3260
|
+
name: "nested",
|
|
3261
|
+
type: "one",
|
|
3262
|
+
fields: {
|
|
3263
|
+
aString: { field: "aString" },
|
|
3264
|
+
},
|
|
3265
|
+
},
|
|
3266
|
+
},
|
|
3267
|
+
},
|
|
3268
|
+
});
|
|
3269
|
+
|
|
3270
|
+
expect(result).toStrictEqual({
|
|
3271
|
+
kind: PlanKind.CONDITIONAL,
|
|
3272
|
+
filters: {
|
|
3273
|
+
nested: {
|
|
3274
|
+
is: {
|
|
3275
|
+
aString: { contains: "str" },
|
|
3276
|
+
},
|
|
3277
|
+
},
|
|
3278
|
+
},
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3282
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
const query = await prisma.resource.findMany({
|
|
3286
|
+
where: { ...result.filters },
|
|
3287
|
+
});
|
|
3288
|
+
|
|
3289
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3290
|
+
fixtureResources
|
|
3291
|
+
.filter((r) => {
|
|
3292
|
+
const nested = fixtureNestedResources.find(
|
|
3293
|
+
(n) => n.id === r.nested.connect?.id
|
|
3294
|
+
);
|
|
3295
|
+
return nested?.aString.includes("str");
|
|
3296
|
+
})
|
|
3297
|
+
.map((r) => r.id)
|
|
3298
|
+
);
|
|
3299
|
+
});
|
|
3300
|
+
|
|
3301
|
+
test("conditional - deeply nested startsWith", async () => {
|
|
3302
|
+
const queryPlan = await cerbos.planResources({
|
|
3303
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3304
|
+
resource: { kind: "resource" },
|
|
3305
|
+
action: "deeply-nested-starts-with",
|
|
3306
|
+
});
|
|
3307
|
+
|
|
3308
|
+
const result = queryPlanToPrisma({
|
|
3309
|
+
queryPlan,
|
|
3310
|
+
mapper: {
|
|
3311
|
+
"request.resource.attr.nested": {
|
|
3312
|
+
relation: {
|
|
3313
|
+
name: "nested",
|
|
3314
|
+
type: "one",
|
|
3315
|
+
fields: {
|
|
3316
|
+
nextlevel: {
|
|
3317
|
+
relation: {
|
|
3318
|
+
name: "nextlevel",
|
|
3319
|
+
type: "one",
|
|
3320
|
+
fields: {
|
|
3321
|
+
aString: { field: "aString" },
|
|
3322
|
+
},
|
|
3323
|
+
},
|
|
3324
|
+
},
|
|
3325
|
+
},
|
|
3326
|
+
},
|
|
3327
|
+
},
|
|
3328
|
+
},
|
|
3329
|
+
});
|
|
3330
|
+
|
|
3331
|
+
expect(result).toStrictEqual({
|
|
3332
|
+
kind: PlanKind.CONDITIONAL,
|
|
3333
|
+
filters: {
|
|
3334
|
+
nested: {
|
|
3335
|
+
is: {
|
|
3336
|
+
nextlevel: {
|
|
3337
|
+
is: {
|
|
3338
|
+
aString: { startsWith: "str" },
|
|
3339
|
+
},
|
|
3340
|
+
},
|
|
3341
|
+
},
|
|
3342
|
+
},
|
|
3343
|
+
},
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3347
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
const query = await prisma.resource.findMany({
|
|
3351
|
+
where: { ...result.filters },
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3355
|
+
fixtureResources
|
|
3356
|
+
.filter((r) => {
|
|
3357
|
+
const nested = fixtureNestedResources.find(
|
|
3358
|
+
(n) => n.id === r.nested.connect?.id
|
|
3359
|
+
);
|
|
3360
|
+
const nextLevel = fixtureNextLevelResources.find(
|
|
3361
|
+
(nl) => nl.id === nested?.nextlevel.connect?.id
|
|
3362
|
+
);
|
|
3363
|
+
return nextLevel?.aString.startsWith("str");
|
|
3364
|
+
})
|
|
3365
|
+
.map((r) => r.id)
|
|
3366
|
+
);
|
|
3367
|
+
});
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
});
|
|
3371
|
+
|
|
3372
|
+
// Complex Operations
|
|
3373
|
+
describe("Complex Operations", () => {
|
|
3374
|
+
describe("Logical Operations", () => {
|
|
3375
|
+
test("conditional - and", async () => {
|
|
3376
|
+
const queryPlan = await cerbos.planResources({
|
|
3377
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3378
|
+
resource: { kind: "resource" },
|
|
3379
|
+
action: "and",
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
3383
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
3384
|
+
{
|
|
3385
|
+
operator: "and",
|
|
3386
|
+
operands: [
|
|
3387
|
+
{
|
|
3388
|
+
operator: "eq",
|
|
3389
|
+
operands: [
|
|
3390
|
+
{ name: "request.resource.attr.aBool" },
|
|
3391
|
+
{ value: true },
|
|
3392
|
+
],
|
|
3393
|
+
},
|
|
3394
|
+
{
|
|
3395
|
+
operator: "ne",
|
|
3396
|
+
operands: [
|
|
3397
|
+
{ name: "request.resource.attr.aString" },
|
|
3398
|
+
{ value: "string" },
|
|
3399
|
+
],
|
|
3400
|
+
},
|
|
3401
|
+
],
|
|
3402
|
+
}
|
|
3403
|
+
);
|
|
3404
|
+
|
|
3405
|
+
const result = queryPlanToPrisma({
|
|
3406
|
+
queryPlan,
|
|
3407
|
+
mapper: {
|
|
3408
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
3409
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
3410
|
+
},
|
|
3411
|
+
});
|
|
3412
|
+
|
|
3413
|
+
expect(result).toStrictEqual({
|
|
3414
|
+
kind: PlanKind.CONDITIONAL,
|
|
3415
|
+
filters: {
|
|
3416
|
+
AND: [
|
|
3417
|
+
{
|
|
3418
|
+
aBool: { equals: true },
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
aString: { not: "string" },
|
|
3422
|
+
},
|
|
3423
|
+
],
|
|
3424
|
+
},
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3428
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
const query = await prisma.resource.findMany({
|
|
3432
|
+
where: { ...result.filters },
|
|
3433
|
+
});
|
|
3434
|
+
expect(query).toEqual(
|
|
3435
|
+
fixtureResources
|
|
3436
|
+
.filter((r) => {
|
|
3437
|
+
return r.aBool && r.aString != "string";
|
|
3438
|
+
})
|
|
3439
|
+
.map((f) => ({ ...f, owners: undefined }))
|
|
3440
|
+
);
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
test("conditional - or", async () => {
|
|
3444
|
+
const queryPlan = await cerbos.planResources({
|
|
3445
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3446
|
+
resource: { kind: "resource" },
|
|
3447
|
+
action: "or",
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3450
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
3451
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
3452
|
+
{
|
|
3453
|
+
operator: "or",
|
|
3454
|
+
operands: [
|
|
3455
|
+
{
|
|
3456
|
+
operator: "eq",
|
|
3457
|
+
operands: [
|
|
3458
|
+
{ name: "request.resource.attr.aBool" },
|
|
3459
|
+
{ value: true },
|
|
3460
|
+
],
|
|
3461
|
+
},
|
|
3462
|
+
{
|
|
3463
|
+
operator: "ne",
|
|
3464
|
+
operands: [
|
|
3465
|
+
{ name: "request.resource.attr.aString" },
|
|
3466
|
+
{ value: "string" },
|
|
3467
|
+
],
|
|
3468
|
+
},
|
|
3469
|
+
],
|
|
3470
|
+
}
|
|
3471
|
+
);
|
|
3472
|
+
|
|
3473
|
+
const result = queryPlanToPrisma({
|
|
3474
|
+
queryPlan,
|
|
3475
|
+
mapper: {
|
|
3476
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
3477
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
3478
|
+
},
|
|
3479
|
+
});
|
|
3480
|
+
|
|
3481
|
+
expect(result).toStrictEqual({
|
|
3482
|
+
kind: PlanKind.CONDITIONAL,
|
|
3483
|
+
filters: {
|
|
3484
|
+
OR: [
|
|
3485
|
+
{
|
|
3486
|
+
aBool: { equals: true },
|
|
3487
|
+
},
|
|
3488
|
+
{
|
|
3489
|
+
aString: { not: "string" },
|
|
3490
|
+
},
|
|
3491
|
+
],
|
|
3492
|
+
},
|
|
3493
|
+
});
|
|
3494
|
+
|
|
3495
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3496
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
const query = await prisma.resource.findMany({
|
|
3500
|
+
where: { ...result.filters },
|
|
3501
|
+
});
|
|
3502
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3503
|
+
fixtureResources
|
|
3504
|
+
.filter((r) => {
|
|
3505
|
+
return r.aBool || r.aString != "string";
|
|
3506
|
+
})
|
|
3507
|
+
.map((r) => r.id)
|
|
3508
|
+
);
|
|
3509
|
+
});
|
|
3510
|
+
|
|
3511
|
+
test("conditional - relation multiple all", async () => {
|
|
3512
|
+
const queryPlan = await cerbos.planResources({
|
|
3513
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3514
|
+
resource: { kind: "resource" },
|
|
3515
|
+
action: "relation-multiple-all",
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
3519
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
3520
|
+
{
|
|
3521
|
+
operator: "and",
|
|
3522
|
+
operands: [
|
|
3523
|
+
{
|
|
3524
|
+
operator: "gt",
|
|
3525
|
+
operands: [
|
|
3526
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
3527
|
+
{ value: 1 },
|
|
3528
|
+
],
|
|
3529
|
+
},
|
|
3530
|
+
{
|
|
3531
|
+
operator: "lt",
|
|
3532
|
+
operands: [
|
|
3533
|
+
{ name: "request.resource.attr.nested.aNumber" },
|
|
3534
|
+
{ value: 3 },
|
|
3535
|
+
],
|
|
3536
|
+
},
|
|
3537
|
+
],
|
|
3538
|
+
}
|
|
3539
|
+
);
|
|
3540
|
+
|
|
3541
|
+
const result = queryPlanToPrisma({
|
|
3542
|
+
queryPlan,
|
|
3543
|
+
mapper: {
|
|
3544
|
+
"request.resource.attr.nested": {
|
|
3545
|
+
relation: {
|
|
3546
|
+
name: "nested",
|
|
3547
|
+
type: "one",
|
|
3548
|
+
},
|
|
3549
|
+
},
|
|
3550
|
+
},
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
expect(result).toStrictEqual({
|
|
3554
|
+
kind: PlanKind.CONDITIONAL,
|
|
3555
|
+
filters: {
|
|
3556
|
+
AND: [
|
|
3557
|
+
{
|
|
3558
|
+
nested: {
|
|
3559
|
+
is: {
|
|
3560
|
+
aNumber: { gt: 1 },
|
|
3561
|
+
},
|
|
3562
|
+
},
|
|
3563
|
+
},
|
|
3564
|
+
{
|
|
3565
|
+
nested: {
|
|
3566
|
+
is: {
|
|
3567
|
+
aNumber: { lt: 3 },
|
|
3568
|
+
},
|
|
3569
|
+
},
|
|
3570
|
+
},
|
|
3571
|
+
],
|
|
3572
|
+
},
|
|
3573
|
+
});
|
|
3574
|
+
|
|
3575
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3576
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
const query = await prisma.resource.findMany({
|
|
3580
|
+
where: { ...result.filters },
|
|
3581
|
+
});
|
|
3582
|
+
|
|
3583
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3584
|
+
fixtureResources
|
|
3585
|
+
.filter((r) => {
|
|
3586
|
+
const nestedResource = fixtureNestedResources.find(
|
|
3587
|
+
(n) => n.id === r.nested.connect?.id
|
|
3588
|
+
);
|
|
3589
|
+
return (
|
|
3590
|
+
(nestedResource?.aNumber ?? 0) > 1 &&
|
|
3591
|
+
(nestedResource?.aNumber ?? 0) < 3
|
|
3592
|
+
);
|
|
3593
|
+
})
|
|
3594
|
+
.map((r) => r.id)
|
|
3595
|
+
);
|
|
3596
|
+
});
|
|
3597
|
+
|
|
3598
|
+
test("conditional - relation multiple or", async () => {
|
|
3599
|
+
const queryPlan = await cerbos.planResources({
|
|
3600
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3601
|
+
resource: { kind: "resource" },
|
|
3602
|
+
action: "relation-multiple-or",
|
|
3603
|
+
});
|
|
3604
|
+
|
|
3605
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
3606
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
3607
|
+
{
|
|
3608
|
+
operator: "or",
|
|
3609
|
+
operands: [
|
|
3610
|
+
{
|
|
3611
|
+
operator: "eq",
|
|
3612
|
+
operands: [
|
|
3613
|
+
{ name: "request.resource.attr.createdBy" },
|
|
3614
|
+
{ value: "user1" },
|
|
3615
|
+
],
|
|
3616
|
+
},
|
|
3617
|
+
{
|
|
3618
|
+
operator: "in",
|
|
3619
|
+
operands: [
|
|
3620
|
+
{ value: "user1" },
|
|
3621
|
+
{ name: "request.resource.attr.ownedBy" },
|
|
3622
|
+
],
|
|
3623
|
+
},
|
|
3624
|
+
],
|
|
3625
|
+
}
|
|
3626
|
+
);
|
|
3627
|
+
|
|
3628
|
+
const result = queryPlanToPrisma({
|
|
3629
|
+
queryPlan,
|
|
3630
|
+
mapper: {
|
|
3631
|
+
"request.resource.attr.createdBy": {
|
|
3632
|
+
relation: {
|
|
3633
|
+
name: "createdBy",
|
|
3634
|
+
type: "one",
|
|
3635
|
+
field: "id",
|
|
3636
|
+
},
|
|
3637
|
+
},
|
|
3638
|
+
"request.resource.attr.ownedBy": {
|
|
3639
|
+
relation: {
|
|
3640
|
+
name: "ownedBy",
|
|
3641
|
+
type: "many",
|
|
3642
|
+
field: "id",
|
|
3643
|
+
},
|
|
3644
|
+
},
|
|
3645
|
+
},
|
|
3646
|
+
});
|
|
3647
|
+
|
|
3648
|
+
expect(result).toStrictEqual({
|
|
3649
|
+
kind: PlanKind.CONDITIONAL,
|
|
3650
|
+
filters: {
|
|
3651
|
+
OR: [
|
|
3652
|
+
{
|
|
3653
|
+
createdBy: {
|
|
3654
|
+
is: {
|
|
3655
|
+
id: {
|
|
3656
|
+
equals: "user1",
|
|
3657
|
+
},
|
|
3658
|
+
},
|
|
3659
|
+
},
|
|
3660
|
+
},
|
|
3661
|
+
{
|
|
3662
|
+
ownedBy: {
|
|
3663
|
+
some: {
|
|
3664
|
+
id: "user1",
|
|
3665
|
+
},
|
|
3666
|
+
},
|
|
3667
|
+
},
|
|
3668
|
+
],
|
|
3669
|
+
},
|
|
3670
|
+
});
|
|
3671
|
+
|
|
3672
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3673
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
const query = await prisma.resource.findMany({
|
|
3677
|
+
where: { ...result.filters },
|
|
3678
|
+
});
|
|
3679
|
+
|
|
3680
|
+
// Should return resources where user1 is either creator or owner
|
|
3681
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3682
|
+
fixtureResources
|
|
3683
|
+
.filter((r) => {
|
|
3684
|
+
const createdBy = r.createdBy?.connect?.id;
|
|
3685
|
+
const ownedBy = Array.isArray(r.ownedBy?.connect)
|
|
3686
|
+
? r.ownedBy?.connect.find((o) => o.id == "user1")
|
|
3687
|
+
: undefined;
|
|
3688
|
+
return createdBy == "user1" || ownedBy;
|
|
3689
|
+
})
|
|
3690
|
+
.map((r) => r.id)
|
|
3691
|
+
);
|
|
3692
|
+
});
|
|
3693
|
+
|
|
3694
|
+
test("conditional - relation multiple none", async () => {
|
|
3695
|
+
const queryPlan = await cerbos.planResources({
|
|
3696
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3697
|
+
resource: { kind: "resource" },
|
|
3698
|
+
action: "relation-multiple-none",
|
|
3699
|
+
});
|
|
3700
|
+
|
|
3701
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
3702
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual(
|
|
3703
|
+
{
|
|
3704
|
+
operator: "and",
|
|
3705
|
+
operands: [
|
|
3706
|
+
{
|
|
3707
|
+
operator: "not",
|
|
3708
|
+
operands: [
|
|
3709
|
+
{
|
|
3710
|
+
operator: "eq",
|
|
3711
|
+
operands: [
|
|
3712
|
+
{ name: "request.resource.attr.createdBy" },
|
|
3713
|
+
{ value: "user1" },
|
|
3714
|
+
],
|
|
3715
|
+
},
|
|
3716
|
+
],
|
|
3717
|
+
},
|
|
3718
|
+
{
|
|
3719
|
+
operator: "not",
|
|
3720
|
+
operands: [
|
|
3721
|
+
{
|
|
3722
|
+
operator: "in",
|
|
3723
|
+
operands: [
|
|
3724
|
+
{ value: "public" },
|
|
3725
|
+
{ name: "request.resource.attr.tags" },
|
|
3726
|
+
],
|
|
3727
|
+
},
|
|
3728
|
+
],
|
|
3729
|
+
},
|
|
3730
|
+
],
|
|
3731
|
+
}
|
|
3732
|
+
);
|
|
3733
|
+
|
|
3734
|
+
const result = queryPlanToPrisma({
|
|
3735
|
+
queryPlan,
|
|
3736
|
+
mapper: {
|
|
3737
|
+
"request.resource.attr.createdBy": {
|
|
3738
|
+
relation: {
|
|
3739
|
+
name: "createdBy",
|
|
3740
|
+
type: "one",
|
|
3741
|
+
field: "id",
|
|
3742
|
+
},
|
|
3743
|
+
},
|
|
3744
|
+
"request.resource.attr.tags": {
|
|
3745
|
+
relation: {
|
|
3746
|
+
name: "tags",
|
|
3747
|
+
type: "many",
|
|
3748
|
+
field: "name",
|
|
3749
|
+
},
|
|
3750
|
+
},
|
|
3751
|
+
},
|
|
3752
|
+
});
|
|
3753
|
+
|
|
3754
|
+
expect(result).toStrictEqual({
|
|
3755
|
+
kind: PlanKind.CONDITIONAL,
|
|
3756
|
+
filters: {
|
|
3757
|
+
AND: [
|
|
3758
|
+
{
|
|
3759
|
+
NOT: {
|
|
3760
|
+
createdBy: {
|
|
3761
|
+
is: {
|
|
3762
|
+
id: {
|
|
3763
|
+
equals: "user1",
|
|
3764
|
+
},
|
|
3765
|
+
},
|
|
3766
|
+
},
|
|
3767
|
+
},
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
NOT: {
|
|
3771
|
+
tags: {
|
|
3772
|
+
some: {
|
|
3773
|
+
name: "public",
|
|
3774
|
+
},
|
|
3775
|
+
},
|
|
3776
|
+
},
|
|
3777
|
+
},
|
|
3778
|
+
],
|
|
3779
|
+
},
|
|
3780
|
+
});
|
|
3781
|
+
|
|
3782
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3783
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
const query = await prisma.resource.findMany({
|
|
3787
|
+
where: { ...result.filters },
|
|
3788
|
+
});
|
|
3789
|
+
|
|
3790
|
+
// Should return resources where user1 is creator AND has public tag
|
|
3791
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3792
|
+
fixtureResources
|
|
3793
|
+
.filter((r) => {
|
|
3794
|
+
const createdBy = r.createdBy?.connect?.id;
|
|
3795
|
+
const hasPublicTag =
|
|
3796
|
+
Array.isArray(r.tags?.connect) &&
|
|
3797
|
+
r.tags?.connect.find((t) => t.id === "tag1"); // tag1 is public
|
|
3798
|
+
return !(createdBy === "user1" && hasPublicTag);
|
|
3799
|
+
})
|
|
3800
|
+
.map((r) => r.id)
|
|
3801
|
+
);
|
|
3802
|
+
});
|
|
3803
|
+
});
|
|
3804
|
+
|
|
3805
|
+
describe("Collection Operations", () => {
|
|
3806
|
+
test("conditional - exists on nested collection", async () => {
|
|
3807
|
+
const queryPlan = await cerbos.planResources({
|
|
3808
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3809
|
+
resource: { kind: "resource" },
|
|
3810
|
+
action: "exists-nested-collection",
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
const result = queryPlanToPrisma({
|
|
3814
|
+
queryPlan,
|
|
3815
|
+
mapper: {
|
|
3816
|
+
"request.resource.attr.categories": {
|
|
3817
|
+
relation: {
|
|
3818
|
+
name: "categories",
|
|
3819
|
+
type: "many",
|
|
3820
|
+
fields: {
|
|
3821
|
+
name: { field: "name" },
|
|
3822
|
+
subCategories: {
|
|
3823
|
+
relation: {
|
|
3824
|
+
name: "subCategories",
|
|
3825
|
+
type: "many",
|
|
3826
|
+
fields: {
|
|
3827
|
+
name: { field: "name" },
|
|
3828
|
+
},
|
|
3829
|
+
},
|
|
3830
|
+
},
|
|
3831
|
+
},
|
|
3832
|
+
},
|
|
3833
|
+
},
|
|
3834
|
+
},
|
|
3835
|
+
});
|
|
3836
|
+
|
|
3837
|
+
expect(result).toStrictEqual({
|
|
3838
|
+
kind: PlanKind.CONDITIONAL,
|
|
3839
|
+
filters: {
|
|
3840
|
+
categories: {
|
|
3841
|
+
some: {
|
|
3842
|
+
AND: [
|
|
3843
|
+
{ name: { equals: "business" } },
|
|
3844
|
+
{
|
|
3845
|
+
subCategories: {
|
|
3846
|
+
some: {
|
|
3847
|
+
name: { equals: "finance" },
|
|
3848
|
+
},
|
|
3849
|
+
},
|
|
3850
|
+
},
|
|
3851
|
+
],
|
|
3852
|
+
},
|
|
3853
|
+
},
|
|
3854
|
+
},
|
|
3855
|
+
});
|
|
3856
|
+
|
|
3857
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
3858
|
+
throw new Error("Expected CONDITIONAL result");
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
const query = await prisma.resource.findMany({
|
|
3862
|
+
where: { ...result.filters },
|
|
3863
|
+
});
|
|
3864
|
+
|
|
3865
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
3866
|
+
fixtureResources
|
|
3867
|
+
.filter((r) => {
|
|
3868
|
+
const categoryConnections = r.categories
|
|
3869
|
+
?.connect as Prisma.CategoryWhereUniqueInput[];
|
|
3870
|
+
return categoryConnections?.some((categoryConnection) => {
|
|
3871
|
+
const category = fixtureCategories.find(
|
|
3872
|
+
(fc) => fc.id === categoryConnection.id
|
|
3873
|
+
);
|
|
3874
|
+
return (
|
|
3875
|
+
category?.name === "business" &&
|
|
3876
|
+
(
|
|
3877
|
+
category?.subCategories
|
|
3878
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
3879
|
+
)?.some(
|
|
3880
|
+
(sc) =>
|
|
3881
|
+
fixtureSubCategories.find((fsc) => fsc.id === sc.id)
|
|
3882
|
+
?.name === "finance"
|
|
3883
|
+
)
|
|
3884
|
+
);
|
|
3885
|
+
});
|
|
3886
|
+
})
|
|
3887
|
+
.map((r) => r.id)
|
|
3888
|
+
);
|
|
3889
|
+
});
|
|
3890
|
+
|
|
3891
|
+
test("isSet with nested relation", async () => {
|
|
3892
|
+
const queryPlan = await cerbos.planResources({
|
|
3893
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3894
|
+
resource: { kind: "resource" },
|
|
3895
|
+
action: "is-set-nested",
|
|
3896
|
+
});
|
|
3897
|
+
|
|
3898
|
+
const result = queryPlanToPrisma({
|
|
3899
|
+
queryPlan,
|
|
3900
|
+
mapper: {
|
|
3901
|
+
"request.resource.attr.nested.aOptionalString": {
|
|
3902
|
+
relation: {
|
|
3903
|
+
name: "nested",
|
|
3904
|
+
type: "one",
|
|
3905
|
+
fields: {
|
|
3906
|
+
aOptionalString: { field: "aOptionalString" },
|
|
3907
|
+
},
|
|
3908
|
+
},
|
|
3909
|
+
},
|
|
3910
|
+
},
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
expect(result).toStrictEqual({
|
|
3914
|
+
kind: PlanKind.CONDITIONAL,
|
|
3915
|
+
filters: {
|
|
3916
|
+
nested: {
|
|
3917
|
+
is: {
|
|
3918
|
+
aOptionalString: { not: null },
|
|
3919
|
+
},
|
|
3920
|
+
},
|
|
3921
|
+
},
|
|
3922
|
+
});
|
|
3923
|
+
});
|
|
3924
|
+
|
|
3925
|
+
test("hasIntersection with nested collection", async () => {
|
|
3926
|
+
const queryPlan = await cerbos.planResources({
|
|
3927
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3928
|
+
resource: { kind: "resource" },
|
|
3929
|
+
action: "has-intersection-nested",
|
|
3930
|
+
});
|
|
3931
|
+
|
|
3932
|
+
const result = queryPlanToPrisma({
|
|
3933
|
+
queryPlan,
|
|
3934
|
+
mapper: {
|
|
3935
|
+
"request.resource.attr.categories": {
|
|
3936
|
+
relation: {
|
|
3937
|
+
name: "categories",
|
|
3938
|
+
type: "many",
|
|
3939
|
+
fields: {
|
|
3940
|
+
subCategories: {
|
|
3941
|
+
relation: {
|
|
3942
|
+
name: "subCategories",
|
|
3943
|
+
type: "many",
|
|
3944
|
+
fields: {
|
|
3945
|
+
name: { field: "name" },
|
|
3946
|
+
},
|
|
3947
|
+
},
|
|
3948
|
+
},
|
|
3949
|
+
},
|
|
3950
|
+
},
|
|
3951
|
+
},
|
|
3952
|
+
},
|
|
3953
|
+
});
|
|
3954
|
+
|
|
3955
|
+
expect(result).toStrictEqual({
|
|
3956
|
+
kind: PlanKind.CONDITIONAL,
|
|
3957
|
+
filters: {
|
|
3958
|
+
categories: {
|
|
3959
|
+
some: {
|
|
3960
|
+
subCategories: {
|
|
3961
|
+
some: {
|
|
3962
|
+
name: { in: ["finance", "tech"] },
|
|
3963
|
+
},
|
|
3964
|
+
},
|
|
3965
|
+
},
|
|
3966
|
+
},
|
|
3967
|
+
},
|
|
3968
|
+
});
|
|
3969
|
+
});
|
|
3970
|
+
});
|
|
3971
|
+
|
|
3972
|
+
describe("Deep Nesting", () => {
|
|
3973
|
+
test("filter on deeply nested relation", async () => {
|
|
3974
|
+
const queryPlan = await cerbos.planResources({
|
|
3975
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
3976
|
+
resource: { kind: "resource" },
|
|
3977
|
+
action: "filter-deeply-nested",
|
|
3978
|
+
});
|
|
3979
|
+
|
|
3980
|
+
const result = queryPlanToPrisma({
|
|
3981
|
+
queryPlan,
|
|
3982
|
+
mapper: {
|
|
3983
|
+
"request.resource.attr.categories": {
|
|
3984
|
+
relation: {
|
|
3985
|
+
name: "categories",
|
|
3986
|
+
type: "many",
|
|
3987
|
+
fields: {
|
|
3988
|
+
subCategories: {
|
|
3989
|
+
relation: {
|
|
3990
|
+
name: "subCategories",
|
|
3991
|
+
type: "many",
|
|
3992
|
+
fields: {
|
|
3993
|
+
labels: {
|
|
3994
|
+
relation: {
|
|
3995
|
+
name: "labels",
|
|
3996
|
+
type: "many",
|
|
3997
|
+
fields: {
|
|
3998
|
+
name: { field: "name" },
|
|
3999
|
+
},
|
|
4000
|
+
},
|
|
4001
|
+
},
|
|
4002
|
+
},
|
|
4003
|
+
},
|
|
4004
|
+
},
|
|
4005
|
+
},
|
|
4006
|
+
},
|
|
4007
|
+
},
|
|
4008
|
+
},
|
|
4009
|
+
});
|
|
4010
|
+
|
|
4011
|
+
expect(result).toStrictEqual({
|
|
4012
|
+
kind: PlanKind.CONDITIONAL,
|
|
4013
|
+
filters: {
|
|
4014
|
+
categories: {
|
|
4015
|
+
some: {
|
|
4016
|
+
subCategories: {
|
|
4017
|
+
some: {
|
|
4018
|
+
labels: {
|
|
4019
|
+
some: {
|
|
4020
|
+
name: { equals: "important" },
|
|
4021
|
+
},
|
|
4022
|
+
},
|
|
4023
|
+
},
|
|
4024
|
+
},
|
|
4025
|
+
},
|
|
4026
|
+
},
|
|
4027
|
+
},
|
|
4028
|
+
});
|
|
4029
|
+
});
|
|
4030
|
+
|
|
4031
|
+
test("map on deeply nested relation", async () => {
|
|
4032
|
+
const queryPlan = await cerbos.planResources({
|
|
4033
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4034
|
+
resource: { kind: "resource" },
|
|
4035
|
+
action: "map-deeply-nested",
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
const result = queryPlanToPrisma({
|
|
4039
|
+
queryPlan,
|
|
4040
|
+
mapper: {
|
|
4041
|
+
"request.resource.attr.categories": {
|
|
4042
|
+
relation: {
|
|
4043
|
+
name: "categories",
|
|
4044
|
+
type: "many",
|
|
4045
|
+
fields: {
|
|
4046
|
+
subCategories: {
|
|
4047
|
+
relation: {
|
|
4048
|
+
name: "subCategories",
|
|
4049
|
+
type: "many",
|
|
4050
|
+
fields: {
|
|
4051
|
+
name: { field: "name" },
|
|
4052
|
+
},
|
|
4053
|
+
},
|
|
4054
|
+
},
|
|
4055
|
+
},
|
|
4056
|
+
},
|
|
4057
|
+
},
|
|
4058
|
+
},
|
|
4059
|
+
});
|
|
4060
|
+
|
|
4061
|
+
expect(result).toStrictEqual({
|
|
4062
|
+
kind: PlanKind.CONDITIONAL,
|
|
4063
|
+
filters: {
|
|
4064
|
+
categories: {
|
|
4065
|
+
some: {
|
|
4066
|
+
subCategories: {
|
|
4067
|
+
some: {
|
|
4068
|
+
name: { in: ["finance", "tech"] },
|
|
4069
|
+
},
|
|
4070
|
+
},
|
|
4071
|
+
},
|
|
4072
|
+
},
|
|
4073
|
+
},
|
|
4074
|
+
});
|
|
4075
|
+
});
|
|
4076
|
+
});
|
|
4077
|
+
});
|
|
4078
|
+
|
|
4079
|
+
// Error Handling
|
|
4080
|
+
describe("Error Cases", () => {
|
|
4081
|
+
test("throws error for invalid query plan", () => {
|
|
4082
|
+
const invalidQueryPlan = {
|
|
4083
|
+
kind: "INVALID_KIND" as PlanKind,
|
|
4084
|
+
};
|
|
4085
|
+
|
|
4086
|
+
expect(() =>
|
|
4087
|
+
queryPlanToPrisma({
|
|
4088
|
+
queryPlan: invalidQueryPlan as unknown as PlanResourcesResponse,
|
|
4089
|
+
mapper: {},
|
|
4090
|
+
})
|
|
4091
|
+
).toThrow("Invalid query plan.");
|
|
4092
|
+
});
|
|
4093
|
+
|
|
4094
|
+
test("throws error for invalid expression structure", () => {
|
|
4095
|
+
const invalidQueryPlan = {
|
|
4096
|
+
kind: PlanKind.CONDITIONAL,
|
|
4097
|
+
condition: {
|
|
4098
|
+
// Missing operator and operands
|
|
4099
|
+
},
|
|
4100
|
+
};
|
|
4101
|
+
|
|
4102
|
+
expect(() =>
|
|
4103
|
+
queryPlanToPrisma({
|
|
4104
|
+
queryPlan: invalidQueryPlan as unknown as PlanResourcesResponse,
|
|
4105
|
+
mapper: {},
|
|
4106
|
+
})
|
|
4107
|
+
).toThrow("Invalid Cerbos expression structure");
|
|
4108
|
+
});
|
|
4109
|
+
|
|
4110
|
+
test("throws error for unsupported operator", () => {
|
|
4111
|
+
const invalidQueryPlan = {
|
|
4112
|
+
kind: PlanKind.CONDITIONAL,
|
|
4113
|
+
condition: {
|
|
4114
|
+
operator: "unsupported",
|
|
4115
|
+
operands: [],
|
|
4116
|
+
},
|
|
4117
|
+
};
|
|
4118
|
+
|
|
4119
|
+
expect(() =>
|
|
4120
|
+
queryPlanToPrisma({
|
|
4121
|
+
queryPlan: invalidQueryPlan as unknown as PlanResourcesResponse,
|
|
4122
|
+
mapper: {},
|
|
4123
|
+
})
|
|
4124
|
+
).toThrow("Unsupported operator: unsupported");
|
|
4125
|
+
});
|
|
4126
|
+
|
|
4127
|
+
test("throws error for invalid operand structure", () => {
|
|
4128
|
+
const invalidQueryPlan = {
|
|
4129
|
+
kind: PlanKind.CONDITIONAL,
|
|
4130
|
+
condition: {
|
|
4131
|
+
operator: "eq",
|
|
4132
|
+
operands: [
|
|
4133
|
+
{}, // Invalid operand without name or value
|
|
4134
|
+
{ value: "test" },
|
|
4135
|
+
],
|
|
4136
|
+
},
|
|
4137
|
+
};
|
|
4138
|
+
|
|
4139
|
+
expect(() =>
|
|
4140
|
+
queryPlanToPrisma({
|
|
4141
|
+
queryPlan: invalidQueryPlan as unknown as PlanResourcesResponse,
|
|
4142
|
+
mapper: {},
|
|
4143
|
+
})
|
|
4144
|
+
).toThrow("No valid left operand found");
|
|
4145
|
+
});
|
|
4146
|
+
});
|
|
4147
|
+
|
|
4148
|
+
// Utility Tests
|
|
4149
|
+
describe("Mapper Functions", () => {
|
|
4150
|
+
test("function mapper for field names", async () => {
|
|
4151
|
+
const queryPlan = await cerbos.planResources({
|
|
4152
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4153
|
+
resource: { kind: "resource" },
|
|
4154
|
+
action: "equal",
|
|
4155
|
+
});
|
|
4156
|
+
|
|
4157
|
+
const result = queryPlanToPrisma({
|
|
4158
|
+
queryPlan,
|
|
4159
|
+
mapper: (key: string) => ({
|
|
4160
|
+
field: key.replace("request.resource.attr.", ""),
|
|
4161
|
+
}),
|
|
4162
|
+
});
|
|
4163
|
+
|
|
4164
|
+
expect(result).toStrictEqual({
|
|
4165
|
+
kind: PlanKind.CONDITIONAL,
|
|
4166
|
+
filters: { aBool: { equals: true } },
|
|
4167
|
+
});
|
|
4168
|
+
});
|
|
4169
|
+
|
|
4170
|
+
test("function mapper for relations", async () => {
|
|
4171
|
+
const queryPlan = await cerbos.planResources({
|
|
4172
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4173
|
+
resource: { kind: "resource" },
|
|
4174
|
+
action: "relation-is",
|
|
4175
|
+
});
|
|
4176
|
+
|
|
4177
|
+
const result = queryPlanToPrisma({
|
|
4178
|
+
queryPlan,
|
|
4179
|
+
mapper: () => ({
|
|
4180
|
+
relation: {
|
|
4181
|
+
name: "createdBy",
|
|
4182
|
+
type: "one",
|
|
4183
|
+
field: "id",
|
|
4184
|
+
},
|
|
4185
|
+
}),
|
|
4186
|
+
});
|
|
4187
|
+
|
|
4188
|
+
expect(result).toStrictEqual({
|
|
4189
|
+
kind: PlanKind.CONDITIONAL,
|
|
4190
|
+
filters: {
|
|
4191
|
+
createdBy: {
|
|
4192
|
+
is: {
|
|
4193
|
+
id: {
|
|
4194
|
+
equals: "user1",
|
|
4195
|
+
},
|
|
4196
|
+
},
|
|
4197
|
+
},
|
|
4198
|
+
},
|
|
4199
|
+
});
|
|
4200
|
+
});
|
|
4201
|
+
});
|
|
4202
|
+
|
|
4203
|
+
// Integration
|
|
4204
|
+
describe("Integration", () => {
|
|
4205
|
+
test("conditional - kitchen sink", async () => {
|
|
4206
|
+
const queryPlan = await cerbos.planResources({
|
|
4207
|
+
principal: {
|
|
4208
|
+
id: "user1",
|
|
4209
|
+
roles: ["USER"],
|
|
4210
|
+
attr: { tags: ["public", "draft"] },
|
|
4211
|
+
},
|
|
4212
|
+
resource: { kind: "resource" },
|
|
4213
|
+
action: "kitchensink",
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
4217
|
+
|
|
4218
|
+
// expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual({
|
|
4219
|
+
// operator: "filter",
|
|
4220
|
+
// operands: [
|
|
4221
|
+
// {
|
|
4222
|
+
// name: "request.resource.attr.tags",
|
|
4223
|
+
// },
|
|
4224
|
+
// {
|
|
4225
|
+
// operator: "lambda",
|
|
4226
|
+
// operands: [
|
|
4227
|
+
// {
|
|
4228
|
+
// operator: "eq",
|
|
4229
|
+
// operands: [
|
|
4230
|
+
// {
|
|
4231
|
+
// name: "tag.name",
|
|
4232
|
+
// },
|
|
4233
|
+
// {
|
|
4234
|
+
// value: "public",
|
|
4235
|
+
// },
|
|
4236
|
+
// ],
|
|
4237
|
+
// },
|
|
4238
|
+
// {
|
|
4239
|
+
// name: "tag",
|
|
4240
|
+
// },
|
|
4241
|
+
// ],
|
|
4242
|
+
// },
|
|
4243
|
+
// ],
|
|
4244
|
+
// });
|
|
4245
|
+
|
|
4246
|
+
const result = queryPlanToPrisma({
|
|
4247
|
+
queryPlan,
|
|
4248
|
+
mapper: {
|
|
4249
|
+
"request.resource.attr.aOptionalString": { field: "aOptionalString" },
|
|
4250
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
4251
|
+
"request.resource.attr.aString": { field: "aString" },
|
|
4252
|
+
"request.resource.attr.tags": {
|
|
4253
|
+
relation: {
|
|
4254
|
+
name: "tags",
|
|
4255
|
+
type: "many",
|
|
4256
|
+
},
|
|
4257
|
+
},
|
|
4258
|
+
"request.resource.attr.nested": {
|
|
4259
|
+
relation: {
|
|
4260
|
+
name: "nested",
|
|
4261
|
+
type: "one",
|
|
4262
|
+
fields: {
|
|
4263
|
+
nextlevel: {
|
|
4264
|
+
relation: {
|
|
4265
|
+
name: "nextlevel",
|
|
4266
|
+
type: "one",
|
|
4267
|
+
fields: {
|
|
4268
|
+
aBool: { field: "aBool" },
|
|
4269
|
+
},
|
|
4270
|
+
},
|
|
4271
|
+
},
|
|
4272
|
+
},
|
|
4273
|
+
},
|
|
4274
|
+
},
|
|
4275
|
+
},
|
|
4276
|
+
});
|
|
4277
|
+
|
|
4278
|
+
// expect(result).toStrictEqual({
|
|
4279
|
+
// kind: PlanKind.CONDITIONAL,
|
|
4280
|
+
// filters: {
|
|
4281
|
+
// tags: {
|
|
4282
|
+
// some: {
|
|
4283
|
+
// name: { equals: "public" },
|
|
4284
|
+
// },
|
|
4285
|
+
// },
|
|
4286
|
+
// },
|
|
4287
|
+
// });
|
|
4288
|
+
|
|
4289
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4290
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
// console.log(JSON.stringify(result, null, 2));
|
|
4294
|
+
await prisma.resource.findMany({
|
|
4295
|
+
where: { ...result.filters },
|
|
4296
|
+
});
|
|
4297
|
+
|
|
4298
|
+
// expect(query.map((r) => r.id)).toEqual(
|
|
4299
|
+
// fixtureResources
|
|
4300
|
+
// .filter(
|
|
4301
|
+
// (a) =>
|
|
4302
|
+
// Array.isArray(a.tags?.connect) &&
|
|
4303
|
+
// a.tags?.connect
|
|
4304
|
+
// .map((t) => {
|
|
4305
|
+
// return fixtureTags.find((f) => f.id === t.id);
|
|
4306
|
+
// })
|
|
4307
|
+
// .filter((t) => t?.name === "public").length > 0
|
|
4308
|
+
// )
|
|
4309
|
+
// .map((r) => r.id)
|
|
4310
|
+
// );
|
|
4311
|
+
});
|
|
4312
|
+
});
|
|
4313
|
+
|
|
4314
|
+
describe("Deep Nested Relations", () => {
|
|
4315
|
+
test("conditional - deeply nested many to many category label", async () => {
|
|
4316
|
+
const queryPlan = await cerbos.planResources({
|
|
4317
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4318
|
+
resource: { kind: "resource" },
|
|
4319
|
+
action: "deep-nested-category-label",
|
|
4320
|
+
});
|
|
4321
|
+
|
|
4322
|
+
expect(queryPlan.kind).toEqual(PlanKind.CONDITIONAL);
|
|
4323
|
+
|
|
4324
|
+
expect((queryPlan as PlanResourcesConditionalResponse).condition).toEqual({
|
|
4325
|
+
operator: "exists",
|
|
4326
|
+
operands: [
|
|
4327
|
+
{
|
|
4328
|
+
name: "request.resource.attr.categories",
|
|
4329
|
+
},
|
|
4330
|
+
{
|
|
4331
|
+
operator: "lambda",
|
|
4332
|
+
operands: [
|
|
4333
|
+
{
|
|
4334
|
+
operator: "exists",
|
|
4335
|
+
operands: [
|
|
4336
|
+
{
|
|
4337
|
+
name: "cat.subCategories",
|
|
4338
|
+
},
|
|
4339
|
+
{
|
|
4340
|
+
operator: "lambda",
|
|
4341
|
+
operands: [
|
|
4342
|
+
{
|
|
4343
|
+
operator: "exists",
|
|
4344
|
+
operands: [
|
|
4345
|
+
{
|
|
4346
|
+
name: "sub.labels",
|
|
4347
|
+
},
|
|
4348
|
+
{
|
|
4349
|
+
operator: "lambda",
|
|
4350
|
+
operands: [
|
|
4351
|
+
{
|
|
4352
|
+
operator: "eq",
|
|
4353
|
+
operands: [
|
|
4354
|
+
{
|
|
4355
|
+
name: "label.name",
|
|
4356
|
+
},
|
|
4357
|
+
{
|
|
4358
|
+
value: "important",
|
|
4359
|
+
},
|
|
4360
|
+
],
|
|
4361
|
+
},
|
|
4362
|
+
{
|
|
4363
|
+
name: "label",
|
|
4364
|
+
},
|
|
4365
|
+
],
|
|
4366
|
+
},
|
|
4367
|
+
],
|
|
4368
|
+
},
|
|
4369
|
+
{
|
|
4370
|
+
name: "sub",
|
|
4371
|
+
},
|
|
4372
|
+
],
|
|
4373
|
+
},
|
|
4374
|
+
],
|
|
4375
|
+
},
|
|
4376
|
+
{
|
|
4377
|
+
name: "cat",
|
|
4378
|
+
},
|
|
4379
|
+
],
|
|
4380
|
+
},
|
|
4381
|
+
],
|
|
4382
|
+
});
|
|
4383
|
+
|
|
4384
|
+
const result = queryPlanToPrisma({
|
|
4385
|
+
queryPlan,
|
|
4386
|
+
mapper: {
|
|
4387
|
+
"request.resource.attr.categories": {
|
|
4388
|
+
relation: {
|
|
4389
|
+
name: "categories",
|
|
4390
|
+
type: "many",
|
|
4391
|
+
fields: {
|
|
4392
|
+
subCategories: {
|
|
4393
|
+
relation: {
|
|
4394
|
+
name: "subCategories",
|
|
4395
|
+
type: "many",
|
|
4396
|
+
fields: {
|
|
4397
|
+
labels: {
|
|
4398
|
+
relation: {
|
|
4399
|
+
name: "labels",
|
|
4400
|
+
type: "many",
|
|
4401
|
+
fields: {
|
|
4402
|
+
name: { field: "name" },
|
|
4403
|
+
},
|
|
4404
|
+
},
|
|
4405
|
+
},
|
|
4406
|
+
},
|
|
4407
|
+
},
|
|
4408
|
+
},
|
|
4409
|
+
},
|
|
4410
|
+
},
|
|
4411
|
+
},
|
|
4412
|
+
},
|
|
4413
|
+
});
|
|
4414
|
+
|
|
4415
|
+
expect(result).toStrictEqual({
|
|
4416
|
+
kind: PlanKind.CONDITIONAL,
|
|
4417
|
+
filters: {
|
|
4418
|
+
categories: {
|
|
4419
|
+
some: {
|
|
4420
|
+
subCategories: {
|
|
4421
|
+
some: {
|
|
4422
|
+
labels: {
|
|
4423
|
+
some: {
|
|
4424
|
+
name: { equals: "important" },
|
|
4425
|
+
},
|
|
4426
|
+
},
|
|
4427
|
+
},
|
|
4428
|
+
},
|
|
4429
|
+
},
|
|
4430
|
+
},
|
|
4431
|
+
},
|
|
4432
|
+
});
|
|
4433
|
+
|
|
4434
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4435
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
const query = await prisma.resource.findMany({
|
|
4439
|
+
where: { ...result.filters },
|
|
4440
|
+
include: {
|
|
4441
|
+
categories: {
|
|
4442
|
+
include: {
|
|
4443
|
+
subCategories: {
|
|
4444
|
+
include: {
|
|
4445
|
+
labels: true,
|
|
4446
|
+
},
|
|
4447
|
+
},
|
|
4448
|
+
},
|
|
4449
|
+
},
|
|
4450
|
+
},
|
|
4451
|
+
});
|
|
4452
|
+
|
|
4453
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
4454
|
+
fixtureResources
|
|
4455
|
+
.filter((r) => {
|
|
4456
|
+
return (r.categories?.connect as Prisma.CategoryWhereUniqueInput[])
|
|
4457
|
+
.map((c) => {
|
|
4458
|
+
return fixtureCategories.find((fc) => fc.id === c.id);
|
|
4459
|
+
})
|
|
4460
|
+
.some((c) => {
|
|
4461
|
+
return (
|
|
4462
|
+
c?.subCategories
|
|
4463
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
4464
|
+
)
|
|
4465
|
+
.map((sc) => {
|
|
4466
|
+
return fixtureSubCategories.find((fsc) => fsc.id === sc.id);
|
|
4467
|
+
})
|
|
4468
|
+
.some((sc) => {
|
|
4469
|
+
return (sc?.labels?.connect as Prisma.LabelWhereUniqueInput[])
|
|
4470
|
+
.map((l) => {
|
|
4471
|
+
return fixtureLabels.find((fl) => fl.id === l.id);
|
|
4472
|
+
})
|
|
4473
|
+
.some((l) => l?.name === "important");
|
|
4474
|
+
});
|
|
4475
|
+
});
|
|
4476
|
+
})
|
|
4477
|
+
.map((r) => r.id)
|
|
4478
|
+
);
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4481
|
+
test("conditional - deep nested exists with multiple conditions", async () => {
|
|
4482
|
+
const queryPlan = await cerbos.planResources({
|
|
4483
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4484
|
+
resource: { kind: "resource" },
|
|
4485
|
+
action: "deep-nested-exists",
|
|
4486
|
+
});
|
|
4487
|
+
|
|
4488
|
+
const result = queryPlanToPrisma({
|
|
4489
|
+
queryPlan,
|
|
4490
|
+
mapper: {
|
|
4491
|
+
"request.resource.attr.categories": {
|
|
4492
|
+
relation: {
|
|
4493
|
+
name: "categories",
|
|
4494
|
+
type: "many",
|
|
4495
|
+
fields: {
|
|
4496
|
+
subCategories: {
|
|
4497
|
+
relation: {
|
|
4498
|
+
name: "subCategories",
|
|
4499
|
+
type: "many",
|
|
4500
|
+
field: "name",
|
|
4501
|
+
},
|
|
4502
|
+
},
|
|
4503
|
+
},
|
|
4504
|
+
},
|
|
4505
|
+
},
|
|
4506
|
+
},
|
|
4507
|
+
});
|
|
4508
|
+
|
|
4509
|
+
expect(result).toStrictEqual({
|
|
4510
|
+
kind: PlanKind.CONDITIONAL,
|
|
4511
|
+
filters: {
|
|
4512
|
+
categories: {
|
|
4513
|
+
some: {
|
|
4514
|
+
AND: [
|
|
4515
|
+
{ name: { equals: "business" } },
|
|
4516
|
+
{
|
|
4517
|
+
subCategories: {
|
|
4518
|
+
some: {
|
|
4519
|
+
name: { equals: "finance" },
|
|
4520
|
+
},
|
|
4521
|
+
},
|
|
4522
|
+
},
|
|
4523
|
+
],
|
|
4524
|
+
},
|
|
4525
|
+
},
|
|
4526
|
+
},
|
|
4527
|
+
});
|
|
4528
|
+
|
|
4529
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4530
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
const query = await prisma.resource.findMany({
|
|
4534
|
+
where: { ...result.filters },
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
4538
|
+
fixtureResources
|
|
4539
|
+
.filter((r) => {
|
|
4540
|
+
const categoryConnections = r.categories
|
|
4541
|
+
?.connect as Prisma.CategoryWhereUniqueInput[];
|
|
4542
|
+
return (
|
|
4543
|
+
categoryConnections?.some((categoryConnection) => {
|
|
4544
|
+
const category = fixtureCategories.find(
|
|
4545
|
+
(fc) => fc.id === categoryConnection.id
|
|
4546
|
+
);
|
|
4547
|
+
return (
|
|
4548
|
+
category?.name === "business" &&
|
|
4549
|
+
(
|
|
4550
|
+
category?.subCategories
|
|
4551
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
4552
|
+
)?.some(
|
|
4553
|
+
(sc) =>
|
|
4554
|
+
fixtureSubCategories.find((fsc) => fsc.id === sc.id)
|
|
4555
|
+
?.name === "finance"
|
|
4556
|
+
)
|
|
4557
|
+
);
|
|
4558
|
+
}) ?? false
|
|
4559
|
+
);
|
|
4560
|
+
})
|
|
4561
|
+
.map((r) => r.id)
|
|
4562
|
+
);
|
|
4563
|
+
});
|
|
4564
|
+
});
|
|
4565
|
+
|
|
4566
|
+
describe("Nested Relations String Operations", () => {
|
|
4567
|
+
test("conditional - nested contains", async () => {
|
|
4568
|
+
const queryPlan = await cerbos.planResources({
|
|
4569
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4570
|
+
resource: { kind: "resource" },
|
|
4571
|
+
action: "nested-contains",
|
|
4572
|
+
});
|
|
4573
|
+
|
|
4574
|
+
const result = queryPlanToPrisma({
|
|
4575
|
+
queryPlan,
|
|
4576
|
+
mapper: {
|
|
4577
|
+
"request.resource.attr.nested": {
|
|
4578
|
+
relation: {
|
|
4579
|
+
name: "nested",
|
|
4580
|
+
type: "one",
|
|
4581
|
+
fields: {
|
|
4582
|
+
aString: { field: "aString" },
|
|
4583
|
+
},
|
|
4584
|
+
},
|
|
4585
|
+
},
|
|
4586
|
+
},
|
|
4587
|
+
});
|
|
4588
|
+
|
|
4589
|
+
expect(result).toStrictEqual({
|
|
4590
|
+
kind: PlanKind.CONDITIONAL,
|
|
4591
|
+
filters: {
|
|
4592
|
+
nested: {
|
|
4593
|
+
is: {
|
|
4594
|
+
aString: { contains: "str" },
|
|
4595
|
+
},
|
|
4596
|
+
},
|
|
4597
|
+
},
|
|
4598
|
+
});
|
|
4599
|
+
|
|
4600
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4601
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
const query = await prisma.resource.findMany({
|
|
4605
|
+
where: { ...result.filters },
|
|
4606
|
+
});
|
|
4607
|
+
|
|
4608
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
4609
|
+
fixtureResources
|
|
4610
|
+
.filter((r) => {
|
|
4611
|
+
const nested = fixtureNestedResources.find(
|
|
4612
|
+
(n) => n.id === r.nested.connect?.id
|
|
4613
|
+
);
|
|
4614
|
+
return nested?.aString.includes("str");
|
|
4615
|
+
})
|
|
4616
|
+
.map((r) => r.id)
|
|
4617
|
+
);
|
|
4618
|
+
});
|
|
4619
|
+
|
|
4620
|
+
test("conditional - deeply nested startsWith", async () => {
|
|
4621
|
+
const queryPlan = await cerbos.planResources({
|
|
4622
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4623
|
+
resource: { kind: "resource" },
|
|
4624
|
+
action: "deeply-nested-starts-with",
|
|
4625
|
+
});
|
|
4626
|
+
|
|
4627
|
+
const result = queryPlanToPrisma({
|
|
4628
|
+
queryPlan,
|
|
4629
|
+
mapper: {
|
|
4630
|
+
"request.resource.attr.nested": {
|
|
4631
|
+
relation: {
|
|
4632
|
+
name: "nested",
|
|
4633
|
+
type: "one",
|
|
4634
|
+
fields: {
|
|
4635
|
+
nextlevel: {
|
|
4636
|
+
relation: {
|
|
4637
|
+
name: "nextlevel",
|
|
4638
|
+
type: "one",
|
|
4639
|
+
fields: {
|
|
4640
|
+
aString: { field: "aString" },
|
|
4641
|
+
},
|
|
4642
|
+
},
|
|
4643
|
+
},
|
|
4644
|
+
},
|
|
4645
|
+
},
|
|
4646
|
+
},
|
|
4647
|
+
},
|
|
4648
|
+
});
|
|
4649
|
+
|
|
4650
|
+
expect(result).toStrictEqual({
|
|
4651
|
+
kind: PlanKind.CONDITIONAL,
|
|
4652
|
+
filters: {
|
|
4653
|
+
nested: {
|
|
4654
|
+
is: {
|
|
4655
|
+
nextlevel: {
|
|
4656
|
+
is: {
|
|
4657
|
+
aString: { startsWith: "str" },
|
|
4658
|
+
},
|
|
4659
|
+
},
|
|
4660
|
+
},
|
|
4661
|
+
},
|
|
4662
|
+
},
|
|
4663
|
+
});
|
|
4664
|
+
|
|
4665
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4666
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
const query = await prisma.resource.findMany({
|
|
4670
|
+
where: { ...result.filters },
|
|
4671
|
+
});
|
|
4672
|
+
|
|
4673
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
4674
|
+
fixtureResources
|
|
4675
|
+
.filter((r) => {
|
|
4676
|
+
const nested = fixtureNestedResources.find(
|
|
4677
|
+
(n) => n.id === r.nested.connect?.id
|
|
4678
|
+
);
|
|
4679
|
+
const nextLevel = fixtureNextLevelResources.find(
|
|
4680
|
+
(nl) => nl.id === nested?.nextlevel.connect?.id
|
|
4681
|
+
);
|
|
4682
|
+
return nextLevel?.aString.startsWith("str");
|
|
4683
|
+
})
|
|
4684
|
+
.map((r) => r.id)
|
|
4685
|
+
);
|
|
4686
|
+
});
|
|
4687
|
+
});
|
|
4688
|
+
|
|
4689
|
+
describe("Collection Operations with Nested Relations", () => {
|
|
4690
|
+
test("conditional - exists on nested collection", async () => {
|
|
4691
|
+
const queryPlan = await cerbos.planResources({
|
|
4692
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4693
|
+
resource: { kind: "resource" },
|
|
4694
|
+
action: "exists-nested-collection",
|
|
4695
|
+
});
|
|
4696
|
+
|
|
4697
|
+
const result = queryPlanToPrisma({
|
|
4698
|
+
queryPlan,
|
|
4699
|
+
mapper: {
|
|
4700
|
+
"request.resource.attr.categories": {
|
|
4701
|
+
relation: {
|
|
4702
|
+
name: "categories",
|
|
4703
|
+
type: "many",
|
|
4704
|
+
fields: {
|
|
4705
|
+
name: { field: "name" },
|
|
4706
|
+
subCategories: {
|
|
4707
|
+
relation: {
|
|
4708
|
+
name: "subCategories",
|
|
4709
|
+
type: "many",
|
|
4710
|
+
fields: {
|
|
4711
|
+
name: { field: "name" },
|
|
4712
|
+
},
|
|
4713
|
+
},
|
|
4714
|
+
},
|
|
4715
|
+
},
|
|
4716
|
+
},
|
|
4717
|
+
},
|
|
4718
|
+
},
|
|
4719
|
+
});
|
|
4720
|
+
|
|
4721
|
+
expect(result).toStrictEqual({
|
|
4722
|
+
kind: PlanKind.CONDITIONAL,
|
|
4723
|
+
filters: {
|
|
4724
|
+
categories: {
|
|
4725
|
+
some: {
|
|
4726
|
+
AND: [
|
|
4727
|
+
{ name: { equals: "business" } },
|
|
4728
|
+
{
|
|
4729
|
+
subCategories: {
|
|
4730
|
+
some: {
|
|
4731
|
+
name: { equals: "finance" },
|
|
4732
|
+
},
|
|
4733
|
+
},
|
|
4734
|
+
},
|
|
4735
|
+
],
|
|
4736
|
+
},
|
|
4737
|
+
},
|
|
4738
|
+
},
|
|
4739
|
+
});
|
|
4740
|
+
|
|
4741
|
+
if (result.kind !== PlanKind.CONDITIONAL) {
|
|
4742
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
const query = await prisma.resource.findMany({
|
|
4746
|
+
where: { ...result.filters },
|
|
4747
|
+
});
|
|
4748
|
+
|
|
4749
|
+
expect(query.map((r) => r.id)).toEqual(
|
|
4750
|
+
fixtureResources
|
|
4751
|
+
.filter((r) => {
|
|
4752
|
+
const categoryConnections = r.categories
|
|
4753
|
+
?.connect as Prisma.CategoryWhereUniqueInput[];
|
|
4754
|
+
return categoryConnections?.some((categoryConnection) => {
|
|
4755
|
+
const category = fixtureCategories.find(
|
|
4756
|
+
(fc) => fc.id === categoryConnection.id
|
|
4757
|
+
);
|
|
4758
|
+
return (
|
|
4759
|
+
category?.name === "business" &&
|
|
4760
|
+
(
|
|
4761
|
+
category?.subCategories
|
|
4762
|
+
?.connect as Prisma.SubCategoryWhereUniqueInput[]
|
|
4763
|
+
)?.some(
|
|
4764
|
+
(sc) =>
|
|
4765
|
+
fixtureSubCategories.find((fsc) => fsc.id === sc.id)?.name ===
|
|
4766
|
+
"finance"
|
|
4767
|
+
)
|
|
4768
|
+
);
|
|
4769
|
+
});
|
|
4770
|
+
})
|
|
4771
|
+
.map((r) => r.id)
|
|
4772
|
+
);
|
|
4773
|
+
});
|
|
4774
|
+
});
|
|
4775
|
+
|
|
4776
|
+
// Types
|
|
4777
|
+
describe("Return Types", () => {
|
|
4778
|
+
test("returns ALWAYS_ALLOWED type", async () => {
|
|
4779
|
+
const queryPlan = await cerbos.planResources({
|
|
4780
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4781
|
+
resource: { kind: "resource" },
|
|
4782
|
+
action: "always-allow",
|
|
4783
|
+
});
|
|
4784
|
+
|
|
4785
|
+
const result = queryPlanToPrisma({
|
|
4786
|
+
queryPlan,
|
|
4787
|
+
});
|
|
4788
|
+
|
|
4789
|
+
expect(result).toStrictEqual({
|
|
4790
|
+
kind: PlanKind.ALWAYS_ALLOWED,
|
|
4791
|
+
});
|
|
4792
|
+
|
|
4793
|
+
// Type assertion check
|
|
4794
|
+
if (result.kind === PlanKind.ALWAYS_ALLOWED) {
|
|
4795
|
+
expect(Object.keys(result)).toEqual(["kind"]);
|
|
4796
|
+
} else {
|
|
4797
|
+
throw new Error("Expected ALWAYS_ALLOWED result");
|
|
4798
|
+
}
|
|
4799
|
+
});
|
|
4800
|
+
|
|
4801
|
+
test("returns ALWAYS_DENIED type", async () => {
|
|
4802
|
+
const queryPlan = await cerbos.planResources({
|
|
4803
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4804
|
+
resource: { kind: "resource" },
|
|
4805
|
+
action: "always-deny",
|
|
4806
|
+
});
|
|
4807
|
+
|
|
4808
|
+
const result = queryPlanToPrisma({
|
|
4809
|
+
queryPlan,
|
|
4810
|
+
});
|
|
4811
|
+
|
|
4812
|
+
expect(result).toStrictEqual({
|
|
4813
|
+
kind: PlanKind.ALWAYS_DENIED,
|
|
4814
|
+
});
|
|
4815
|
+
|
|
4816
|
+
// Type assertion check
|
|
4817
|
+
if (result.kind === PlanKind.ALWAYS_DENIED) {
|
|
4818
|
+
expect(Object.keys(result)).toEqual(["kind"]);
|
|
4819
|
+
} else {
|
|
4820
|
+
throw new Error("Expected ALWAYS_DENIED result");
|
|
4821
|
+
}
|
|
4822
|
+
});
|
|
4823
|
+
|
|
4824
|
+
test("returns CONDITIONAL type with filters", async () => {
|
|
4825
|
+
const queryPlan = await cerbos.planResources({
|
|
4826
|
+
principal: { id: "user1", roles: ["USER"] },
|
|
4827
|
+
resource: { kind: "resource" },
|
|
4828
|
+
action: "equal",
|
|
4829
|
+
});
|
|
4830
|
+
|
|
4831
|
+
const result = queryPlanToPrisma({
|
|
4832
|
+
queryPlan,
|
|
4833
|
+
mapper: {
|
|
4834
|
+
"request.resource.attr.aBool": { field: "aBool" },
|
|
4835
|
+
},
|
|
4836
|
+
});
|
|
4837
|
+
|
|
4838
|
+
expect(result).toStrictEqual({
|
|
4839
|
+
kind: PlanKind.CONDITIONAL,
|
|
4840
|
+
filters: { aBool: { equals: true } },
|
|
4841
|
+
});
|
|
4842
|
+
|
|
4843
|
+
// Type assertion check
|
|
4844
|
+
if (result.kind === PlanKind.CONDITIONAL) {
|
|
4845
|
+
expect(Object.keys(result).sort()).toEqual(["filters", "kind"].sort());
|
|
4846
|
+
expect(typeof result.filters).toBe("object");
|
|
4847
|
+
} else {
|
|
4848
|
+
throw new Error("Expected CONDITIONAL result");
|
|
4849
|
+
}
|
|
4850
|
+
});
|
|
4851
|
+
|
|
4852
|
+
test("validates result type structure at compile time", () => {
|
|
4853
|
+
// Type-level test
|
|
4854
|
+
type ResultType = QueryPlanToPrismaResult;
|
|
4855
|
+
|
|
4856
|
+
// These should compile
|
|
4857
|
+
const allowed: ResultType = { kind: PlanKind.ALWAYS_ALLOWED };
|
|
4858
|
+
const denied: ResultType = { kind: PlanKind.ALWAYS_DENIED };
|
|
4859
|
+
const conditional: ResultType = {
|
|
4860
|
+
kind: PlanKind.CONDITIONAL,
|
|
4861
|
+
filters: { someField: { equals: true } },
|
|
4862
|
+
};
|
|
4863
|
+
|
|
4864
|
+
// Verify the objects exist to prevent unused variable warnings
|
|
4865
|
+
expect(allowed.kind).toBe(PlanKind.ALWAYS_ALLOWED);
|
|
4866
|
+
expect(denied.kind).toBe(PlanKind.ALWAYS_DENIED);
|
|
4867
|
+
expect(conditional.kind).toBe(PlanKind.CONDITIONAL);
|
|
4868
|
+
});
|
|
4869
|
+
});
|