@cerbos/orm-prisma 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,20 @@
2
2
 
3
3
  An adapater library that takes a [Cerbos](https://cerbos.dev) Query Plan ([PlanResources API](https://docs.cerbos.dev/cerbos/latest/api/index.html#resources-query-plan)) response and converts it into a [Prisma](https://prisma.io) where class object. This is designed to work alongside a project using the [Cerbos Javascript SDK](https://github.com/cerbos/cerbos-sdk-javascript).
4
4
 
5
- The following conditions are supported: `and`, `or`, `eq`, `ne`, `lt`, `gt`, `lte`, `gte` and `in`.
5
+ The following conditions are supported: `and`, `or`, `eq`, `ne`, `lt`, `gt`, `lte`, `gte` and `in`, as well as relational filters `some`, `none`, `is` and `isNot`.
6
+
7
+ Not Supported:
8
+
9
+ - `every`
10
+ - `contains`
11
+ - `search`
12
+ - `mode`
13
+ - `startsWith`
14
+ - `endsWith`
15
+ - `isSet`
16
+ - Scalar filters
17
+ - Atomic number operations
18
+ - Composite keys
6
19
 
7
20
  ## Requirements
8
21
  - Cerbos > v0.16
@@ -14,13 +27,39 @@ The following conditions are supported: `and`, `or`, `eq`, `ne`, `lt`, `gt`, `lt
14
27
  npm install @cerbos/orm-prisma
15
28
  ```
16
29
 
17
- This package exports a single function:
30
+ This package exports a function:
18
31
 
19
- ```js
20
- queryPlanToPrisma({ queryPlan, fieldNameMapper }): PrismaCondition
32
+ ```ts
33
+ import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
34
+
35
+ queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper }): {
36
+ kind: PlanKind,
37
+ filters?: any // a filter to pass to the findMany() function of Prisma
38
+ }
21
39
  ```
22
40
 
23
- The function reqiures the full query plan from Cerbos to be passed in an object along with a `fieldNameMapper`.
41
+ where `PlanKind` is:
42
+
43
+ ```ts
44
+ export enum PlanKind {
45
+ /**
46
+ * The specified action is always allowed for the principal on resources matching the input.
47
+ */
48
+ ALWAYS_ALLOWED = "KIND_ALWAYS_ALLOWED",
49
+
50
+ /**
51
+ * The specified action is always denied for the principal on resources matching the input.
52
+ */
53
+ ALWAYS_DENIED = "KIND_ALWAYS_DENIED",
54
+
55
+ /**
56
+ * The specified action is conditionally allowed for the principal on resources matching the input.
57
+ */
58
+ CONDITIONAL = "KIND_CONDITIONAL",
59
+ }
60
+ ```
61
+
62
+ The function reqiures the full query plan from Cerbos to be passed in an object along with a `fieldNameMapper` and option `relationMapper` if the model has relations.
24
63
 
25
64
  A basic implementation can be as simple as:
26
65
 
@@ -28,6 +67,8 @@ A basic implementation can be as simple as:
28
67
  import { GRPC as Cerbos } from "@cerbos/grpc";
29
68
  import { PrismaClient } from "@prisma/client";
30
69
 
70
+ import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";
71
+
31
72
  const prisma = new PrismaClient();
32
73
  const cerbos = new Cerbos("localhost:3592", { tls: false });
33
74
 
@@ -40,18 +81,30 @@ const queryPlan = await cerbos.planResources({
40
81
  })
41
82
 
42
83
  // Generate the prisma filter from the query plan
43
- const filters = queryPlanToPrisma({
84
+ const result = queryPlanToPrisma({
44
85
  queryPlan,
45
86
  fieldNameMapper: {
46
87
  "request.resource.attr.aFieldName": "prismaModelFieldName"
88
+ },
89
+ relationMapper: {
90
+ "request.resource.attr.aRelatedModel": {
91
+ "relation": "aRelatedModel",
92
+ "field": "id" // the column it is joined on
93
+ }
47
94
  }
48
95
  });
49
96
 
97
+ // The query plan says the user would always be denied
98
+ // return empty or throw an error depending on your app.
99
+ if(result.kind == PlanKind.ALWAYS_DENIED) {
100
+ return console.log([]);
101
+ }
102
+
50
103
  // Pass the filters in as where conditions
51
104
  // If you have prexisting where conditions, you can pass them in an AND clause
52
105
  const result = await prisma.myModel.findMany({
53
106
  where: {
54
- AND: filters
107
+ AND: result.filters
55
108
  },
56
109
  });
57
110
 
@@ -68,7 +121,6 @@ const filters = queryPlanToPrisma({
68
121
  }
69
122
  });
70
123
 
71
-
72
124
  //or
73
125
 
74
126
  const filters = queryPlanToPrisma({
@@ -85,6 +137,37 @@ const filters = queryPlanToPrisma({
85
137
  });
86
138
  ```
