@cerbos/orm-prisma 2.0.0 → 3.0.1

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/lib/index.js CHANGED
@@ -1,19 +1,59 @@
1
- import { PlanKind, } from "@cerbos/core";
2
- export { PlanKind };
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PlanKind = void 0;
4
+ exports.queryPlanToPrisma = queryPlanToPrisma;
5
+ const core_1 = require("@cerbos/core");
6
+ Object.defineProperty(exports, "PlanKind", { enumerable: true, get: function () { return core_1.PlanKind; } });
7
+ function isNamedOperand(operand) {
8
+ return "name" in operand && typeof operand.name === "string";
9
+ }
10
+ function isValueOperand(operand) {
11
+ return "value" in operand && operand.value !== undefined;
12
+ }
13
+ function isOperatorOperand(operand) {
14
+ return ("operator" in operand &&
15
+ typeof operand.operator === "string" &&
16
+ "operands" in operand &&
17
+ Array.isArray(operand.operands));
18
+ }
19
+ function assertDefined(value, message) {
20
+ if (value === undefined) {
21
+ throw new Error(message);
22
+ }
23
+ return value;
24
+ }
25
+ function getLeafField(path) {
26
+ const fieldName = path[path.length - 1];
27
+ if (!fieldName) {
28
+ throw new Error("Field path cannot be empty");
29
+ }
30
+ return fieldName;
31
+ }
32
+ function getFilterEntry(filter) {
33
+ const entry = Object.entries(filter)[0];
34
+ if (!entry) {
35
+ throw new Error("Filter must contain at least one entry");
36
+ }
37
+ return entry;
38
+ }
39
+ function isResolvedFieldReference(operand) {
40
+ return "path" in operand;
41
+ }
42
+ function isResolvedValue(operand) {
43
+ return "value" in operand;
44
+ }
3
45
  /**
4
46
  * Converts a Cerbos query plan to a Prisma filter.
5
- * @param {QueryPlanToPrismaArgs} args - The arguments containing the query plan and mapper.
6
- * @returns {QueryPlanToPrismaResult} The result containing the kind and filters.
7
47
  */
8
- export function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
48
+ function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
9
49
  switch (queryPlan.kind) {
10
- case PlanKind.ALWAYS_ALLOWED:
11
- return { kind: PlanKind.ALWAYS_ALLOWED };
12
- case PlanKind.ALWAYS_DENIED:
13
- return { kind: PlanKind.ALWAYS_DENIED };
14
- case PlanKind.CONDITIONAL:
50
+ case core_1.PlanKind.ALWAYS_ALLOWED:
51
+ return { kind: core_1.PlanKind.ALWAYS_ALLOWED };
52
+ case core_1.PlanKind.ALWAYS_DENIED:
53
+ return { kind: core_1.PlanKind.ALWAYS_DENIED };
54
+ case core_1.PlanKind.CONDITIONAL:
15
55
  return {
16
- kind: PlanKind.CONDITIONAL,
56
+ kind: core_1.PlanKind.CONDITIONAL,
17
57
  filters: buildPrismaFilterFromCerbosExpression(queryPlan.condition, mapper),
18
58
  };
19
59
  default:
@@ -22,17 +62,13 @@ export function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
22
62
  }
23
63
  /**
24
64
  * 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
30
65
  */
