@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/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
- * @param {string} reference - The field reference.
29
- * @param {Mapper} mapper - The mapper configuration.
30
- * @returns {object} The resolved field reference with path and relation.
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
- if (config) {
38
- if (config.relation) {
39
- const { name, type, field, fields } = config.relation;
40
- return {
41
- path: [name, field].filter(Boolean),
42
- relation: { name, type, field, nestedMapper: fields },
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
- // Check for parent relation with multiple parts
48
- if (parts.length > 1) {
49
- const parentPath = parts.slice(0, -1).join(".");
50
- const parentConfig = typeof mapper === "function" ? mapper(parentPath) : mapper[parentPath];
51
- if (parentConfig === null || parentConfig === void 0 ? void 0 : parentConfig.relation) {
52
- const { name, type, fields } = parentConfig.relation;
53
- const fieldConfig = fields === null || fields === void 0 ? void 0 : fields[lastPart];
54
- const fieldName = (fieldConfig === null || fieldConfig === void 0 ? void 0 : fieldConfig.field) || lastPart;
55
- return {
56
- path: [name],
57
- relation: { name, type, field: fieldName, nestedMapper: fields },
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
- // Fallback to raw reference if no mapper fits
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
- * @param {object} relation - The relation configuration.
67
- * @returns {string} The Prisma relation operator.
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 Prisma WHERE conditions from a Cerbos expression.
72
- * @param {PlanExpressionOperand} expression - The Cerbos expression.
73
- * @param {Mapper} mapper - The mapper configuration.
74
- * @returns {PrismaFilter} The Prisma filter.
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 resolve an operand.
83
- * @param {PlanExpressionOperand} operand - The operand from expression.
84
- * @returns {any} An object with a field reference or value.
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 resolveOperand = (operand) => {
87
- if ("name" in operand && operand.name) {
88
- return resolveFieldReference(operand.name, mapper);
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
- else if ("value" in operand && operand.value !== undefined) {
91
- return { value: operand.value };
227
+ if (!isResolvedValue(right)) {
228
+ throw new Error("Right operand must be a value");
92
229
  }
93
- else if ("operator" in operand) {
94
- // Nested expression resolution
95
- const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
96
- return { value: nestedResult };
230
+ if (!prismaOperator) {
231
+ throw new Error(`Unsupported operator: ${operator}`);
97
232
  }
98
- throw new Error("Operand must have name, value, or be an expression");
233
+ return { [prismaOperator]: right.value };
99
234
  };
100
- // Process the operator type
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
- // Relational operators: find left and right operands
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
- const prismaOperator = {
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
- // Inclusion operator for lists
171
- const { path, relation } = resolveOperand(operands.find((o) => "name" in o));
172
- const { value } = resolveOperand(operands.find((o) => "value" in o));
173
- if (relation) {
174
- const relationOperator = getPrismaRelationOperator(relation);
175
- return {
176
- [relation.name]: { [relationOperator]: { [relation.field]: value } },
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 path.reduceRight((acc, key, index) => index === path.length - 1 ? { [key]: { in: value } } : { [key]: acc }, {});
284
+ return { [path[path.length - 1]]: { in: value } };
180
285
  }
181
286
  case "contains":
182
287
  case "startsWith":
183
288
  case "endsWith": {
184
- // String operators; ensure value is a string
185
- const leftOperand = operands.find((o) => "name" in o);
186
- if (!leftOperand)
187
- throw new Error("No operand with 'name' found");
188
- const rightOperand = operands.find((o) => "value" in o);
189
- if (!rightOperand)
190
- throw new Error("No operand with 'value' found");
191
- const { path, relation } = resolveOperand(leftOperand);
192
- const { value } = resolveOperand(rightOperand);
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
- if (relation) {
196
- const relationOperator = getPrismaRelationOperator(relation);
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 path.reduceRight((acc, key, index) => index === path.length - 1
204
- ? { [key]: { [operator]: value } }
205
- : { [key]: acc }, {});
309
+ return fieldFilter;
206
310
  }
207
311
  case "isSet": {
208
- // Check if a field is set (not null) or unset (null)
209
- const leftOperand = operands.find((o) => "name" in o);
210
- if (!leftOperand)
211
- throw new Error("No operand with 'name' found");
212
- const rightOperand = operands.find((o) => "value" in o);
213
- if (!rightOperand)
214
- throw new Error("No operand with 'value' found");
215
- const { path, relation } = resolveOperand(leftOperand);
216
- const { value } = resolveOperand(rightOperand);
217
- if (relation) {
218
- const relationOperator = getPrismaRelationOperator(relation);
219
- return {
220
- [relation.name]: {
221
- [relationOperator]: {
222
- [relation.field]: value ? { not: null } : { equals: null },
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 path.reduceRight((acc, key, index) => index === path.length - 1
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
- return {
245
- [relationKey]: { some: { [selectKey]: { in: rightOperand.value } } },
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, relation } = resolveFieldReference(leftOperand.name, mapper);
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 (relation) {
257
- return {
258
- [relation.name]: {
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.reduceRight((acc, key, index) => index === path.length - 1
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
- const { relation } = resolveFieldReference(collection.name, mapper);
288
- if (!relation)
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, mapper);
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
- return { [relation.name]: { some: lambdaCondition } };
425
+ case "filter":
426
+ return { [relation.name]: { some: filterValue } };
294
427
  case "exists_one":
295
428
  return {
296
- [relation.name]: { some: lambdaCondition },
429
+ [relation.name]: {
430
+ some: filterValue,
431
+ },
297
432
  AND: [
298
433
  {
299
434
  [relation.name]: {
300
- every: { OR: [lambdaCondition, { NOT: lambdaCondition }] },
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: lambdaCondition } };
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
- const { relation } = resolveFieldReference(collection.name, mapper);
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
- return {
329
- [relation.name]: {
330
- [getPrismaRelationOperator(relation)]: {
331
- select: {
332
- [projection.name.replace(`${variable.name}.`, "")]: true,
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
+ };