87
139
 
140
+
141
+ The `relationMapper` is used to convert references to fields which are joins at the database level
142
+
143
+ ```js
144
+ const filters = queryPlanToPrisma({
145
+ queryPlan,
146
+ fieldNameMapper: {},
147
+ relationMapper: {
148
+ "request.resource.attr.aRelatedModel": {
149
+ "relation": "aRelatedModel",
150
+ "field": "id" // the column it is joined on
151
+ }
152
+ }
153
+ });
154
+
155
+ //or
156
+
157
+ const filters = queryPlanToPrisma({
158
+ queryPlan,
159
+ fieldNameMapper: {},
160
+ relationMapper: (fieldName: string): string => {
161
+ if(fieldName.indexOf("request.resource.") > 0) {
162
+ return {
163
+ "relation": fieldName.replace("request.resource.attr.", ""),
164
+ "field": "id" // the column it is joined on
165
+ }
166
+ }
167
+ }
168
+ });
169
+ ```
170
+
88
171
  ## Full Example
89
172
 
90
173
  A full Prisma application making use of this adapater can be found at [https://github.com/cerbos/express-prisma-cerbos](https://github.com/cerbos/express-prisma-cerbos)
@@ -1,9 +1,24 @@
1
- import { PlanResourcesResponse } from "@cerbos/core";
1
+ import { PlanResourcesResponse, PlanKind as PK } from "@cerbos/core";
2
+ export declare type PlanKind = PK;
3
+ export declare const PlanKind: typeof PK;
4
+ declare type FieldMapper = {
5
+ [key: string]: string;
6
+ } | ((key: string) => string);
7
+ declare type Relation = {
8
+ relation: string;
9
+ field: string;
10
+ };
11
+ declare type RelationMapper = {
12
+ [key: string]: Relation;
13
+ } | ((key: string) => Relation);
2
14
  interface QueryPlanToPrismaArgs {
3
15
  queryPlan: PlanResourcesResponse;
4
- fieldNameMapper: {
5
- [key: string]: string;
6
- } | ((key: string) => string);
16
+ fieldNameMapper: FieldMapper;
17
+ relationMapper?: RelationMapper;
7
18
  }
8
- export default function queryPlanToPrisma({ queryPlan, fieldNameMapper, }: QueryPlanToPrismaArgs): any;
19
+ interface QueryPlanToPrismaResult {
20
+ kind: PlanKind;
21
+ filters?: any;
22
+ }
23
+ export declare function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper }: QueryPlanToPrismaArgs): QueryPlanToPrismaResult;
9
24
  export {};
package/lib/cjs/index.js CHANGED
@@ -1,21 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queryPlanToPrisma = exports.PlanKind = void 0;
3
4
  const core_1 = require("@cerbos/core");