31
- const resolveFieldReference = (reference, mapper) => {
66
+ function resolveFieldReference(reference, mapper) {
32
67
  const parts = reference.split(".");
33
68
  const config = typeof mapper === "function" ? mapper(reference) : mapper[reference];
34
69
  let matchedPrefix = "";
35
70
  let matchedConfig;
71
+ // If no direct match, look for partial matches
36
72
  if (!config) {
37
73
  for (let i = parts.length - 1; i >= 0; i--) {
38
74
  const prefix = parts.slice(0, i + 1).join(".");
@@ -44,8 +80,9 @@ const resolveFieldReference = (reference, mapper) => {
44
80
  }
45
81
  }
46
82
  }
47
- const activeConfig = config || matchedConfig;
48
- if (activeConfig === null || activeConfig === void 0 ? void 0 : activeConfig.relation) {
83
+ const activeConfig = config ?? matchedConfig;
84
+ // Handle relation mapping
85
+ if (activeConfig?.relation) {
49
86
  const { name, type, fields } = activeConfig.relation;
50
87
  const matchedParts = matchedPrefix ? matchedPrefix.split(".") : [];
51
88
  const remainingParts = matchedPrefix
@@ -53,14 +90,27 @@ const resolveFieldReference = (reference, mapper) => {
53
90
  : parts.slice(1);
54
91
  let field;
55
92
  const relations = [
56
- { name, type, field: activeConfig.relation.field, nestedMapper: fields },
93
+ {
94
+ name,
95
+ type,
96
+ field: activeConfig.relation.field,
97
+ nestedMapper: fields,
98
+ },
57
99
  ];
100
+ // Process nested relations
58
101
  if (fields && remainingParts.length > 0) {
59
102
  let currentMapper = fields;
60
103
  let currentParts = remainingParts;
61
104
  while (currentParts.length > 0) {
62
- const nextConfig = currentMapper[currentParts[0]];
63
- if (nextConfig === null || nextConfig === void 0 ? void 0 : nextConfig.relation) {
105
+ if (!currentMapper) {
106
+ break;
107
+ }
108
+ const currentPart = currentParts[0];
109
+ if (!currentPart) {
110
+ break;
111
+ }
112
+ const nextConfig = currentMapper[currentPart];
113
+ if (nextConfig?.relation) {
64
114
  relations.push({
65
115
  name: nextConfig.relation.name,
66
116
  type: nextConfig.relation.type,
@@ -71,167 +121,130 @@ const resolveFieldReference = (reference, mapper) => {
71
121
  currentParts = currentParts.slice(1);
72
122
  }
73
123
  else {
74
- field = (nextConfig === null || nextConfig === void 0 ? void 0 : nextConfig.field) || currentParts[currentParts.length - 1];
124
+ const lastPart = currentParts[currentParts.length - 1];
125
+ if (!lastPart) {
126
+ break;
127
+ }
128
+ field = nextConfig?.field || lastPart;
75
129
  break;
76
130
  }
77
131
  }
78
132
  }
79
133
  return { path: field ? [field] : remainingParts, relations };
80
134
  }
81
- return { path: [(activeConfig === null || activeConfig === void 0 ? void 0 : activeConfig.field) || reference] };
82
- };
135
+ // Simple field mapping
136
+ return { path: [activeConfig?.field || reference] };
137
+ }
83
138
  /**
84
139
  * Determines the appropriate Prisma operator based on relation type.
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)
91
140
  */
92
- const getPrismaRelationOperator = (relation) => (relation.type === "one" ? "is" : "some");
141
+ function getPrismaRelationOperator(relation) {
142
+ return relation.type === "one" ? "is" : "some";
143
+ }
93
144
  /**
94
145
  * 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
146
  */
101
- const buildNestedRelationFilter = (relations, fieldFilter) => {
147
+ function buildNestedRelationFilter(relations, fieldFilter) {
102
148
  if (relations.length === 0)
103
149
  return fieldFilter;
104
150
  let currentFilter = fieldFilter;
151
+ // Build nested structure from inside out
105
152
  for (let i = relations.length - 1; i >= 0; i--) {
106
153
  const relation = relations[i];
154
+ if (!relation) {
155
+ throw new Error("Relation mapping is missing");
156
+ }
107
157
  const relationOperator = getPrismaRelationOperator(relation);
158
+ // Handle special case for the deepest relation
108
159
  if (relation.field && i === relations.length - 1) {
109
- const [, filterValue] = Object.entries(currentFilter)[0];
110
- currentFilter = { [relation.field]: filterValue };
160
+ const [key, filterValue] = getFilterEntry(currentFilter);
161
+ if (key === "NOT") {
162
+ currentFilter = { NOT: { [relation.field]: filterValue } };
163
+ }
164
+ else {
165
+ currentFilter = { [relation.field]: filterValue };
166
+ }
111
167
  }
112
168
  currentFilter = { [relation.name]: { [relationOperator]: currentFilter } };
113
169
  }
114
170
  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
171
  }
159
172
  /**
160
173
  * 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
174
  */
168
- const resolveOperand = (operand, mapper) => {
169
- if (isPlanExpressionWithName(operand)) {
175
+ function resolveOperand(operand, mapper) {
176
+ if (isNamedOperand(operand)) {
170
177
  return resolveFieldReference(operand.name, mapper);
171
178
  }
172
- else if (isPlanExpressionWithValue(operand)) {
179
+ else if (isValueOperand(operand)) {
173
180
  return { value: operand.value };
174
181
  }
175
- else if (isPlanExpressionWithOperator(operand)) {
182
+ else if (isOperatorOperand(operand)) {
176
183
  const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
177
184
  return { value: nestedResult };
178
185
  }
179
186
  throw new Error("Operand must have name, value, or be an expression");
180
- };
187
+ }
188
+ /**
189
+ * Creates a scoped mapper for collection operations
190
+ */
191
+ function createScopedMapper(collectionPath, variableName, fullMapper) {
192
+ return (key) => {
193
+ // If the key starts with the variable name, it's accessing the collection item
194
+ if (key.startsWith(variableName + ".")) {
195
+ const strippedKey = key.replace(variableName + ".", "");
196
+ const parts = strippedKey.split(".");
197
+ // Get the collection's relation config
198
+ const collectionConfig = typeof fullMapper === "function"
199
+ ? fullMapper(collectionPath)
200
+ : fullMapper[collectionPath];
201
+ if (collectionConfig?.relation?.fields) {
202
+ // For nested paths, traverse the fields configuration
203
+ const baseConfig = collectionConfig.relation.fields;
204
+ if (!baseConfig) {
205
+ return { field: strippedKey };
206
+ }
207
+ let currentConfig = baseConfig;
208
+ let field = parts[0] || strippedKey;
209
+ for (let i = 0; i < parts.length - 1; i++) {
210
+ const part = parts[i];
211
+ const nextPart = parts[i + 1];
212
+ if (!part || !nextPart) {
213
+ break;
214
+ }
215
+ const nextConfig = currentConfig[part];
216
+ if (nextConfig?.relation?.fields) {
217
+ currentConfig = nextConfig.relation.fields;
218
+ field = nextPart;
219
+ }
220
+ else {
221
+ break;
222
+ }
223
+ }
224
+ if (!field) {
225
+ field = strippedKey;
226
+ }
227
+ // Return the field config if it exists, otherwise create a default one
228
+ return currentConfig[field] || { field };
229
+ }
230
+ return { field: strippedKey };
231
+ }
232
+ // For keys not referencing the collection item, use the full mapper
233
+ if (typeof fullMapper === "function") {
234
+ return fullMapper(key);
235
+ }
236
+ return fullMapper[key] || { field: key };
237
+ };
238
+ }
181
239
  /**
182
240
  * 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
189
241
  */
190
- const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
242
+ function buildPrismaFilterFromCerbosExpression(expression, mapper) {
191
243
  // Validate expression structure
192
- if (!("operator" in expression) || !("operands" in expression)) {
244
+ if (!isOperatorOperand(expression)) {
193
245
  throw new Error("Invalid Cerbos expression structure");
194
246
  }
195
247
  const { operator, operands } = expression;
196
- /**
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
202
- */
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;
226
- }
227
- if (!isResolvedValue(right)) {
228
- throw new Error("Right operand must be a value");
229
- }
230
- if (!prismaOperator) {
231
- throw new Error(`Unsupported operator: ${operator}`);
232
- }
233
- return { [prismaOperator]: right.value };
234
- };
235
248
  // Process different operator types
236
249
  switch (operator) {
237
250
  case "and":
@@ -242,272 +255,375 @@ const buildPrismaFilterFromCerbosExpression = (expression, mapper) => {
242
255
  return {
243
256
  OR: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
244
257
  };
245
- case "not":
258
+ case "not": {
259
+ const operand = operands[0];
260
+ if (!operand) {
261
+ throw new Error("not operator requires an operand");
262
+ }
246
263
  return {
247
- NOT: buildPrismaFilterFromCerbosExpression(operands[0], mapper),
264
+ NOT: buildPrismaFilterFromCerbosExpression(operand, mapper),
248
265
  };
266
+ }
249
267
  case "eq":
250
268
  case "ne":
251
269
  case "lt":
252
270
  case "le":
253
271
  case "gt":
254
272
  case "ge": {
255
- const leftOperand = operands.find((o) => isPlanExpressionWithName(o) || isPlanExpressionWithOperator(o));
256
- if (!leftOperand)
257
- throw new Error("No valid left operand found");
258
- const rightOperand = operands.find((o) => o !== leftOperand);
259
- if (!rightOperand)
260
- throw new Error("No valid right operand found");
261
- const left = resolveOperand(leftOperand, mapper);
262
- const right = resolveOperand(rightOperand, mapper);
263
- return processRelationalOperator(operator, left, right);
273
+ return handleRelationalOperator(operator, operands, mapper);
264
274
  }
265
275
  case "in": {
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);
283
- }
284
- return { [path[path.length - 1]]: { in: value } };
276
+ return handleInOperator(operands, mapper);
285
277
  }
286
278
  case "contains":
287
279
  case "startsWith":
288
280
  case "endsWith": {
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;
303
- if (typeof value !== "string")
304
- throw new Error(`${operator} operator requires string value`);
305
- const fieldFilter = { [path[path.length - 1]]: { [operator]: value } };
306
- if (relations && relations.length > 0) {
307
- return buildNestedRelationFilter(relations, fieldFilter);
308
- }
309
- return fieldFilter;
281
+ return handleStringOperator(operator, operands, mapper);
310
282
  }
311
283
  case "isSet": {
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);
330
- }
331
- return fieldFilter;
284
+ return handleIsSetOperator(operands, mapper);
332
285
  }
333
286
  case "hasIntersection": {
334
- if (operands.length !== 2)
335
- throw new Error("hasIntersection requires exactly two operands");
336
- const [leftOperand, rightOperand] = operands;
337
- if ("operator" in leftOperand && leftOperand.operator === "map") {
338
- if (!("value" in rightOperand))
339
- throw new Error("Second operand of hasIntersection must be a value");
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
- });
366
- }
367
- if (!("name" in leftOperand))
368
- throw new Error("First operand of hasIntersection must be a field reference or map expression");
369
- if (!("value" in rightOperand))
370
- throw new Error("Second operand of hasIntersection must be a value");
371
- const { path, relations } = resolveFieldReference(leftOperand.name, mapper);
372
- if (!Array.isArray(rightOperand.value))
373
- throw new Error("hasIntersection requires an array value");
374
- if (relations && relations.length > 0) {
375
- const fieldFilter = {
376
- [path[path.length - 1]]: { in: rightOperand.value },
377
- };
378
- return buildNestedRelationFilter(relations, fieldFilter);
379
- }
380
- return { [path[path.length - 1]]: { some: rightOperand.value } };
287
+ return handleHasIntersectionOperator(operands, mapper);
381
288
  }
382
289
  case "lambda": {
383
- const [condition, variable] = operands;
384
- if (!("name" in variable))
385
- throw new Error("Lambda variable must have a name");
386
- return buildPrismaFilterFromCerbosExpression(condition, (key) => ({ field: key.replace(`${variable.name}.`, "") }));
290
+ return handleLambdaOperator(operands);
387
291
  }
388
292
  case "exists":
389
293
  case "exists_one":
390
294
  case "all":
391
295
  case "except":
392
296
  case "filter": {
393
- if (operands.length !== 2)
394
- throw new Error(`${operator} requires exactly two operands`);
395
- const [collection, lambda] = operands;
396
- if (!("name" in collection))
397
- throw new Error("First operand of exists/all/except must be a collection reference");
398
- if (!("operator" in lambda))
399
- throw new Error("Second operand of exists/all/except must be a lambda expression");
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)
408
- throw new Error(`${operator} operator requires a relation mapping`);
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
- }
423
- switch (operator) {
424
- case "exists":
425
- case "filter":
426
- return { [relation.name]: { some: filterValue } };
427
- case "exists_one":
428
- return {
429
- [relation.name]: {
430
- some: filterValue,
431
- },
432
- AND: [
433
- {
434
- [relation.name]: {
435
- every: {
436
- OR: [filterValue, { NOT: filterValue }],
437
- },
438
- },
439
- },
440
- ],
441
- };
442
- case "all":
443
- return { [relation.name]: { every: filterValue } };
444
- default:
445
- throw new Error(`Unexpected operator: ${operator}`);
446
- }
297
+ return handleCollectionOperator(operator, operands, mapper);
447
298
  }
448
299
  case "map": {
449
- if (operands.length !== 2)
450
- throw new Error("map requires exactly two operands");
451
- const [collection, lambda] = operands;
452
- if (!("name" in collection))
453
- throw new Error("First operand of map must be a collection reference");
454
- if (!("operator" in lambda) || lambda.operator !== "lambda")
455
- throw new Error("Second operand of map must be a lambda expression");
456
- // Get variable name from lambda
457
- const [projection, variable] = lambda.operands;
458
- if (!("name" in projection) || !("name" in variable))
459
- throw new Error("Invalid map lambda expression structure");
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 },
471
- },
472
- });
300
+ return handleMapOperator(operands, mapper);
473
301
  }
