@cerbos/orm-prisma 2.0.0-alpha.1 → 2.0.0-alpha.2

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.
Files changed (3) hide show
  1. package/lib/cjs/index.js +361 -189
  2. package/lib/index.js +361 -189
  3. package/package.json +3 -3
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
- * @param {string} reference - The field reference.
33
- * @param {Mapper} mapper - The mapper configuration.
34
- * @returns {object} The resolved field reference with path and relation.
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
- if (config) {
42
- if (config.relation) {
43
- const { name, type, field, fields } = config.relation;
44
- return {
45
- path: [name, field].filter(Boolean),
46
- relation: { name, type, field, nestedMapper: fields },
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
- // Check for parent relation with multiple parts
52
- if (parts.length > 1) {
53
- const parentPath = parts.slice(0, -1).join(".");
54
- const parentConfig = typeof mapper === "function" ? mapper(parentPath) : mapper[parentPath];
55
- if (parentConfig === null || parentConfig === void 0 ? void 0 : parentConfig.relation) {
56
- const { name, type, fields } = parentConfig.relation;
57
- const fieldConfig = fields === null || fields === void 0 ? void 0 : fields[lastPart];
58
- const fieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.field) || lastPart;
59
- return {
60
- path: [name],
61
- relation: { name, type, field: fieldName, nestedMapper: fields },
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
- // Fallback to raw reference if no mapper fits
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
- * @param {object} relation - The relation configuration.
71
- * @returns {string} The Prisma relation operator.
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 Prisma WHERE conditions from a Cerbos expression.
76
- * @param {PlanExpressionOperand} expression - The Cerbos expression.
77
- * @param {Mapper} mapper - The mapper configuration.
78
- * @returns {PrismaFilter} The Prisma filter.
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 resolve an operand.
87
- * @param {PlanExpressionOperand} operand - The operand from expression.
88
- * @returns {any} An object with a field reference or value.
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 resolveOperand = (operand) => {
91
- if ("name" in operand && operand.name) {
92
- return resolveFieldReference(operand.name, mapper);
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
- else if ("value" in operand && operand.value !== undefined) {
95
- return { value: operand.value };
231
+ if (!isResolvedValue(right)) {
232
+ throw new Error("Right operand must be a value");
96
233
  }
97
- else if ("operator" in operand) {
98
- // Nested expression resolution
99
- const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
100
- return { value: nestedResult };
234
+ if (!prismaOperator) {
235
+ throw new Error(`Unsupported operator: ${operator}`);
101
236
  }
102
- throw new Error("Operand must have name, value, or be an expression");
237
+ return { [prismaOperator]: right.value };
103
238
  };
104
- // Process the operator type
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
- // Relational operators: find left and right operands
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
- const prismaOperator = {
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
- // Inclusion operator for lists
175
- const { path, relation } = resolveOperand(operands.find((o) => "name" in o));
176
- const { value } = resolveOperand(operands.find((o) => "value" in o));
177
- if (relation) {
178
- const relationOperator = getPrismaRelationOperator(relation);
179
- return {
180
- [relation.name]: { [relationOperator]: { [relation.field]: value } },
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 path.reduceRight((acc, key, index) => index === path.length - 1 ? { [key]: { in: value } } : { [key]: acc }, {});
288
+ return { [path[path.length - 1]]: { in: value } };
184
289
  }
185
290
  case "contains":
186
291
  case "startsWith":
187
292
  case "endsWith": {
188
- // String operators; ensure value is a string
189
- const leftOperand = operands.find((o) => "name" in o);
190
- if (!leftOperand)
191
- throw new Error("No operand with 'name' found");
192
- const rightOperand = operands.find((o) => "value" in o);
193
- if (!rightOperand)
194
- throw new Error("No operand with 'value' found");
195
- const { path, relation } = resolveOperand(leftOperand);
196
- const { value } = resolveOperand(rightOperand);
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
- if (relation) {
200
- const relationOperator = getPrismaRelationOperator(relation);
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 path.reduceRight((acc, key, index) => index === path.length - 1
208
- ? { [key]: { [operator]: value } }
209
- : { [key]: acc }, {});
313
+ return fieldFilter;
210
314
  }
211
315
  case "isSet": {
212
- // Check if a field is set (not null) or unset (null)
213
- const leftOperand = operands.find((o) => "name" in o);
214
- if (!leftOperand)
215
- throw new Error("No operand with 'name' found");
216
- const rightOperand = operands.find((o) => "value" in o);
217
- if (!rightOperand)
218
- throw new Error("No operand with 'value' found");
219
- const { path, relation } = resolveOperand(leftOperand);
220
- const { value } = resolveOperand(rightOperand);
221
- if (relation) {
222
- const relationOperator = getPrismaRelationOperator(relation);
223
- return {
224
- [relation.name]: {
225
- [relationOperator]: {
226
- [relation.field]: value ? { not: null } : { equals: null },
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 path.reduceRight((acc, key, index) => index === path.length - 1
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
- return {
249
- [relationKey]: { some: { [selectKey]: { in: rightOperand.value } } },
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, relation } = resolveFieldReference(leftOperand.name, mapper);
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 (relation) {
261
- return {
262
- [relation.name]: {
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.reduceRight((acc, key, index) => index === path.length - 1
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
- const { relation } = resolveFieldReference(collection.name, mapper);
292
- if (!relation)
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, mapper);
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
- return { [relation.name]: { some: lambdaCondition } };
429
+ case "filter":
430
+ return { [relation.name]: { some: filterValue } };
298
431
  case "exists_one":
299
432
  return {
300
- [relation.name]: { some: lambdaCondition },
433
+ [relation.name]: {
434
+ some: filterValue,
435
+ },
301
436
  AND: [
302
437
  {
303
438
  [relation.name]: {
304
- every: { OR: [lambdaCondition, { NOT: lambdaCondition }] },
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: lambdaCondition } };
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
- const { relation } = resolveFieldReference(collection.name, mapper);
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
- return {
333
- [relation.name]: {
334
- [getPrismaRelationOperator(relation)]: {
335
- select: {
336
- [projection.name.replace(`${variable.name}.`, "")]: true,
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
+ };