4
- function queryPlanToPrisma({ queryPlan, fieldNameMapper, }) {
5
- if (queryPlan.kind === core_1.PlanKind.ALWAYS_ALLOWED)
6
- return {};
7
- if (queryPlan.kind === core_1.PlanKind.ALWAYS_DENIED)
8
- return { "1": { "equals": 0 } };
9
- return mapOperand(queryPlan.condition, (key) => {
5
+ exports.PlanKind = core_1.PlanKind;
6
+ function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper = {} }) {
7
+ const toFieldName = (key) => {
10
8
  if (typeof fieldNameMapper === "function") {
11
9
  return fieldNameMapper(key);
12
10
  }
11
+ else if (fieldNameMapper[key]) {
12
+ return fieldNameMapper[key];
13
+ }
13
14
  else {
14
- return (fieldNameMapper[key] = fieldNameMapper[key] || key);
15
+ return key;
16
+ }
17
+ };
18
+ const toRelationName = (key) => {
19
+ if (typeof relationMapper === "function") {
20
+ return relationMapper(key);
21
+ }
22
+ else if (relationMapper[key]) {
23
+ return relationMapper[key];
15
24
  }
16
- }, {});
25
+ return null;
26
+ };
27
+ switch (queryPlan.kind) {
28
+ case exports.PlanKind.ALWAYS_ALLOWED:
29
+ return {
30
+ kind: exports.PlanKind.ALWAYS_ALLOWED,
31
+ filters: {}
32
+ };
33
+ case exports.PlanKind.ALWAYS_DENIED:
34
+ return {
35
+ kind: exports.PlanKind.ALWAYS_DENIED
36
+ };
37
+ case exports.PlanKind.CONDITIONAL:
38
+ return {
39
+ kind: exports.PlanKind.CONDITIONAL,
40
+ filters: mapOperand(queryPlan.condition, toFieldName, toRelationName)
41
+ };
42
+ default:
43
+ throw Error(`Invalid query plan.`);
44
+ }
17
45
  }
18
- exports.default = queryPlanToPrisma;
46
+ exports.queryPlanToPrisma = queryPlanToPrisma;
19
47
  function isExpression(e) {
20
48
  return e.operator !== undefined;
21
49
  }
