@gavdi/cap-mcp 0.9.0 → 0.9.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.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpSessionManager = void 0;
4
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
5
+ const crypto_1 = require("crypto");
6
+ const env_sanitizer_1 = require("../config/env-sanitizer");
7
+ const logger_1 = require("../logger");
8
+ const factory_1 = require("./factory");
9
+ /**
10
+ * Manages active MCP server sessions and their lifecycle
11
+ * Handles session creation, storage, retrieval, and cleanup for MCP protocol communication
12
+ */
13
+ class McpSessionManager {
14
+ /** Map storing active sessions by their unique session IDs */
15
+ sessions;
16
+ /**
17
+ * Creates a new session manager with empty session storage
18
+ */
19
+ constructor() {
20
+ this.sessions = new Map();
21
+ }
22
+ /**
23
+ * Retrieves the complete map of active sessions
24
+ * @returns Map of session IDs to their corresponding session objects
25
+ */
26
+ getSessions() {
27
+ return this.sessions;
28
+ }
29
+ /**
30
+ * Checks if a session exists for the given session ID
31
+ * @param sessionID - Unique identifier for the session
32
+ * @returns True if session exists, false otherwise
33
+ */
34
+ hasSession(sessionID) {
35
+ return this.sessions.has(sessionID);
36
+ }
37
+ /**
38
+ * Retrieves a specific session by its ID
39
+ * @param sessionID - Unique identifier for the session
40
+ * @returns Session object if found, undefined otherwise
41
+ */
42
+ getSession(sessionID) {
43
+ return this.sessions.get(sessionID);
44
+ }
45
+ /**
46
+ * Creates a new MCP session with server and transport configuration
47
+ * Initializes MCP server with provided annotations and establishes transport connection
48
+ * @param config - CAP configuration for the MCP server
49
+ * @param annotations - Optional parsed MCP annotations for resources, tools, and prompts
50
+ * @returns Promise resolving to the created session object
51
+ */
52
+ async createSession(config, annotations) {
53
+ logger_1.LOGGER.debug("Initialize session request received");
54
+ const server = (0, factory_1.createMcpServer)(config, annotations);
55
+ const transport = this.createTransport(server);
56
+ await server.connect(transport);
57
+ return { server, transport };
58
+ }
59
+ /**
60
+ * Creates and configures HTTP transport for MCP communication
61
+ * Sets up session ID generation, response format, and event handlers
62
+ * @param server - MCP server instance to associate with the transport
63
+ * @returns Configured StreamableHTTPServerTransport instance
64
+ */
65
+ createTransport(server) {
66
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
67
+ sessionIdGenerator: () => (0, crypto_1.randomUUID)(),
68
+ enableJsonResponse: (0, env_sanitizer_1.isTestEnvironment)(),
69
+ onsessioninitialized: (sid) => {
70
+ logger_1.LOGGER.debug("Session initialized with ID: ", sid);
71
+ this.sessions.set(sid, {
72
+ server: server,
73
+ transport: transport,
74
+ });
75
+ },
76
+ });
77
+ transport.onclose = () => this.onCloseSession(transport);
78
+ return transport;
79
+ }
80
+ /**
81
+ * Handles session cleanup when transport connection closes
82
+ * Removes the session from active sessions map when connection terminates
83
+ * @param transport - Transport instance that was closed
84
+ */
85
+ onCloseSession(transport) {
86
+ if (!transport.sessionId || !this.sessions.has(transport.sessionId)) {
87
+ return;
88
+ }
89
+ this.sessions.delete(transport.sessionId);
90
+ }
91
+ }
92
+ exports.McpSessionManager = McpSessionManager;
package/lib/mcp/tools.js CHANGED
@@ -4,11 +4,14 @@ exports.assignToolToServer = assignToolToServer;
4
4
  const utils_1 = require("./utils");
5
5
  const logger_1 = require("../logger");
6
6
  const constants_1 = require("./constants");
7
+ const zod_1 = require("zod");
7
8
  /* @ts-ignore */
8
9
  const cds = global.cds || require("@sap/cds"); // This is a work around for missing cds context
9
10
  /**
10
- * Assigns the annotated tool to the server.
11
- * This is done by reference, and will therefore mutate the provided server.
11
+ * Registers a CAP function or action as an executable MCP tool
12
+ * Handles both bound (entity-level) and unbound (service-level) operations
13
+ * @param model - The tool annotation containing operation metadata and parameters
14
+ * @param server - The MCP server instance to register the tool with
12
15
  */
