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