@@ -25,58 +53,94 @@ function isValue(e) {
25
53
  function isVariable(e) {
26
54
  return e.variable !== undefined;
27
55
  }
28
- function getOperandName(operands) {
29
- return (operands.find(o => o.hasOwnProperty("name"))).name;
56
+ function getOperandVariable(operands) {
57
+ const op = operands.find(o => o.hasOwnProperty("name"));
58
+ if (!op)
59
+ return;
60
+ return op.name;
30
61
  }
31
62
  function getOperandValue(operands) {
32
- return (operands.find(o => o.hasOwnProperty("value"))).value;
63
+ const op = operands.find(o => isValue(o));
64
+ if (!op)
65
+ return;
66
+ return op.value;
33
67
  }
34
- function mapOperand(operand, getFieldName, output = {}) {
35
- if (isExpression(operand)) {
36
- const { operator, operands } = operand;
37
- switch (operator) {
38
- case "and":
39
- output.AND = operands.map((o) => mapOperand(o, getFieldName, {}));
40
- break;
41
- case "or":
42
- output.OR = operands.map((o) => mapOperand(o, getFieldName, {}));
43
- break;
44
- case "eq":
45
- output[getFieldName(getOperandName(operands))] = {
46
- equals: getOperandValue(operands),
47
- };
48
- break;
49
- case "ne":
50
- output[getFieldName(getOperandName(operands))] = {
51
- not: getOperandValue(operands),
52
- };
53
- break;
54
- case "lt":
55
- output[getFieldName(getOperandName(operands))] = {
56
- lt: getOperandValue(operands),
57
- };
58
- break;
59
- case "gt":
60
- output[getFieldName(getOperandName(operands))] = {
61
- gt: getOperandValue(operands),
62
- };
63
- break;
64
- case "le":
65
- output[getFieldName(getOperandName(operands))] = {
66
- lte: getOperandValue(operands),
67
- };
68
- break;
69
- case "ge":
70
- output[getFieldName(getOperandName(operands))] = {
71
- gte: getOperandValue(operands),
72
- };
73
- break;
74
- case "in":
75
- output[getFieldName(getOperandName(operands))] = {
76
- in: getOperandValue(operands),
77
- };
78
- break;
79
- }
68
+ const OPERATORS = {
69
+ "eq": {
70
+ relationalCondition: "is",
71
+ fieldCondition: "equals"
72
+ },
73
+ "ne": {
74
+ relationalCondition: "isNot",
75
+ fieldCondition: "not"
76
+ },
77
+ "in": {
78
+ relationalCondition: "some",
79
+ fieldCondition: "in"
80
+ },
81
+ "lt": {
82
+ fieldCondition: "lt"
83
+ },
84
+ "gt": {
85
+ fieldCondition: "gt"
86
+ },
87
+ "le": {
88
+ fieldCondition: "lte"
89
+ },
90
+ "ge": {
91
+ fieldCondition: "gte"
92
+ }
93
+ };
94
+ function mapOperand(operand, getFieldName, getRelationName, output = {}) {
95
+ if (!isExpression(operand))
96
+ throw Error(`Query plan did not contain an expression for operand ${operand}`);
97
+ const { operator, operands } = operand;
98
+ // HANDLE NESTING OPERATIONS: AND/OR/NOT
99
+ if (operator == "and") {
100
+ if (operands.length < 2)
101
+ throw Error("Expected atleast 2 operands");
102
+ output.AND = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
103
+ return output;
104
+ }
105
+ if (operator == "or") {
106
+ if (operands.length < 2)
107
+ throw Error("Expected atleast 2 operands");
108
+ output.OR = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
109
+ return output;
110
+ }
111
+ if (operator == "not") {
112
+ if (operands.length > 1)
113
+ throw Error("Expected only one operand");
114
+ output.NOT = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}))[0];
115
+ return output;
116
+ }
117
+ // get the operation parameters
118
+ const operation = OPERATORS[operator];
119
+ if (!operation)
120
+ throw Error(`Unsupported operator ${operator}`);
121
+ const opVariable = getOperandVariable(operands);
122
+ if (!opVariable)
123
+ throw Error(`Unexpected variable ${operands}`);
124
+ const opValue = getOperandValue(operands);
125
+ const relation = getRelationName(opVariable);
126
+ const fieldName = getFieldName(opVariable);
127
+ // There is a relational mapper for this variable
128
+ if (relation && operation.relationalCondition) {
129
+ output[relation.relation] = {
130
+ [operation.relationalCondition]: {
131
+ [relation.field]: opValue
132
+ }
133
+ };
134
+ return output;
135
+ }
136
+ else if (fieldName && operation.fieldCondition) {
137
+ // There is a field mapper for this variable
138
+ output[fieldName] = {
139
+ [operation.fieldCondition]: opValue,
140
+ };
141
+ return output;
142
+ }
143
+ else {
144
+ throw Error("Failed to map");
80
145
  }
81
- return output;
82
146
  }
package/lib/index.d.ts CHANGED
@@ -1,9 +1,24 @@
1
- import { PlanResourcesResponse } from "@cerbos/core";
1
+ import { PlanResourcesResponse, PlanKind as PK } from "@cerbos/core";
2
+ export declare type PlanKind = PK;
3
+ export declare const PlanKind: typeof PK;
4
+ declare type FieldMapper = {
5
+ [key: string]: string;
6
+ } | ((key: string) => string);
7
+ declare type Relation = {
8
+ relation: string;
9
+ field: string;
10
+ };
11
+ declare type RelationMapper = {
12
+ [key: string]: Relation;
13
+ } | ((key: string) => Relation);
2
14
  interface QueryPlanToPrismaArgs {
3
15
  queryPlan: PlanResourcesResponse;
4
- fieldNameMapper: {
5
- [key: string]: string;
6
- } | ((key: string) => string);
16
+ fieldNameMapper: FieldMapper;
17
+ relationMapper?: RelationMapper;
7
18
  }
8
- export default function queryPlanToPrisma({ queryPlan, fieldNameMapper, }: QueryPlanToPrismaArgs): any;
19
+ interface QueryPlanToPrismaResult {
20
+ kind: PlanKind;
21
+ filters?: any;
22
+ }
23
+ export declare function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper }: QueryPlanToPrismaArgs): QueryPlanToPrismaResult;
9
24
  export {};