13
16
  function assignToolToServer(model, server) {
14
17
  logger_1.LOGGER.debug("Adding tool", model);
@@ -21,7 +24,11 @@ function assignToolToServer(model, server) {
21
24
  assignUnboundOperation(parameters, model, server);
22
25
  }
23
26
  /**
24
- * Creates tool handler for bound action/function imports
27
+ * Registers a bound operation that operates on a specific entity instance
28
+ * Requires entity key parameters in addition to operation parameters
29
+ * @param params - Zod schema definitions for operation parameters
30
+ * @param model - Tool annotation with bound operation metadata
31
+ * @param server - MCP server instance to register with
25
32
  */
26
33
  function assignBoundOperation(params, model, server) {
27
34
  if (!model.keyTypeMap || model.keyTypeMap.size <= 0) {
@@ -29,7 +36,12 @@ function assignBoundOperation(params, model, server) {
29
36
  throw new Error("Bound operation cannot be assigned to tool list, missing keys");
30
37
  }
31
38
  const keys = buildToolParameters(model.keyTypeMap);
32
- server.registerTool(model.name, { ...keys, ...params }, async (data) => {
39
+ const inputSchema = buildZodSchema({ ...keys, ...params });
40
+ server.registerTool(model.name, {
41
+ title: model.name,
42
+ description: model.description,
43
+ inputSchema: inputSchema,
44
+ }, async (args) => {
33
45
  const service = cds.services[model.serviceName];
34
46
  if (!service) {
35
47
  logger_1.LOGGER.error("Invalid CAP service - undefined");
@@ -45,7 +57,7 @@ function assignBoundOperation(params, model, server) {
45
57
  }
46
58
  const operationInput = {};
47
59
  const operationKeys = {};
48
- for (const [k, v] of Object.entries(data)) {
60
+ for (const [k, v] of Object.entries(args)) {
49
61
  if (model.keyTypeMap?.has(k)) {
50
62
  operationKeys[k] = v;
51
63
  }
@@ -61,16 +73,28 @@ function assignBoundOperation(params, model, server) {
61
73
  });
62
74
  return {
63
75
  content: Array.isArray(response)
64
- ? response.map((el) => ({ type: "text", text: String(el) }))
65
- : [{ type: "text", text: String(response) }],
76
+ ? response.map((el) => ({
77
+ type: "text",
78
+ text: formatResponseValue(el),
79
+ }))
80
+ : [{ type: "text", text: formatResponseValue(response) }],
66
81
  };
67
82
  });
68
83
  }
69
84
  /**
70
- * Creates a tool handler for unbound action/function imports
85
+ * Registers an unbound operation that operates at the service level
86
+ * Does not require entity keys, only operation parameters
87
+ * @param params - Zod schema definitions for operation parameters
88
+ * @param model - Tool annotation with unbound operation metadata
89
+ * @param server - MCP server instance to register with
71
90
  */
72
91
  function assignUnboundOperation(params, model, server) {
73
- server.registerTool(model.name, params, async (data) => {
92
+ const inputSchema = buildZodSchema(params);
93
+ server.registerTool(model.name, {
94
+ title: model.name,
95
+ description: model.description,
96
+ inputSchema: inputSchema,
97
+ }, async (args) => {
74
98
  const service = cds.services[model.serviceName];
75
99
  if (!service) {
76
100
  logger_1.LOGGER.error("Invalid CAP service - undefined");
@@ -84,16 +108,21 @@ function assignUnboundOperation(params, model, server) {
84
108
  ],
85
109
  };
86
110
  }
87
- const response = await service.send(model.target, data);
111
+ const response = await service.send(model.target, args);
88
112
  return {
89
113
  content: Array.isArray(response)
90
- ? response.map((el) => ({ type: "text", text: String(el) }))
91
- : [{ type: "text", text: String(response) }],
114
+ ? response.map((el) => ({
115
+ type: "text",
116
+ text: formatResponseValue(el),
117
+ }))
118
+ : [{ type: "text", text: formatResponseValue(response) }],
92
119
  };
93
120
  });
94
121
  }
95
122
  /**
96
- * Builds the parameters that the MCP server should take in for the given tool's parameters
123
+ * Converts a map of CDS parameter types to MCP parameter schema definitions
124
+ * @param params - Map of parameter names to their CDS type strings
125
+ * @returns Record of parameter names to Zod schema types
97
126
  */
98
127
  function buildToolParameters(params) {
99
128
  if (!params || params.size <= 0)
@@ -104,3 +133,43 @@ function buildToolParameters(params) {
104
133
  }
105
134
  return result;
106
135
  }
136
+ /**
137
+ * Converts a value to a string representation suitable for MCP responses
138
+ * Handles objects and arrays by JSON stringifying them instead of using String()
139
+ * @param value - The value to convert to string
140
+ * @returns String representation of the value
141
+ */
142
+ function formatResponseValue(value) {
143
+ if (value === null || value === undefined) {
144
+ return String(value);
145
+ }
146
+ if (typeof value === "object") {
147
+ try {
148
+ return JSON.stringify(value, null, 2);
149
+ }
150
+ catch (error) {
151
+ // Fallback to String() if JSON.stringify fails (e.g., circular references)
152
+ return String(value);
153
+ }
154
+ }
155
+ return String(value);
156
+ }
157
+ /**
158
+ * Constructs a complete Zod schema object for MCP tool input validation
159
+ * @param params - Record of parameter names to Zod schema types
160
+ * @returns Zod schema record suitable for MCP tool registration
161
+ */
162
+ function buildZodSchema(params) {
163
+ const schema = {};
164
+ for (const [key, zodType] of Object.entries(params)) {
165
+ // The parameter is already a Zod type from determineMcpParameterType
166
+ if (zodType && typeof zodType === "object" && "describe" in zodType) {
167
+ schema[key] = zodType;
168
+ }
169
+ else {
170
+ // Fallback to string if not a valid Zod type
171
+ schema[key] = zod_1.z.string().describe(`Parameter: ${key}`);
172
+ }
173
+ }
174
+ return schema;
175
+ }
package/lib/mcp/utils.js CHANGED
@@ -6,7 +6,9 @@ exports.writeODataDescriptionForResource = writeODataDescriptionForResource;
6
6
  const constants_1 = require("./constants");
7
7
  const zod_1 = require("zod");
8
8
  /**
9
- * Takes in the string based type name of the CDS type found through CSN and converts it to zod type
9
+ * Converts a CDS type string to the corresponding Zod schema type
10
+ * @param cdsType - The CDS type name (e.g., 'String', 'Integer')
11
+ * @returns Zod schema instance for the given type
10
12
  */
11
13
  function determineMcpParameterType(cdsType) {
12
14
  switch (cdsType) {
@@ -19,8 +21,10 @@ function determineMcpParameterType(cdsType) {
19
21
  }
20
22
  }
21
23
  /**
22
- * Session handler for MCP server.
23
- * Rejects or approves incoming requests based on existing MCP session header ID
24
+ * Handles incoming MCP session requests by validating session IDs and routing to appropriate session
25
+ * @param req - Express request object containing session headers
26
+ * @param res - Express response object for sending responses
27
+ * @param sessions - Map of active MCP sessions keyed by session ID
24
28
  */
25
29
  async function handleMcpSessionRequest(req, res, sessions) {
26
30
  const sessionIdHeader = req.headers[constants_1.MCP_SESSION_HEADER];
@@ -35,6 +39,11 @@ async function handleMcpSessionRequest(req, res, sessions) {
35
39
  }
36
40
  await session.transport.handleRequest(req, res);
37
41
  }
42
+ /**
43
+ * Writes a detailed OData description for a resource including available query parameters and properties
44
+ * @param model - The resource annotation to generate description for
45
+ * @returns Formatted description string with OData query syntax examples
46
+ */
38
47
  function writeODataDescriptionForResource(model) {
39
48
  let description = `${model.description}.${constants_1.NEW_LINE}`;
40
49
  description += `Should be queried using OData v4 query style using the following allowed parameters.${constants_1.NEW_LINE}`;
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ODataValidationError = exports.ODataQueryValidator = exports.ODataQueryValidationSchemas = void 0;
4
+ const zod_1 = require("zod");
5
+ const logger_1 = require("../logger");
6
+ /**
7
+ * Comprehensive validation system for OData query parameters with security controls
8
+ * Provides schema validation, injection attack prevention, and type safety
9
+ */
10
+ // Allowed OData operators
11
+ const ALLOWED_OPERATORS = new Set([
12
+ "eq",
13
+ "ne",
14
+ "gt",
15
+ "ge",
16
+ "lt",
17
+ "le",
18
+ "and",
19
+ "or",
20
+ "not",
21
+ "contains",
22
+ "startswith",
23
+ "endswith",
24
+ "indexof",
25
+ "length",
26
+ "substring",
27
+ "tolower",
28
+ "toupper",
29
+ "trim",
30
+ ]);
31
+ // Forbidden patterns that could indicate injection attempts
32
+ const FORBIDDEN_PATTERNS = [
33
+ /;/g, // SQL statement terminator
34
+ /--/g, // SQL comment
35
+ /\/\*/g, // Multi-line comment start
36
+ /\*\//g, // Multi-line comment end
37
+ /xp_/gi, // Extended procedures
38
+ /sp_/gi, // Stored procedures
39
+ /exec/gi, // Execute command
40
+ /union/gi, // Union queries
41
+ /insert/gi, // Insert statements
42
+ /update/gi, // Update statements
43
+ /delete/gi, // Delete statements
44
+ /drop/gi, // Drop statements
45
+ /create/gi, // Create statements
46
+ /alter/gi, // Alter statements
47
+ /script/gi, // Script tags
48
+ /javascript/gi, // JavaScript
49
+ /eval/gi, // Eval function
50
+ /expression/gi, // Expression evaluation
51
+ /\bor\s+\d+\s*=\s*\d+/gi, // SQL injection pattern like \"OR 1=1\"
52
+ /\band\s+\d+\s*=\s*\d+/gi, // SQL injection pattern like \"AND 1=1\"
53
+ ];
54
+ // Validation schemas
55
+ exports.ODataQueryValidationSchemas = {
56
+ top: zod_1.z.number().int().min(1).max(1000),
57
+ skip: zod_1.z.number().int().min(0),
58
+ select: zod_1.z
59
+ .string()
60
+ .min(1)
61
+ .max(500)
62
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_,\s]*$/),
63
+ orderby: zod_1.z
64
+ .string()
65
+ .min(1)
66
+ .max(200)
67
+ .regex(/^[a-zA-Z_][a-zA-Z0-9_\s]+(asc|desc)?(,\s*[a-zA-Z_][a-zA-Z0-9_\s]+(asc|desc)?)*$/i),
68
+ filter: zod_1.z.string().min(1).max(1000),
69
+ };
70
+ /**
71
+ * Validates and sanitizes OData query parameters with comprehensive security checks
72
+ * Prevents injection attacks, validates property references, and ensures type safety
73
+ */
74
+ class ODataQueryValidator {
75
+ allowedProperties;
76
+ allowedTypes;
77
+ /**
78
+ * Creates a new OData query validator for a specific entity
79
+ * @param properties - Map of allowed entity properties to their CDS types
80
+ */
81
+ constructor(properties) {
82
+ this.allowedProperties = new Set(properties.keys());
83
+ this.allowedTypes = new Map(properties);
84
+ }
85
+ /**
86
+ * Validates and parses the $top query parameter
87
+ * @param value - String value of the $top parameter
88
+ * @returns Validated integer value between 1 and 1000
89
+ * @throws Error if value is invalid or out of range
90
+ */
91
+ validateTop(value) {
92
+ const parsed = parseFloat(value);
93
+ if (isNaN(parsed) || !Number.isInteger(parsed)) {
94
+ throw new Error(`Invalid top parameter: ${value}`);
95
+ }
96
+ return exports.ODataQueryValidationSchemas.top.parse(parsed);
97
+ }
98
+ /**
99
+ * Validates and parses the $skip query parameter
100
+ * @param value - String value of the $skip parameter
101
+ * @returns Validated non-negative integer value
102
+ * @throws Error if value is invalid or negative
103
+ */
104
+ validateSkip(value) {
105
+ const parsed = parseFloat(value);
106
+ if (isNaN(parsed) || !Number.isInteger(parsed)) {
107
+ throw new Error(`Invalid skip parameter: ${value}`);
108
+ }
109
+ return exports.ODataQueryValidationSchemas.skip.parse(parsed);
110
+ }
111
+ /**
112
+ * Validates and sanitizes the $select query parameter
113
+ * @param value - Comma-separated list of property names
114
+ * @returns Array of validated property names
115
+ * @throws Error if any property is invalid or not allowed
116
+ */
117
+ validateSelect(value) {
118
+ const decoded = decodeURIComponent(value);
119
+ const validated = exports.ODataQueryValidationSchemas.select.parse(decoded);
120
+ const columns = validated.split(",").map((col) => col.trim());
121
+ // Validate each column exists in entity
122
+ for (const column of columns) {
123
+ if (!this.allowedProperties.has(column)) {
124
+ throw new Error(`Invalid select column: ${column}. Allowed columns: ${Array.from(this.allowedProperties).join(", ")}`);
125
+ }
126
+ }
127
+ return columns;
128
+ }
129
+ /**
130
+ * Validates and sanitizes the $orderby query parameter
131
+ * @param value - Order by clause with property names and optional asc/desc
132
+ * @returns Validated order by string
133
+ * @throws Error if any property is invalid or not allowed
134
+ */
135
+ validateOrderBy(value) {
136
+ const decoded = decodeURIComponent(value);
137
+ const validated = exports.ODataQueryValidationSchemas.orderby.parse(decoded);
138
+ // Extract property names and validate they exist
139
+ const orderClauses = validated.split(",").map((clause) => clause.trim());
140
+ for (const clause of orderClauses) {
141
+ const parts = clause.split(/\s+/);
142
+ const property = parts[0];
143
+ if (!this.allowedProperties.has(property)) {
144
+ throw new Error(`Invalid orderby property: ${property}. Allowed properties: ${Array.from(this.allowedProperties).join(", ")}`);
145
+ }
146
+ }
147
+ return validated;
148
+ }
149
+ /**
150
+ * Validates and sanitizes the $filter query parameter with comprehensive security checks
151
+ * Prevents injection attacks, validates operators and property references
152
+ * @param value - OData filter expression
153
+ * @returns Sanitized filter string in CDS syntax
154
+ * @throws Error if filter contains forbidden patterns or invalid syntax
155
+ */
156
+ validateFilter(value) {
157
+ const input = value?.replace("filter=", "");
158
+ if (!input || input.trim().length === 0) {
159
+ throw new Error("Filter parameter cannot be empty");
160
+ }
161
+ const decoded = decodeURIComponent(input);
162
+ const validated = exports.ODataQueryValidationSchemas.filter.parse(decoded);
163
+ // Check for forbidden patterns
164
+ for (const pattern of FORBIDDEN_PATTERNS) {
165
+ if (pattern.test(validated)) {
166
+ logger_1.LOGGER.warn(`Potentially malicious filter pattern detected: ${pattern.source}`);
167
+ throw new Error("Filter contains forbidden patterns");
168
+ }
169
+ }
170
+ // Parse and validate filter structure
171
+ return this.parseAndValidateFilter(validated);
172
+ }
173
+ /**
174
+ * Parses OData filter expression and validates all components
175
+ * @param filter - Decoded and pre-validated filter string
176
+ * @returns CDS-compatible filter expression
177
+ */
178
+ parseAndValidateFilter(filter) {
179
+ // Tokenize the filter expression
180
+ const tokens = this.tokenizeFilter(filter);
181
+ // Validate tokens
182
+ this.validateFilterTokens(tokens);
183
+ // Convert OData operators to CDS syntax
184
+ return this.convertToCdsFilter(tokens);
185
+ }
186
+ /**
187
+ * Tokenizes filter expression into structured components for validation
188
+ * @param filter - Filter string to tokenize
189
+ * @returns Array of typed tokens representing the filter structure
190
+ */
191
+ tokenizeFilter(filter) {
192
+ const tokens = [];
193
+ // Enhanced tokenizer with OData operator support - literals first to avoid misclassification
194
+ const tokenRegex = /(\b(?:eq|ne|gt|ge|lt|le|contains|startswith|endswith)\b)|('[^']*'|"[^"]*"|\d+(?:\.\d+)?)|([<>=!]+)|(\b(?:and|or|not)\b)|(\(|\))|(\w+)/gi;
195
+ let match;
196
+ while ((match = tokenRegex.exec(filter)) !== null) {
197
+ const token = match[0];
198
+ if (match[1]) {
199
+ // OData operators
200
+ tokens.push({ type: "operator", value: token.toLowerCase() });
201
+ }
202
+ else if (match[2]) {
203
+ // Literal values (strings, numbers) - prioritized to avoid misclassification
204
+ tokens.push({ type: "literal", value: token });
205
+ }
206
+ else if (match[3]) {
207
+ // Comparison operators
208
+ tokens.push({ type: "operator", value: token });
209
+ }
210
+ else if (match[4]) {
211
+ // Logical operators
212
+ tokens.push({ type: "logical", value: token.toLowerCase() });
213
+ }
214
+ else if (match[5]) {
215
+ // Parentheses
216
+ tokens.push({ type: "paren", value: token });
217
+ }
218
+ else if (match[6]) {
219
+ // Property or function names
220
+ tokens.push({ type: "property", value: token });
221
+ }
222
+ }
223
+ return tokens;
224
+ }
225
+ /**
226
+ * Validates all filter tokens against allowed properties and operators
227
+ * @param tokens - Array of parsed filter tokens to validate
228
+ * @throws Error if any token contains invalid properties or operators
229
+ */
230
+ validateFilterTokens(tokens) {
231
+ for (const token of tokens) {
232
+ switch (token.type) {
233
+ case "property":
234
+ // Check if it's a known OData function
235
+ if (!ALLOWED_OPERATORS.has(token.value.toLowerCase()) &&
236
+ !this.allowedProperties.has(token.value)) {
237
+ throw new Error(`Invalid property in filter: ${token.value}. Allowed properties: ${Array.from(this.allowedProperties).join(", ")}`);
238
+ }
239
+ break;
240
+ case "operator":
241
+ // Validate operator format (both symbols and OData operators)
242
+ if (!/^[<>=!]+$/.test(token.value) &&
243
+ !ALLOWED_OPERATORS.has(token.value.toLowerCase())) {
244
+ throw new Error(`Invalid operator: ${token.value}`);
245
+ }
246
+ break;
247
+ case "logical":
248
+ if (!["and", "or", "not"].includes(token.value)) {
249
+ throw new Error(`Invalid logical operator: ${token.value}`);
250
+ }
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ /**
256
+ * Converts validated OData filter tokens to CDS-compatible filter syntax
257
+ * @param tokens - Array of validated filter tokens
258
+ * @returns CDS filter expression string
259
+ */
260
+ convertToCdsFilter(tokens) {
261
+ const cdsTokens = [];
262
+ for (const token of tokens) {
263
+ if (token.type === "operator") {
264
+ // Convert OData operators to CDS operators
265
+ switch (token.value.toLowerCase()) {
266
+ case "eq":
267
+ cdsTokens.push("=");
268
+ break;
269
+ case "ne":
270
+ cdsTokens.push("!=");
271
+ break;
272
+ case "gt":
273
+ cdsTokens.push(">");
274
+ break;
275
+ case "ge":
276
+ cdsTokens.push(">=");
277
+ break;
278
+ case "lt":
279
+ cdsTokens.push("<");
280
+ break;
281
+ case "le":
282
+ cdsTokens.push("<=");
283
+ break;
284
+ default:
285
+ cdsTokens.push(token.value);
286
+ break;
287
+ }
288
+ }
289
+ else {
290
+ cdsTokens.push(token.value);
291
+ continue;
292
+ }
293
+ }
294
+ return cdsTokens.join(" ");
295
+ }
296
+ }
297
+ exports.ODataQueryValidator = ODataQueryValidator;
298
+ /**
299
+ * Specialized error class for OData query parameter validation failures
300
+ * Provides additional context about which parameter and value caused the error
301
+ */
302
+ class ODataValidationError extends Error {
303
+ parameter;
304
+ value;
305
+ /**
306
+ * Creates a new OData validation error
307
+ * @param message - Error description
308
+ * @param parameter - Name of the parameter that failed validation
309
+ * @param value - The invalid value that caused the error
310
+ */
311
+ constructor(message, parameter, value) {
312
+ super(message);
313
+ this.parameter = parameter;
314
+ this.value = value;
315
+ this.name = "ODataValidationError";
316
+ }
317
+ }
318
+ exports.ODataValidationError = ODataValidationError;