@cerbos/orm-prisma 2.0.0-alpha.1 → 2.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 +145 -4
- package/lib/cjs/index.d.ts +6 -4
- package/lib/cjs/index.js +361 -189
- package/lib/index.d.ts +6 -4
- package/lib/index.js +361 -189
- package/package.json +5 -5
package/lib/index.js
CHANGED
|
@@ -6,12 +6,9 @@ export { PlanKind };
|
|
|
6
6
|
* @returns {QueryPlanToPrismaResult} The result containing the kind and filters.
|
|
7
7
|
*/
|
|
8
8
|
export function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
|
|
9
|
-
// Choose filter conversion based on plan kind
|
|
10
9
|
switch (queryPlan.kind) {
|
|
11
10
|
case PlanKind.ALWAYS_ALLOWED:
|
|
12
|
-
return {
|
|
13
|
-
kind: PlanKind.ALWAYS_ALLOWED,
|
|
14
|
-
};
|
|
11
|
+
return { kind: PlanKind.ALWAYS_ALLOWED };
|
|
15
12
|
case PlanKind.ALWAYS_DENIED:
|
|
16
13
|
return { kind: PlanKind.ALWAYS_DENIED };
|
|
17
14
|
case PlanKind.CONDITIONAL:
|
|
@@ -24,248 +21,365 @@ export function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
|
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
/**
|
|
27
|
-
* Resolves a field reference considering relations.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
24
|
+
* Resolves a field reference considering relations and nested fields.
|
|
25
|
+
* Handles both direct field mappings and relation mappings with support for nested structures.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} reference - The field reference from Cerbos
|
|
28
|
+
* @param {Mapper} mapper - Configuration for field/relation mapping
|
|
29
|
+
* @returns {Object} Resolved reference with path and optional relations
|
|
31
30
|
*/
|
|
32
31
|
const resolveFieldReference = (reference, mapper) => {
|
|
33
32
|
const parts = reference.split(".");
|
|
34
|
-
const lastPart = parts[parts.length - 1];
|
|
35
|
-
// First try exact match
|
|
36
33
|
const config = typeof mapper === "function" ? mapper(reference) : mapper[reference];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
let matchedPrefix = "";
|
|
35
|
+
let matchedConfig;
|
|
36
|
+
if (!config) {
|
|
37
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
38
|
+
const prefix = parts.slice(0, i + 1).join(".");
|
|
39
|
+
const prefixConfig = typeof mapper === "function" ? mapper(prefix) : mapper[prefix];
|
|
40
|
+
if (prefixConfig) {
|
|
41
|
+
matchedPrefix = prefix;
|
|
42
|
+
matchedConfig = prefixConfig;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
|
-
return { path: [config.field || reference] };
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
const activeConfig = config || matchedConfig;
|
|
48
|
+
if (activeConfig === null || activeConfig === void 0 ? void 0 : activeConfig.relation) {
|
|
49
|
+
const { name, type, fields } = activeConfig.relation;
|
|
50
|
+
const matchedParts = matchedPrefix ? matchedPrefix.split(".") : [];
|
|
51
|
+
const remainingParts = matchedPrefix
|
|
52
|
+
? parts.slice(matchedParts.length)
|
|
53
|
+
: parts.slice(1);
|
|
54
|
+
let field;
|
|
55
|
+
const relations = [
|
|
56
|
+
{ name, type, field: activeConfig.relation.field, nestedMapper: fields },
|
|
57
|
+
];
|
|
58
|
+
if (fields && remainingParts.length > 0) {
|
|
59
|
+
let currentMapper = fields;
|
|
60
|
+
let currentParts = remainingParts;
|
|
61
|
+
while (currentParts.length > 0) {
|
|
62
|
+
const nextConfig = currentMapper[currentParts[0]];
|
|
63
|
+
if (nextConfig === null || nextConfig === void 0 ? void 0 : nextConfig.relation) {
|
|
64
|
+
relations.push({
|
|
65
|
+
name: nextConfig.relation.name,
|
|
66
|
+
type: nextConfig.relation.type,
|
|
67
|
+
field: nextConfig.relation.field,
|
|
68
|
+
nestedMapper: nextConfig.relation.fields,
|
|
69
|
+
});
|
|
70
|
+
currentMapper = nextConfig.relation.fields || {};
|
|
71
|
+
currentParts = currentParts.slice(1);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
field = (nextConfig === null || nextConfig === void 0 ? void 0 : nextConfig.field) || currentParts[currentParts.length - 1];
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
}
|
|
79
|
+
return { path: field ? [field] : remainingParts, relations };
|
|
60
80
|
}
|
|
61
|
-
|
|
62
|
-
return { path: [reference] };
|
|
81
|
+
return { path: [(activeConfig === null || activeConfig === void 0 ? void 0 : activeConfig.field) || reference] };
|
|
63
82
|
};
|
|
64
83
|
/**
|
|
65
84
|
* Determines the appropriate Prisma operator based on relation type.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
85
|
+
* Used for building relation filters in Prisma queries.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object} relation - Relation configuration
|
|
88
|
+
* @param {string} relation.name - Name of the relation
|
|
89
|
+
* @param {('one'|'many')} relation.type - Type of relation
|
|
90
|
+
* @returns {string} Prisma relation operator ('is' for one-to-one, 'some' for one-to-many)
|
|
68
91
|
*/
|
|
69
92
|
const getPrismaRelationOperator = (relation) => (relation.type === "one" ? "is" : "some");
|
|
70
93
|
/**
|
|
71
|
-
* Builds
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* @
|
|
94
|
+
* Builds a nested relation filter for Prisma queries.
|
|
95
|
+
* Handles multiple levels of relations and applies appropriate operators.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array} relations - Array of relation configurations
|
|
98
|
+
* @param {Object} fieldFilter - Base filter to apply at the deepest level
|
|
99
|
+
* @returns {Object} Nested Prisma filter structure
|
|
100
|
+
*/
|
|
101
|
+
const buildNestedRelationFilter = (relations, fieldFilter) => {
|
|
102
|
+
if (relations.length === 0)
|
|
103
|
+
return fieldFilter;
|
|
104
|
+
let currentFilter = fieldFilter;
|
|
105
|
+
for (let i = relations.length - 1; i >= 0; i--) {
|
|
106
|
+
const relation = relations[i];
|
|
107
|
+
const relationOperator = getPrismaRelationOperator(relation);
|
|
108
|
+
if (relation.field && i === relations.length - 1) {
|
|
109
|
+
const [, filterValue] = Object.entries(currentFilter)[0];
|
|
110
|
+
currentFilter = { [relation.field]: filterValue };
|
|
111
|
+
}
|
|
112
|
+
currentFilter = { [relation.name]: { [relationOperator]: currentFilter } };
|
|
113
|
+
}
|
|
114
|
+
return currentFilter;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Type guard to check if an operand is a resolved field reference.
|
|
118
|
+
* @param {ResolvedOperand} operand - Operand to check
|
|
119
|
+
* @returns {boolean} True if operand is a field reference
|
|
120
|
+
*/
|
|
121
|
+
function isResolvedFieldReference(operand) {
|
|
122
|
+
return "path" in operand;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Type guard to check if an operand is a resolved value.
|
|
126
|
+
* @param {ResolvedOperand} operand - Operand to check
|
|
127
|
+
* @returns {boolean} True if operand is a value
|
|
128
|
+
*/
|
|
129
|
+
function isResolvedValue(operand) {
|
|
130
|
+
return "value" in operand;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Type guard for PlanExpressionOperand with name property.
|
|
134
|
+
* @param {PlanExpressionOperand} operand - Operand to check
|
|
135
|
+
* @returns {boolean} True if operand has name property
|
|
136
|
+
*/
|
|
137
|
+
function isPlanExpressionWithName(operand) {
|
|
138
|
+
return "name" in operand && typeof operand.name === "string";
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Type guard for PlanExpressionOperand with value property.
|
|
142
|
+
* @param {PlanExpressionOperand} operand - Operand to check
|
|
143
|
+
* @returns {boolean} True if operand has value property
|
|
144
|
+
*/
|
|
145
|
+
function isPlanExpressionWithValue(operand) {
|
|
146
|
+
return "value" in operand && operand.value !== undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Type guard for PlanExpressionOperand with operator property.
|
|
150
|
+
* @param {PlanExpressionOperand} operand - Operand to check
|
|
151
|
+
* @returns {boolean} True if operand has operator and operands properties
|
|
152
|
+
*/
|
|
153
|
+
function isPlanExpressionWithOperator(operand) {
|
|
154
|
+
return ("operator" in operand &&
|
|
155
|
+
typeof operand.operator === "string" &&
|
|
156
|
+
"operands" in operand &&
|
|
157
|
+
Array.isArray(operand.operands));
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolves a PlanExpressionOperand into a ResolvedOperand.
|
|
161
|
+
* Handles field references, values, and nested expressions.
|
|
162
|
+
*
|
|
163
|
+
* @param {PlanExpressionOperand} operand - Operand to resolve
|
|
164
|
+
* @param {Mapper} mapper - Field mapping configuration
|
|
165
|
+
* @returns {ResolvedOperand} Resolved operand
|
|
166
|
+
* @throws {Error} If operand is invalid
|
|
167
|
+
*/
|
|
168
|
+
const resolveOperand = (operand, mapper) => {
|
|
169
|
+
if (isPlanExpressionWithName(operand)) {
|
|
170
|
+
return resolveFieldReference(operand.name, mapper);
|
|
171
|
+
}
|
|
172
|
+
else if (isPlanExpressionWithValue(operand)) {
|
|
173
|
+
return { value: operand.value };
|
|
174
|
+
}
|
|
175
|
+
else if (isPlanExpressionWithOperator(operand)) {
|
|
176
|
+
const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
|
|
177
|
+
return { value: nestedResult };
|
|
178
|
+
}
|
|
179
|
+
throw new Error("Operand must have name, value, or be an expression");
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Builds a Prisma filter from a Cerbos expression.
|
|
183
|
+
* Main function for converting Cerbos query conditions to Prisma filters.
|
|
184
|
+
*
|
|
185
|
+
* @param {PlanExpressionOperand} expression - Cerbos expression to convert
|
|
186
|
+
* @param {Mapper} mapper - Field mapping configuration
|
|
187
|
+
* @returns {PrismaFilter} Prisma compatible filter
|
|
188
|
+
* @throws {Error} If expression is invalid or operator is unsupported
|
|
75
189
|
*/
|
|
76
190
|
const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
|
|
191
|
+
// Validate expression structure
|
|
77
192
|
if (!("operator" in expression) || !("operands" in expression)) {
|
|
78
193
|
throw new Error("Invalid Cerbos expression structure");
|
|
79
194
|
}
|
|
80
195
|
const { operator, operands } = expression;
|
|
81
196
|
/**
|
|
82
|
-
* Helper function to
|
|
83
|
-
* @param {
|
|
84
|
-
* @
|
|
197
|
+
* Helper function to process relational operators (eq, ne, lt, etc.)
|
|
198
|
+
* @param {string} operator - Relational operator
|
|
199
|
+
* @param {ResolvedOperand} left - Left operand
|
|
200
|
+
* @param {ResolvedOperand} right - Right operand
|
|
201
|
+
* @returns {PrismaFilter} Prisma filter for the relation
|
|
85
202
|
*/
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
203
|
+
const processRelationalOperator = (operator, left, right) => {
|
|
204
|
+
const prismaOperator = {
|
|
205
|
+
eq: "equals",
|
|
206
|
+
ne: "not",
|
|
207
|
+
lt: "lt",
|
|
208
|
+
le: "lte",
|
|
209
|
+
gt: "gt",
|
|
210
|
+
ge: "gte",
|
|
211
|
+
}[operator];
|
|
212
|
+
if (isResolvedFieldReference(left)) {
|
|
213
|
+
const { path, relations } = left;
|
|
214
|
+
if (!isResolvedValue(right)) {
|
|
215
|
+
throw new Error("Right operand must be a value");
|
|
216
|
+
}
|
|
217
|
+
if (!prismaOperator) {
|
|
218
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
219
|
+
}
|
|
220
|
+
const filterValue = { [prismaOperator]: right.value };
|
|
221
|
+
const fieldFilter = { [path[path.length - 1]]: filterValue };
|
|
222
|
+
if (relations && relations.length > 0) {
|
|
223
|
+
return buildNestedRelationFilter(relations, fieldFilter);
|
|
224
|
+
}
|
|
225
|
+
return fieldFilter;
|
|
89
226
|
}
|
|
90
|
-
|
|
91
|
-
|
|
227
|
+
if (!isResolvedValue(right)) {
|
|
228
|
+
throw new Error("Right operand must be a value");
|
|
92
229
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
|
|
96
|
-
return { value: nestedResult };
|
|
230
|
+
if (!prismaOperator) {
|
|
231
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
97
232
|
}
|
|
98
|
-
|
|
233
|
+
return { [prismaOperator]: right.value };
|
|
99
234
|
};
|
|
100
|
-
// Process
|
|
235
|
+
// Process different operator types
|
|
101
236
|
switch (operator) {
|
|
102
|
-
case "and":
|
|
103
|
-
// Combine operands with logical AND
|
|
237
|
+
case "and":
|
|
104
238
|
return {
|
|
105
239
|
AND: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
|
|
106
240
|
};
|
|
107
|
-
|
|
108
|
-
case "or": {
|
|
109
|
-
// Combine operands with logical OR
|
|
241
|
+
case "or":
|
|
110
242
|
return {
|
|
111
243
|
OR: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
|
|
112
244
|
};
|
|
113
|
-
|
|
114
|
-
case "not": {
|
|
115
|
-
// Negate the operand filter
|
|
245
|
+
case "not":
|
|
116
246
|
return {
|
|
117
247
|
NOT: buildPrismaFilterFromCerbosExpression(operands[0], mapper),
|
|
118
248
|
};
|
|
119
|
-
}
|
|
120
249
|
case "eq":
|
|
121
250
|
case "ne":
|
|
122
251
|
case "lt":
|
|
123
252
|
case "le":
|
|
124
253
|
case "gt":
|
|
125
254
|
case "ge": {
|
|
126
|
-
|
|
127
|
-
const leftOperand = operands.find((o) => "name" in o || "operator" in o);
|
|
255
|
+
const leftOperand = operands.find((o) => isPlanExpressionWithName(o) || isPlanExpressionWithOperator(o));
|
|
128
256
|
if (!leftOperand)
|
|
129
257
|
throw new Error("No valid left operand found");
|
|
130
258
|
const rightOperand = operands.find((o) => o !== leftOperand);
|
|
131
259
|
if (!rightOperand)
|
|
132
260
|
throw new Error("No valid right operand found");
|
|
133
|
-
const left = resolveOperand(leftOperand);
|
|
134
|
-
const right = resolveOperand(rightOperand);
|
|
135
|
-
|
|
136
|
-
eq: "equals",
|
|
137
|
-
ne: "not",
|
|
138
|
-
lt: "lt",
|
|
139
|
-
le: "lte",
|
|
140
|
-
gt: "gt",
|
|
141
|
-
ge: "gte",
|
|
142
|
-
}[operator];
|
|
143
|
-
if ("path" in left) {
|
|
144
|
-
const { path, relation } = left;
|
|
145
|
-
if (relation) {
|
|
146
|
-
const relationOperator = getPrismaRelationOperator(relation);
|
|
147
|
-
if (relation.field) {
|
|
148
|
-
return {
|
|
149
|
-
[relation.name]: {
|
|
150
|
-
[relationOperator]: {
|
|
151
|
-
[relation.field]: { [prismaOperator]: right.value },
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
[relation.name]: {
|
|
158
|
-
[relationOperator]: { [prismaOperator]: right.value },
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
// Create nested filter for field path
|
|
163
|
-
return path.reduceRight((acc, key, index) => index === path.length - 1
|
|
164
|
-
? { [key]: { [prismaOperator]: right.value } }
|
|
165
|
-
: { [key]: acc }, {});
|
|
166
|
-
}
|
|
167
|
-
return { [prismaOperator]: right.value };
|
|
261
|
+
const left = resolveOperand(leftOperand, mapper);
|
|
262
|
+
const right = resolveOperand(rightOperand, mapper);
|
|
263
|
+
return processRelationalOperator(operator, left, right);
|
|
168
264
|
}
|
|
169
265
|
case "in": {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
266
|
+
const nameOperand = operands.find(isPlanExpressionWithName);
|
|
267
|
+
if (!nameOperand)
|
|
268
|
+
throw new Error("Name operand is undefined");
|
|
269
|
+
const valueOperand = operands.find(isPlanExpressionWithValue);
|
|
270
|
+
if (!valueOperand)
|
|
271
|
+
throw new Error("Value operand is undefined");
|
|
272
|
+
const resolved = resolveOperand(nameOperand, mapper);
|
|
273
|
+
if (!isResolvedFieldReference(resolved))
|
|
274
|
+
throw new Error("Name operand must resolve to a field reference");
|
|
275
|
+
const { path, relations } = resolved;
|
|
276
|
+
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
277
|
+
if (!isResolvedValue(resolvedValue))
|
|
278
|
+
throw new Error("Value operand must resolve to a value");
|
|
279
|
+
const { value } = resolvedValue;
|
|
280
|
+
if (relations && relations.length > 0) {
|
|
281
|
+
const fieldFilter = { [path[path.length - 1]]: value };
|
|
282
|
+
return buildNestedRelationFilter(relations, fieldFilter);
|
|
178
283
|
}
|
|
179
|
-
return
|
|
284
|
+
return { [path[path.length - 1]]: { in: value } };
|
|
180
285
|
}
|
|
181
286
|
case "contains":
|
|
182
287
|
case "startsWith":
|
|
183
288
|
case "endsWith": {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
289
|
+
const nameOperand = operands.find(isPlanExpressionWithName);
|
|
290
|
+
if (!nameOperand)
|
|
291
|
+
throw new Error("Name operand is undefined");
|
|
292
|
+
const resolved = resolveOperand(nameOperand, mapper);
|
|
293
|
+
if (!isResolvedFieldReference(resolved))
|
|
294
|
+
throw new Error("Name operand must resolve to a field reference");
|
|
295
|
+
const { path, relations } = resolved;
|
|
296
|
+
const valueOperand = operands.find(isPlanExpressionWithValue);
|
|
297
|
+
if (!valueOperand)
|
|
298
|
+
throw new Error("Value operand is undefined");
|
|
299
|
+
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
300
|
+
if (!isResolvedValue(resolvedValue))
|
|
301
|
+
throw new Error("Value operand must resolve to a value");
|
|
302
|
+
const { value } = resolvedValue;
|
|
193
303
|
if (typeof value !== "string")
|
|
194
304
|
throw new Error(`${operator} operator requires string value`);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return
|
|
198
|
-
[relation.name]: {
|
|
199
|
-
[relationOperator]: { [relation.field]: { [operator]: value } },
|
|
200
|
-
},
|
|
201
|
-
};
|
|
305
|
+
const fieldFilter = { [path[path.length - 1]]: { [operator]: value } };
|
|
306
|
+
if (relations && relations.length > 0) {
|
|
307
|
+
return buildNestedRelationFilter(relations, fieldFilter);
|
|
202
308
|
}
|
|
203
|
-
return
|
|
204
|
-
? { [key]: { [operator]: value } }
|
|
205
|
-
: { [key]: acc }, {});
|
|
309
|
+
return fieldFilter;
|
|
206
310
|
}
|
|
207
311
|
case "isSet": {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
312
|
+
const nameOperand = operands.find(isPlanExpressionWithName);
|
|
313
|
+
if (!nameOperand)
|
|
314
|
+
throw new Error("Name operand is undefined");
|
|
315
|
+
const resolved = resolveOperand(nameOperand, mapper);
|
|
316
|
+
if (!isResolvedFieldReference(resolved))
|
|
317
|
+
throw new Error("Name operand must resolve to a field reference");
|
|
318
|
+
const { path, relations } = resolved;
|
|
319
|
+
const valueOperand = operands.find(isPlanExpressionWithValue);
|
|
320
|
+
if (!valueOperand)
|
|
321
|
+
throw new Error("Value operand is undefined");
|
|
322
|
+
const resolvedValue = resolveOperand(valueOperand, mapper);
|
|
323
|
+
const fieldFilter = {
|
|
324
|
+
[path[path.length - 1]]: resolvedValue
|
|
325
|
+
? { not: null }
|
|
326
|
+
: { equals: null },
|
|
327
|
+
};
|
|
328
|
+
if (relations && relations.length > 0) {
|
|
329
|
+
return buildNestedRelationFilter(relations, fieldFilter);
|
|
226
330
|
}
|
|
227
|
-
return
|
|
228
|
-
? { [key]: value ? { not: null } : { equals: null } }
|
|
229
|
-
: { [key]: acc }, {});
|
|
331
|
+
return fieldFilter;
|
|
230
332
|
}
|
|
231
333
|
case "hasIntersection": {
|
|
232
|
-
// Ensure exactly two operands for intersection check
|
|
233
334
|
if (operands.length !== 2)
|
|
234
335
|
throw new Error("hasIntersection requires exactly two operands");
|
|
235
336
|
const [leftOperand, rightOperand] = operands;
|
|
236
|
-
// Handle map expressions as first operand
|
|
237
337
|
if ("operator" in leftOperand && leftOperand.operator === "map") {
|
|
238
|
-
const mapResult = buildPrismaFilterFromCerbosExpression(leftOperand, mapper);
|
|
239
|
-
const relationKey = Object.keys(mapResult)[0];
|
|
240
|
-
const relation = mapResult[relationKey];
|
|
241
|
-
const selectKey = Object.keys(relation[Object.keys(relation)[0]].select)[0];
|
|
242
338
|
if (!("value" in rightOperand))
|
|
243
339
|
throw new Error("Second operand of hasIntersection must be a value");
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
340
|
+
const [collection, lambda] = leftOperand.operands;
|
|
341
|
+
if (!("name" in collection))
|
|
342
|
+
throw new Error("First operand of map must be a collection reference");
|
|
343
|
+
// Get variable name from lambda
|
|
344
|
+
if (!isPlanExpressionWithOperator(lambda)) {
|
|
345
|
+
throw new Error("Lambda expression must have operands");
|
|
346
|
+
}
|
|
347
|
+
const [, variable] = lambda.operands;
|
|
348
|
+
if (!("name" in variable))
|
|
349
|
+
throw new Error("Lambda variable must have a name");
|
|
350
|
+
// Create scoped mapper for the collection
|
|
351
|
+
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
|
|
352
|
+
const { relations } = resolveFieldReference(collection.name, mapper);
|
|
353
|
+
if (!relations || relations.length === 0)
|
|
354
|
+
throw new Error("Map operation requires relations");
|
|
355
|
+
if (!("operands" in lambda))
|
|
356
|
+
throw new Error("Invalid lambda expression structure");
|
|
357
|
+
const [projection] = lambda.operands;
|
|
358
|
+
if (!("name" in projection))
|
|
359
|
+
throw new Error("Invalid map lambda expression structure");
|
|
360
|
+
// Use scoped mapper for resolving the projection
|
|
361
|
+
const resolved = resolveFieldReference(projection.name, scopedMapper);
|
|
362
|
+
const fieldName = resolved.path[resolved.path.length - 1];
|
|
363
|
+
return buildNestedRelationFilter(relations, {
|
|
364
|
+
[fieldName]: { in: rightOperand.value },
|
|
365
|
+
});
|
|
247
366
|
}
|
|
248
|
-
// Handle direct field reference
|
|
249
367
|
if (!("name" in leftOperand))
|
|
250
368
|
throw new Error("First operand of hasIntersection must be a field reference or map expression");
|
|
251
369
|
if (!("value" in rightOperand))
|
|
252
370
|
throw new Error("Second operand of hasIntersection must be a value");
|
|
253
|
-
const { path,
|
|
371
|
+
const { path, relations } = resolveFieldReference(leftOperand.name, mapper);
|
|
254
372
|
if (!Array.isArray(rightOperand.value))
|
|
255
373
|
throw new Error("hasIntersection requires an array value");
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
[
|
|
259
|
-
some: { [relation.field]: { in: rightOperand.value } },
|
|
260
|
-
},
|
|
374
|
+
if (relations && relations.length > 0) {
|
|
375
|
+
const fieldFilter = {
|
|
376
|
+
[path[path.length - 1]]: { in: rightOperand.value },
|
|
261
377
|
};
|
|
378
|
+
return buildNestedRelationFilter(relations, fieldFilter);
|
|
262
379
|
}
|
|
263
|
-
return path.
|
|
264
|
-
? { [key]: { hasSome: rightOperand.value } }
|
|
265
|
-
: { [key]: acc }, {});
|
|
380
|
+
return { [path[path.length - 1]]: { some: rightOperand.value } };
|
|
266
381
|
}
|
|
267
382
|
case "lambda": {
|
|
268
|
-
// Handle lambda expressions by replacing variable prefix on keys
|
|
269
383
|
const [condition, variable] = operands;
|
|
270
384
|
if (!("name" in variable))
|
|
271
385
|
throw new Error("Lambda variable must have a name");
|
|
@@ -276,7 +390,6 @@ const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
|
|
|
276
390
|
case "all":
|
|
277
391
|
case "except":
|
|
278
392
|
case "filter": {
|
|
279
|
-
// Operators applying conditions on collections via lambda expressions
|
|
280
393
|
if (operands.length !== 2)
|
|
281
394
|
throw new Error(`${operator} requires exactly two operands`);
|
|
282
395
|
const [collection, lambda] = operands;
|
|
@@ -284,34 +397,55 @@ const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
|
|
|
284
397
|
throw new Error("First operand of exists/all/except must be a collection reference");
|
|
285
398
|
if (!("operator" in lambda))
|
|
286
399
|
throw new Error("Second operand of exists/all/except must be a lambda expression");
|
|
287
|
-
|
|
288
|
-
|
|
400
|
+
// Get variable name from lambda
|
|
401
|
+
const [, variable] = lambda.operands;
|
|
402
|
+
if (!("name" in variable))
|
|
403
|
+
throw new Error("Lambda variable must have a name");
|
|
404
|
+
// Create scoped mapper for the collection
|
|
405
|
+
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
|
|
406
|
+
const { relations } = resolveFieldReference(collection.name, mapper);
|
|
407
|
+
if (!relations)
|
|
289
408
|
throw new Error(`${operator} operator requires a relation mapping`);
|
|
290
|
-
const lambdaCondition = buildPrismaFilterFromCerbosExpression(lambda,
|
|
409
|
+
const lambdaCondition = buildPrismaFilterFromCerbosExpression(lambda.operands[0], // Use the condition part of the lambda
|
|
410
|
+
scopedMapper);
|
|
411
|
+
const relation = relations[0];
|
|
412
|
+
let filterValue = lambdaCondition;
|
|
413
|
+
// If the lambda condition already has a relation structure, merge it
|
|
414
|
+
if (lambdaCondition.AND || lambdaCondition.OR) {
|
|
415
|
+
filterValue = lambdaCondition;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const filterField = relation.field || Object.keys(lambdaCondition)[0];
|
|
419
|
+
filterValue = {
|
|
420
|
+
[filterField]: lambdaCondition[Object.keys(lambdaCondition)[0]],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
291
423
|
switch (operator) {
|
|
292
424
|
case "exists":
|
|
293
|
-
|
|
425
|
+
case "filter":
|
|
426
|
+
return { [relation.name]: { some: filterValue } };
|
|
294
427
|
case "exists_one":
|
|
295
428
|
return {
|
|
296
|
-
[relation.name]: {
|
|
429
|
+
[relation.name]: {
|
|
430
|
+
some: filterValue,
|
|
431
|
+
},
|
|
297
432
|
AND: [
|
|
298
433
|
{
|
|
299
434
|
[relation.name]: {
|
|
300
|
-
every: {
|
|
435
|
+
every: {
|
|
436
|
+
OR: [filterValue, { NOT: filterValue }],
|
|
437
|
+
},
|
|
301
438
|
},
|
|
302
439
|
},
|
|
303
440
|
],
|
|
304
441
|
};
|
|
305
442
|
case "all":
|
|
306
|
-
return { [relation.name]: { every:
|
|
307
|
-
case "filter":
|
|
308
|
-
return { [relation.name]: { some: lambdaCondition } };
|
|
443
|
+
return { [relation.name]: { every: filterValue } };
|
|
309
444
|
default:
|
|
310
445
|
throw new Error(`Unexpected operator: ${operator}`);
|
|
311
446
|
}
|
|
312
447
|
}
|
|
313
448
|
case "map": {
|
|
314
|
-
// Process map operator to project field values
|
|
315
449
|
if (operands.length !== 2)
|
|
316
450
|
throw new Error("map requires exactly two operands");
|
|
317
451
|
const [collection, lambda] = operands;
|
|
@@ -319,23 +453,61 @@ const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
|
|
|
319
453
|
throw new Error("First operand of map must be a collection reference");
|
|
320
454
|
if (!("operator" in lambda) || lambda.operator !== "lambda")
|
|
321
455
|
throw new Error("Second operand of map must be a lambda expression");
|
|
322
|
-
|
|
323
|
-
if (!relation)
|
|
324
|
-
throw new Error("map operator requires a relation mapping");
|
|
456
|
+
// Get variable name from lambda
|
|
325
457
|
const [projection, variable] = lambda.operands;
|
|
326
458
|
if (!("name" in projection) || !("name" in variable))
|
|
327
459
|
throw new Error("Invalid map lambda expression structure");
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
460
|
+
// Create scoped mapper for the collection
|
|
461
|
+
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
|
|
462
|
+
const { relations } = resolveFieldReference(collection.name, mapper);
|
|
463
|
+
if (!relations)
|
|
464
|
+
throw new Error("map operator requires a relation mapping");
|
|
465
|
+
// Use scoped mapper for resolving the projection
|
|
466
|
+
const resolved = resolveFieldReference(projection.name, scopedMapper);
|
|
467
|
+
const fieldName = resolved.path[resolved.path.length - 1];
|
|
468
|
+
return buildNestedRelationFilter(relations, {
|
|
469
|
+
[getPrismaRelationOperator(relations[relations.length - 1])]: {
|
|
470
|
+
select: { [fieldName]: true },
|
|
335
471
|
},
|
|
336
|
-
};
|
|
472
|
+
});
|
|
337
473
|
}
|
|
338
474
|
default:
|
|
339
475
|
throw new Error(`Unsupported operator: ${operator}`);
|
|
340
476
|
}
|
|
341
477
|
};
|
|
478
|
+
/**
|
|
479
|
+
* Creates a scoped mapper for collection operations that preserves access to parent scope
|
|
480
|
+
* and handles nested collections
|
|
481
|
+
*/
|
|
482
|
+
const createScopedMapper = (collectionPath, variableName, fullMapper) => (key) => {
|
|
483
|
+
var _a, _b, _c;
|
|
484
|
+
// If the key starts with the variable name, it's accessing the collection item
|
|
485
|
+
if (key.startsWith(variableName + ".")) {
|
|
486
|
+
const strippedKey = key.replace(variableName + ".", "");
|
|
487
|
+
const parts = strippedKey.split(".");
|
|
488
|
+
// Get the collection's relation config
|
|
489
|
+
const collectionConfig = typeof fullMapper === "function"
|
|
490
|
+
? fullMapper(collectionPath)
|
|
491
|
+
: fullMapper[collectionPath];
|
|
492
|
+
if ((_a = collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.relation) === null || _a === void 0 ? void 0 : _a.fields) {
|
|
493
|
+
// For nested paths, traverse the fields configuration
|
|
494
|
+
let currentConfig = collectionConfig.relation.fields;
|
|
495
|
+
let field = parts[0];
|
|
496
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
497
|
+
const part = parts[i];
|
|
498
|
+
if ((_c = (_b = currentConfig[part]) === null || _b === void 0 ? void 0 : _b.relation) === null || _c === void 0 ? void 0 : _c.fields) {
|
|
499
|
+
currentConfig = currentConfig[part].relation.fields;
|
|
500
|
+
field = parts[i + 1];
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Return the field config if it exists, otherwise create a default one
|
|
507
|
+
return currentConfig[field] || { field };
|
|
508
|
+
}
|
|
509
|
+
return { field: strippedKey };
|
|
510
|
+
}
|
|
511
|
+
// For keys not referencing the collection item, use the full mapper
|
|
512
|
+
return typeof fullMapper === "function" ? fullMapper(key) : fullMapper[key];
|
|
513
|
+
};
|