package/lib/index.js CHANGED
@@ -1,17 +1,44 @@
1
- import { PlanKind } from "@cerbos/core";
2
- export default function queryPlanToPrisma({ queryPlan, fieldNameMapper, }) {
3
- if (queryPlan.kind === PlanKind.ALWAYS_ALLOWED)
4
- return {};
5
- if (queryPlan.kind === PlanKind.ALWAYS_DENIED)
6
- return { "1": { "equals": 0 } };
7
- return mapOperand(queryPlan.condition, (key) => {
1
+ import { PlanKind as PK } from "@cerbos/core";
2
+ export const PlanKind = PK;
3
+ export function queryPlanToPrisma({ queryPlan, fieldNameMapper, relationMapper = {} }) {
4
+ const toFieldName = (key) => {
8
5
  if (typeof fieldNameMapper === "function") {
9
6
  return fieldNameMapper(key);
10
7
  }
8
+ else if (fieldNameMapper[key]) {
9
+ return fieldNameMapper[key];
10
+ }
11
11
  else {
12
- return (fieldNameMapper[key] = fieldNameMapper[key] || key);
12
+ return key;
13
+ }
14
+ };
15
+ const toRelationName = (key) => {
16
+ if (typeof relationMapper === "function") {
17
+ return relationMapper(key);
18
+ }
19
+ else if (relationMapper[key]) {
20
+ return relationMapper[key];
13
21
  }
14
- }, {});
22
+ return null;
23
+ };
24
+ switch (queryPlan.kind) {
25
+ case PlanKind.ALWAYS_ALLOWED:
26
+ return {
27
+ kind: PlanKind.ALWAYS_ALLOWED,
28
+ filters: {}
29
+ };
30
+ case PlanKind.ALWAYS_DENIED:
31
+ return {
32
+ kind: PlanKind.ALWAYS_DENIED
33
+ };
34
+ case PlanKind.CONDITIONAL:
35
+ return {
36
+ kind: PlanKind.CONDITIONAL,
37
+ filters: mapOperand(queryPlan.condition, toFieldName, toRelationName)
38
+ };
39
+ default:
40
+ throw Error(`Invalid query plan.`);
41
+ }
15
42
  }
16
43
  function isExpression(e) {
17
44
  return e.operator !== undefined;
@@ -22,58 +49,94 @@ function isValue(e) {
22
49
  function isVariable(e) {
23
50
  return e.variable !== undefined;
24
51
  }
25
- function getOperandName(operands) {
26
- return (operands.find(o => o.hasOwnProperty("name"))).name;
52
+ function getOperandVariable(operands) {
53
+ const op = operands.find(o => o.hasOwnProperty("name"));
54
+ if (!op)
55
+ return;
56
+ return op.name;
27
57
  }
28
58
  function getOperandValue(operands) {
29
- return (operands.find(o => o.hasOwnProperty("value"))).value;
59
+ const op = operands.find(o => isValue(o));
60
+ if (!op)
61
+ return;
62
+ return op.value;
30
63
  }
31
- function mapOperand(operand, getFieldName, output = {}) {
32
- if (isExpression(operand)) {
33
- const { operator, operands } = operand;
34
- switch (operator) {
35
- case "and":
36
- output.AND = operands.map((o) => mapOperand(o, getFieldName, {}));
37
- break;
38
- case "or":
39
- output.OR = operands.map((o) => mapOperand(o, getFieldName, {}));
40
- break;
41
- case "eq":
42
- output[getFieldName(getOperandName(operands))] = {
43
- equals: getOperandValue(operands),
44
- };
45
- break;
46
- case "ne":
47
- output[getFieldName(getOperandName(operands))] = {
48
- not: getOperandValue(operands),
49
- };
50
- break;
51
- case "lt":
52
- output[getFieldName(getOperandName(operands))] = {
53
- lt: getOperandValue(operands),
54
- };
55
- break;
56
- case "gt":
57
- output[getFieldName(getOperandName(operands))] = {
58
- gt: getOperandValue(operands),
59
- };
60
- break;
61
- case "le":
62
- output[getFieldName(getOperandName(operands))] = {
63
- lte: getOperandValue(operands),
64
- };
65
- break;
66
- case "ge":
67
- output[getFieldName(getOperandName(operands))] = {
68
- gte: getOperandValue(operands),
69
- };
70
- break;
71
- case "in":
72
- output[getFieldName(getOperandName(operands))] = {
73
- in: getOperandValue(operands),
74
- };
75
- break;
76
- }
64
+ const OPERATORS = {
65
+ "eq": {
66
+ relationalCondition: "is",
67
+ fieldCondition: "equals"
68
+ },
69
+ "ne": {
70
+ relationalCondition: "isNot",
71
+ fieldCondition: "not"
72
+ },
73
+ "in": {
74
+ relationalCondition: "some",
75
+ fieldCondition: "in"
76
+ },
77
+ "lt": {
78
+ fieldCondition: "lt"
79
+ },
80
+ "gt": {
81
+ fieldCondition: "gt"
82
+ },
83
+ "le": {
84
+ fieldCondition: "lte"
85
+ },
86
+ "ge": {
87
+ fieldCondition: "gte"
88
+ }
89
+ };
90
+ function mapOperand(operand, getFieldName, getRelationName, output = {}) {
91
+ if (!isExpression(operand))
92
+ throw Error(`Query plan did not contain an expression for operand ${operand}`);
93
+ const { operator, operands } = operand;
94
+ // HANDLE NESTING OPERATIONS: AND/OR/NOT
95
+ if (operator == "and") {
96
+ if (operands.length < 2)
97
+ throw Error("Expected atleast 2 operands");
98
+ output.AND = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
99
+ return output;
100
+ }
101
+ if (operator == "or") {
102
+ if (operands.length < 2)
103
+ throw Error("Expected atleast 2 operands");
104
+ output.OR = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}));
105
+ return output;
106
+ }
107
+ if (operator == "not") {
108
+ if (operands.length > 1)
109
+ throw Error("Expected only one operand");
110
+ output.NOT = operands.map((o) => mapOperand(o, getFieldName, getRelationName, {}))[0];
111
+ return output;
112
+ }
113
+ // get the operation parameters
114
+ const operation = OPERATORS[operator];
115
+ if (!operation)
116
+ throw Error(`Unsupported operator ${operator}`);
117
+ const opVariable = getOperandVariable(operands);
118
+ if (!opVariable)
119
+ throw Error(`Unexpected variable ${operands}`);
120
+ const opValue = getOperandValue(operands);
121
+ const relation = getRelationName(opVariable);
122
+ const fieldName = getFieldName(opVariable);
123
+ // There is a relational mapper for this variable
124
+ if (relation && operation.relationalCondition) {
125
+ output[relation.relation] = {
126
+ [operation.relationalCondition]: {
127
+ [relation.field]: opValue
128
+ }
129
+ };
130
+ return output;
131
+ }
132
+ else if (fieldName && operation.fieldCondition) {
133
+ // There is a field mapper for this variable
134
+ output[fieldName] = {
135
+ [operation.fieldCondition]: opValue,
136
+ };
137
+ return output;
138
+ }
139
+ else {
140
+ throw Error("Failed to map");
77
141
  }
78
- return output;
79
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerbos/orm-prisma",
3
- "version": "0.1.4",
3
+ "version": "1.0.0",
4
4
  "homepage": "https://cerbos.dev",
5
5
  "description": "",
6
6
  "private": false,