474
302
  default:
475
303
  throw new Error(`Unsupported operator: ${operator}`);
476
304
  }
477
- };
305
+ }
478
306
  /**
479
- * Creates a scoped mapper for collection operations that preserves access to parent scope
480
- * and handles nested collections
307
+ * Helper function to process relational operators (eq, ne, lt, etc.)
481
308
  */
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 };
309
+ function handleRelationalOperator(operator, operands, mapper) {
310
+ const prismaOperator = {
311
+ eq: "equals",
312
+ ne: "not",
313
+ lt: "lt",
314
+ le: "lte",
315
+ gt: "gt",
316
+ ge: "gte",
317
+ }[operator];
318
+ if (!prismaOperator) {
319
+ throw new Error(`Unsupported operator: ${operator}`);
320
+ }
321
+ const leftOperand = operands.find((o) => isNamedOperand(o) || isOperatorOperand(o));
322
+ if (!leftOperand)
323
+ throw new Error("No valid left operand found");
324
+ const rightOperand = operands.find((o) => o !== leftOperand);
325
+ if (!rightOperand)
326
+ throw new Error("No valid right operand found");
327
+ const left = resolveOperand(leftOperand, mapper);
328
+ const right = resolveOperand(rightOperand, mapper);
329
+ if (isResolvedFieldReference(left)) {
330
+ const { path, relations } = left;
331
+ if (!isResolvedValue(right)) {
332
+ throw new Error("Right operand must be a value");
333
+ }
334
+ const filterValue = { [prismaOperator]: right.value };
335
+ const fieldName = getLeafField(path);
336
+ const fieldFilter = { [fieldName]: filterValue };
337
+ if (relations && relations.length > 0) {
338
+ return buildNestedRelationFilter(relations, fieldFilter);
508
339
  }
509
- return { field: strippedKey };
340
+ return fieldFilter;
510
341
  }
511
- // For keys not referencing the collection item, use the full mapper
512
- return typeof fullMapper === "function" ? fullMapper(key) : fullMapper[key];
513
- };
342
+ if (!isResolvedValue(right)) {
343
+ throw new Error("Right operand must be a value");
344
+ }
345
+ return { [prismaOperator]: right.value };
346
+ }
347
+ /**
348
+ * Helper function to handle "in" operator
349
+ */
350
+ function handleInOperator(operands, mapper) {
351
+ const nameOperand = operands.find(isNamedOperand);
352
+ if (!nameOperand)
353
+ throw new Error("Name operand is undefined");
354
+ const valueOperand = operands.find(isValueOperand);
355
+ if (!valueOperand)
356
+ throw new Error("Value operand is undefined");
357
+ const resolved = resolveOperand(nameOperand, mapper);
358
+ if (!isResolvedFieldReference(resolved)) {
359
+ throw new Error("Name operand must resolve to a field reference");
360
+ }
361
+ const { path, relations } = resolved;
362
+ const resolvedValue = resolveOperand(valueOperand, mapper);
363
+ if (!isResolvedValue(resolvedValue)) {
364
+ throw new Error("Value operand must resolve to a value");
365
+ }
366
+ const { value } = resolvedValue;
367
+ const values = Array.isArray(value) ? value : [value];
368
+ const fieldName = getLeafField(path);
369
+ if (relations && relations.length > 0) {
370
+ const fieldFilter = values.length === 1
371
+ ? { [fieldName]: values[0] }
372
+ : { [fieldName]: { in: values } };
373
+ return buildNestedRelationFilter(relations, fieldFilter);
374
+ }
375
+ return values.length === 1
376
+ ? { [fieldName]: values[0] }
377
+ : { [fieldName]: { in: values } };
378
+ }
379
+ /**
380
+ * Helper function to handle string operators (contains, startsWith, endsWith)
381
+ */
382
+ function handleStringOperator(operator, operands, mapper) {
383
+ const nameOperand = operands.find(isNamedOperand);
384
+ if (!nameOperand)
385
+ throw new Error("Name operand is undefined");
386
+ const resolved = resolveOperand(nameOperand, mapper);
387
+ if (!isResolvedFieldReference(resolved)) {
388
+ throw new Error("Name operand must resolve to a field reference");
389
+ }
390
+ const { path, relations } = resolved;
391
+ const valueOperand = operands.find(isValueOperand);
392
+ if (!valueOperand)
393
+ throw new Error("Value operand is undefined");
394
+ const resolvedValue = resolveOperand(valueOperand, mapper);
395
+ if (!isResolvedValue(resolvedValue)) {
396
+ throw new Error("Value operand must resolve to a value");
397
+ }
398
+ const { value } = resolvedValue;
399
+ if (typeof value !== "string") {
400
+ throw new Error(`${operator} operator requires string value`);
401
+ }
402
+ const fieldName = getLeafField(path);
403
+ const fieldFilter = { [fieldName]: { [operator]: value } };
404
+ if (relations && relations.length > 0) {
405
+ return buildNestedRelationFilter(relations, fieldFilter);
406
+ }
407
+ return fieldFilter;
408
+ }
409
+ /**
410
+ * Helper function to handle "isSet" operator
411
+ */
412
+ function handleIsSetOperator(operands, mapper) {
413
+ const nameOperand = operands.find(isNamedOperand);
414
+ if (!nameOperand)
415
+ throw new Error("Name operand is undefined");
416
+ const resolved = resolveOperand(nameOperand, mapper);
417
+ if (!isResolvedFieldReference(resolved)) {
418
+ throw new Error("Name operand must resolve to a field reference");
419
+ }
420
+ const { path, relations } = resolved;
421
+ const valueOperand = operands.find(isValueOperand);
422
+ if (!valueOperand)
423
+ throw new Error("Value operand is undefined");
424
+ const resolvedValue = resolveOperand(valueOperand, mapper);
425
+ if (!isResolvedValue(resolvedValue)) {
426
+ throw new Error("Value operand must resolve to a value");
427
+ }
428
+ const fieldName = getLeafField(path);
429
+ const fieldFilter = {
430
+ [fieldName]: resolvedValue.value ? { not: null } : { equals: null },
431
+ };
432
+ if (relations && relations.length > 0) {
433
+ return buildNestedRelationFilter(relations, fieldFilter);
434
+ }
435
+ return fieldFilter;
436
+ }
437
+ /**
438
+ * Helper function to handle "hasIntersection" operator
439
+ */
440
+ function handleHasIntersectionOperator(operands, mapper) {
441
+ if (operands.length !== 2) {
442
+ throw new Error("hasIntersection requires exactly two operands");
443
+ }
444
+ const leftOperand = assertDefined(operands[0], "hasIntersection requires a left operand");
445
+ const rightOperand = assertDefined(operands[1], "hasIntersection requires a right operand");
446
+ // Check if left operand is a map operation
447
+ if (isOperatorOperand(leftOperand) && leftOperand.operator === "map") {
448
+ if (!isValueOperand(rightOperand)) {
449
+ throw new Error("Second operand of hasIntersection must be a value");
450
+ }
451
+ const collection = assertDefined(leftOperand.operands[0], "Map expression must include a collection reference");
452
+ const lambda = assertDefined(leftOperand.operands[1], "Map expression must include a lambda expression");
453
+ if (!isNamedOperand(collection)) {
454
+ throw new Error("First operand of map must be a collection reference");
455
+ }
456
+ // Get variable name from lambda
457
+ if (!isOperatorOperand(lambda)) {
458
+ throw new Error("Lambda expression must have operands");
459
+ }
460
+ const variable = assertDefined(lambda.operands[1], "Lambda variable must have a name");
461
+ if (!isNamedOperand(variable)) {
462
+ throw new Error("Lambda variable must have a name");
463
+ }
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 || relations.length === 0) {
468
+ throw new Error("Map operation requires relations");
469
+ }
470
+ const projection = assertDefined(lambda.operands[0], "Invalid map lambda expression structure");
471
+ if (!isNamedOperand(projection)) {
472
+ throw new Error("Invalid map lambda expression structure");
473
+ }
474
+ // Use scoped mapper for resolving the projection
475
+ const resolved = resolveFieldReference(projection.name, scopedMapper);
476
+ const fieldName = getLeafField(resolved.path);
477
+ return buildNestedRelationFilter(relations, {
478
+ [fieldName]: { in: rightOperand.value },
479
+ });
480
+ }
481
+ // Handle regular field reference
482
+ if (!isNamedOperand(leftOperand)) {
483
+ throw new Error("First operand of hasIntersection must be a field reference or map expression");
484
+ }
485
+ if (!isValueOperand(rightOperand)) {
486
+ throw new Error("Second operand of hasIntersection must be a value");
487
+ }
488
+ const { path, relations } = resolveFieldReference(leftOperand.name, mapper);
489
+ if (!Array.isArray(rightOperand.value)) {
490
+ throw new Error("hasIntersection requires an array value");
491
+ }
492
+ if (relations && relations.length > 0) {
493
+ const fieldName = getLeafField(path);
494
+ const fieldFilter = {
495
+ [fieldName]: { in: rightOperand.value },
496
+ };
497
+ return buildNestedRelationFilter(relations, fieldFilter);
498
+ }
499
+ const fieldName = getLeafField(path);
500
+ return { [fieldName]: { some: rightOperand.value } };
501
+ }
502
+ /**
503
+ * Helper function to handle "lambda" operator
504
+ */
505
+ function handleLambdaOperator(operands) {
506
+ const condition = assertDefined(operands[0], "Lambda requires a condition operand");
507
+ const variable = assertDefined(operands[1], "Lambda requires a variable operand");
508
+ if (!isNamedOperand(variable)) {
509
+ throw new Error("Lambda variable must have a name");
510
+ }
511
+ return buildPrismaFilterFromCerbosExpression(condition, (key) => ({
512
+ field: key.replace(`${variable.name}.`, ""),
513
+ }));
514
+ }
515
+ /**
516
+ * Helper function to handle collection operators (exists, all, except, filter)
517
+ */
518
+ function handleCollectionOperator(operator, operands, mapper) {
519
+ if (operands.length !== 2) {
520
+ throw new Error(`${operator} requires exactly two operands`);
521
+ }
522
+ const collection = assertDefined(operands[0], `${operator} requires a collection operand`);
523
+ const lambda = assertDefined(operands[1], `${operator} requires a lambda operand`);
524
+ if (!isNamedOperand(collection)) {
525
+ throw new Error(`First operand of ${operator} must be a collection reference`);
526
+ }
527
+ if (!isOperatorOperand(lambda)) {
528
+ throw new Error(`Second operand of ${operator} must be a lambda expression`);
529
+ }
530
+ // Get variable name from lambda
531
+ const variable = assertDefined(lambda.operands[1], "Lambda variable must have a name");
532
+ if (!isNamedOperand(variable)) {
533
+ throw new Error("Lambda variable must have a name");
534
+ }
535
+ // Create scoped mapper for the collection
536
+ const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
537
+ const { relations } = resolveFieldReference(collection.name, mapper);
538
+ if (!relations || relations.length === 0) {
539
+ throw new Error(`${operator} operator requires a relation mapping`);
540
+ }
541
+ const lambdaConditionOperand = assertDefined(lambda.operands[0], "Lambda expression must provide a condition");
542
+ const lambdaCondition = buildPrismaFilterFromCerbosExpression(lambdaConditionOperand, // Use the condition part of the lambda
543
+ scopedMapper);
544
+ const relation = assertDefined(relations[0], `${operator} operator requires a relation mapping`);
545
+ let filterValue = lambdaCondition;
546
+ // If the lambda condition already has a relation structure, merge it
547
+ if (lambdaCondition["AND"] || lambdaCondition["OR"]) {
548
+ filterValue = lambdaCondition;
549
+ }
550
+ else {
551
+ const lambdaKeys = Object.keys(lambdaCondition);
552
+ const defaultKey = lambdaKeys[0];
553
+ if (!defaultKey) {
554
+ throw new Error("Lambda condition must have at least one field");
555
+ }
556
+ const lambdaFieldValue = lambdaCondition[defaultKey];
557
+ if (lambdaFieldValue === undefined) {
558
+ throw new Error("Lambda condition field value cannot be undefined");
559
+ }
560
+ const filterField = relation.field || defaultKey;
561
+ filterValue = {
562
+ [filterField]: lambdaFieldValue,
563
+ };
564
+ }
565
+ switch (operator) {
566
+ case "exists":
567
+ case "filter":
568
+ return { [relation.name]: { some: filterValue } };
569
+ case "except":
570
+ return { [relation.name]: { some: { NOT: filterValue } } };
571
+ case "exists_one":
572
+ return {
573
+ [relation.name]: {
574
+ some: filterValue,
575
+ },
576
+ AND: [
577
+ {
578
+ [relation.name]: {
579
+ every: {
580
+ OR: [filterValue, { NOT: filterValue }],
581
+ },
582
+ },
583
+ },
584
+ ],
585
+ };
586
+ case "all":
587
+ return { [relation.name]: { every: filterValue } };
588
+ default:
589
+ throw new Error(`Unexpected operator: ${operator}`);
590
+ }
591
+ }
592
+ /**
593
+ * Helper function to handle "map" operator
594
+ */
595
+ function handleMapOperator(operands, mapper) {
596
+ if (operands.length !== 2) {
597
+ throw new Error("map requires exactly two operands");
598
+ }
599
+ const collection = assertDefined(operands[0], "map requires a collection operand");
600
+ const lambda = assertDefined(operands[1], "map requires a lambda operand");
601
+ if (!isNamedOperand(collection)) {
602
+ throw new Error("First operand of map must be a collection reference");
603
+ }
604
+ if (!isOperatorOperand(lambda) || lambda.operator !== "lambda") {
605
+ throw new Error("Second operand of map must be a lambda expression");
606
+ }
607
+ // Get variable name from lambda
608
+ const projection = assertDefined(lambda.operands[0], "Map lambda expression must provide a projection");
609
+ const variable = assertDefined(lambda.operands[1], "Map lambda expression must provide a variable");
610
+ if (!isNamedOperand(projection) || !isNamedOperand(variable)) {
611
+ throw new Error("Invalid map lambda expression structure");
612
+ }
613
+ // Create scoped mapper for the collection
614
+ const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
615
+ const { relations } = resolveFieldReference(collection.name, mapper);
616
+ if (!relations || relations.length === 0) {
617
+ throw new Error("map operator requires a relation mapping");
618
+ }
619
+ // Use scoped mapper for resolving the projection
620
+ const resolved = resolveFieldReference(projection.name, scopedMapper);
621
+ const fieldName = getLeafField(resolved.path);
622
+ const lastRelation = assertDefined(relations[relations.length - 1], "Relation mapping must contain at least one relation");
623
+ return buildNestedRelationFilter(relations, {
624
+ [getPrismaRelationOperator(lastRelation)]: {
625
+ select: { [fieldName]: true },
626
+ },
627
+ });
628
+ }
629
+ //# sourceMappingURL=index.js.map