@railtownai/railengine 0.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.
@@ -0,0 +1,890 @@
1
+ /**
2
+ * Base error class for all Railtown API errors.
3
+ */
4
+ class RailtownError extends Error {
5
+ constructor(message, statusCode, responseBody) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.responseBody = responseBody;
9
+ this.name = "RailtownError";
10
+ Object.setPrototypeOf(this, RailtownError.prototype);
11
+ }
12
+ }
13
+ /**
14
+ * Error thrown when authentication fails (401).
15
+ */
16
+ class RailtownUnauthorizedError extends RailtownError {
17
+ constructor(message = "Authentication failed", responseBody) {
18
+ super(message, 401, responseBody);
19
+ this.name = "RailtownUnauthorizedError";
20
+ Object.setPrototypeOf(this, RailtownUnauthorizedError.prototype);
21
+ }
22
+ }
23
+ /**
24
+ * Error thrown when a resource is not found (404).
25
+ */
26
+ class RailtownNotFoundError extends RailtownError {
27
+ constructor(message = "Resource not found", responseBody) {
28
+ super(message, 404, responseBody);
29
+ this.name = "RailtownNotFoundError";
30
+ Object.setPrototypeOf(this, RailtownNotFoundError.prototype);
31
+ }
32
+ }
33
+ /**
34
+ * Error thrown when a bad request is made (400).
35
+ */
36
+ class RailtownBadRequestError extends RailtownError {
37
+ constructor(message = "Bad request", responseBody) {
38
+ super(message, 400, responseBody);
39
+ this.name = "RailtownBadRequestError";
40
+ Object.setPrototypeOf(this, RailtownBadRequestError.prototype);
41
+ }
42
+ }
43
+ /**
44
+ * Error thrown when a conflict occurs (409).
45
+ */
46
+ class RailtownConflictError extends RailtownError {
47
+ constructor(message = "Conflict", responseBody) {
48
+ super(message, 409, responseBody);
49
+ this.name = "RailtownConflictError";
50
+ Object.setPrototypeOf(this, RailtownConflictError.prototype);
51
+ }
52
+ }
53
+ /**
54
+ * Error thrown when a server error occurs (500+).
55
+ */
56
+ class RailtownServerError extends RailtownError {
57
+ constructor(message = "Server error", statusCode = 500, responseBody) {
58
+ super(message, statusCode, responseBody);
59
+ this.name = "RailtownServerError";
60
+ Object.setPrototypeOf(this, RailtownServerError.prototype);
61
+ }
62
+ }
63
+ /**
64
+ * Maps HTTP status codes to appropriate Railtown error classes.
65
+ */
66
+ function createRailtownError(statusCode, message, responseBody) {
67
+ switch (statusCode) {
68
+ case 401:
69
+ return new RailtownUnauthorizedError(message, responseBody);
70
+ case 404:
71
+ return new RailtownNotFoundError(message, responseBody);
72
+ case 400:
73
+ return new RailtownBadRequestError(message, responseBody);
74
+ case 409:
75
+ return new RailtownConflictError(message, responseBody);
76
+ default:
77
+ if (statusCode >= 500) {
78
+ return new RailtownServerError(message, statusCode, responseBody);
79
+ }
80
+ return new RailtownError(message || `HTTP ${statusCode}`, statusCode, responseBody);
81
+ }
82
+ }
83
+ /**
84
+ * Strips the /api suffix from a URL if present.
85
+ *
86
+ * @param url - The URL to process
87
+ * @returns URL without /api suffix
88
+ */
89
+ function stripApiSuffix(url) {
90
+ return url.replace(/\/api\/?$/, "");
91
+ }
92
+
93
+ /**
94
+ * Makes an HTTP request with timeout support.
95
+ *
96
+ * @param url - Request URL
97
+ * @param options - Request options
98
+ * @returns Response object
99
+ * @throws {Error} If request fails or times out
100
+ */
101
+ async function makeHttpRequest(url, options = {}) {
102
+ const { timeout = 30000, signal, ...fetchOptions } = options;
103
+ const controller = new AbortController();
104
+ const timeoutId = timeout > 0 ? setTimeout(() => controller.abort(), timeout) : null;
105
+ try {
106
+ const response = await fetch(url, {
107
+ ...fetchOptions,
108
+ signal: signal || controller.signal
109
+ });
110
+ if (timeoutId)
111
+ clearTimeout(timeoutId);
112
+ return response;
113
+ }
114
+ catch (error) {
115
+ if (timeoutId)
116
+ clearTimeout(timeoutId);
117
+ throw error;
118
+ }
119
+ }
120
+ /**
121
+ * Handles HTTP response errors by creating appropriate RailtownError.
122
+ *
123
+ * @param response - HTTP response object
124
+ * @param defaultMessage - Default error message
125
+ * @returns Never (always throws)
126
+ * @throws {RailtownError} Appropriate error based on status code
127
+ */
128
+ async function handleHttpError(response, defaultMessage) {
129
+ const responseBody = await getResponseBody(response);
130
+ throw createRailtownError(response.status, defaultMessage, responseBody);
131
+ }
132
+ /**
133
+ * Safely extracts response body as text, returning empty string on error.
134
+ *
135
+ * @param response - HTTP response object
136
+ * @returns Response body as string (empty string if extraction fails)
137
+ */
138
+ async function getResponseBody(response) {
139
+ return await response.text().catch(() => "");
140
+ }
141
+ /**
142
+ * Handles request errors (timeout, network, etc.) with consistent error messages.
143
+ *
144
+ * @param error - Caught error
145
+ * @param timeout - Request timeout in milliseconds
146
+ * @param context - Context description for error message
147
+ * @returns Never (always throws)
148
+ * @throws {RailtownError} If error is a RailtownError
149
+ * @throws {Error} For timeout or other errors
150
+ */
151
+ function handleRequestError(error, timeout, context) {
152
+ if (error instanceof RailtownError) {
153
+ throw error;
154
+ }
155
+ if (error instanceof Error && error.name === "AbortError") {
156
+ throw new Error(`Request timeout after ${timeout}ms`);
157
+ }
158
+ throw new Error(`${context}: ${error instanceof Error ? error.message : String(error)}`);
159
+ }
160
+
161
+ /**
162
+ * Validates and normalizes RailEngine configuration.
163
+ *
164
+ * @param pat - PAT token from options or environment variable
165
+ * @param engineId - Engine ID from options or environment variable
166
+ * @param apiUrl - Base API URL from options or environment variable
167
+ * @returns Validated and normalized configuration
168
+ * @throws {RailtownBadRequestError} If PAT or engineId is missing
169
+ */
170
+ function validateRailEngineConfig(pat, engineId, apiUrl) {
171
+ // Get PAT from parameter or environment variable
172
+ const resolvedPat = pat ?? process.env.ENGINE_PAT;
173
+ if (!resolvedPat || typeof resolvedPat !== "string" || resolvedPat.trim().length === 0) {
174
+ throw new RailtownBadRequestError("PAT is required. Provide it via constructor parameter (pat) or ENGINE_PAT environment variable.");
175
+ }
176
+ // Get engineId from parameter or environment variable
177
+ const resolvedEngineId = engineId ?? process.env.ENGINE_ID;
178
+ if (!resolvedEngineId || typeof resolvedEngineId !== "string" || resolvedEngineId.trim().length === 0) {
179
+ throw new RailtownBadRequestError("Engine ID is required. Provide it via constructor parameter (engineId) or ENGINE_ID environment variable.");
180
+ }
181
+ // Get apiUrl from parameter or environment variable, default to production
182
+ const resolvedApiUrl = apiUrl ?? process.env.RAILTOWN_API_URL ?? "https://cndr.railtown.ai/api";
183
+ const trimmedApiUrl = resolvedApiUrl.trim();
184
+ // Normalize URL (strip /api suffix if present, it will be added back when constructing endpoints)
185
+ const normalizedApiUrl = stripApiSuffix(trimmedApiUrl);
186
+ return {
187
+ pat: resolvedPat,
188
+ engineId: resolvedEngineId,
189
+ apiUrl: trimmedApiUrl, // Original URL for display
190
+ normalizedApiUrl // Normalized URL for internal use
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Search a vector store using semantic similarity.
196
+ *
197
+ * @param client - RailEngine client instance
198
+ * @param engineId - Engine identifier
199
+ * @param vectorStore - Vector store name (e.g., "VectorStore1", "VectorStore2", "VectorStore3")
200
+ * @param query - Search query string
201
+ * @param options - Optional filter function and schema override
202
+ * @returns Async iterable of search results (validated against schema if provided)
203
+ */
204
+ async function* searchVectorStore(client, engineId, vectorStore, query, options) {
205
+ try {
206
+ // Determine which schema to use (override or default)
207
+ const schema = options?.schema ?? client._getSchema();
208
+ // Make POST request to search endpoint
209
+ const response = await client._makeRequest("POST", `/Engine/${engineId}/Embeddings/Search`, {
210
+ body: {
211
+ VectorStore: vectorStore,
212
+ Query: query
213
+ }
214
+ });
215
+ // Parse response
216
+ const data = await response.json().catch(() => null);
217
+ if (!data) {
218
+ return;
219
+ }
220
+ // Handle array of results
221
+ const dataRecord = data;
222
+ const results = Array.isArray(data)
223
+ ? data
224
+ : (Array.isArray(dataRecord.results)
225
+ ? dataRecord.results
226
+ : Array.isArray(dataRecord.items)
227
+ ? dataRecord.items
228
+ : []);
229
+ // Process each result
230
+ for (const item of results) {
231
+ if (!item || typeof item !== "object") {
232
+ continue;
233
+ }
234
+ let validatedItem = item;
235
+ // Validate against schema if provided
236
+ if (schema) {
237
+ try {
238
+ validatedItem = schema.parse(item);
239
+ }
240
+ catch {
241
+ // Skip invalid items
242
+ continue;
243
+ }
244
+ }
245
+ // Apply filter if provided
246
+ if (options?.filterFn && !options.filterFn(validatedItem)) {
247
+ continue;
248
+ }
249
+ yield validatedItem;
250
+ }
251
+ }
252
+ catch {
253
+ // Return empty iterable on error (graceful degradation)
254
+ return;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Collects all items from an async iterable into an array.
260
+ *
261
+ * @param iterable - Async iterable to collect
262
+ * @returns Promise that resolves to an array of all items
263
+ */
264
+ /**
265
+ * Extracts and deserializes the Body field from a storage document.
266
+ * The Body field is a JSON string that contains the actual document data.
267
+ *
268
+ * @param document - Storage document with Body field
269
+ * @param schema - Optional Zod schema to validate the deserialized body
270
+ * @returns Deserialized body object or null if extraction/validation fails
271
+ */
272
+ function extractAndDeserializeBody(document, schema) {
273
+ // Extract Body or content field (JSON string)
274
+ // API may return either "Body" or "content" field
275
+ const bodyString = (document.Body || document.content);
276
+ if (!bodyString || typeof bodyString !== "string") {
277
+ return null;
278
+ }
279
+ // Parse JSON string
280
+ let bodyObject;
281
+ try {
282
+ bodyObject = JSON.parse(bodyString);
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ // Validate against schema if provided
288
+ if (schema) {
289
+ try {
290
+ return schema.parse(bodyObject);
291
+ }
292
+ catch {
293
+ return null;
294
+ }
295
+ }
296
+ return bodyObject;
297
+ }
298
+ /**
299
+ * Get a storage document by EventId.
300
+ *
301
+ * @param client - RailEngine client instance
302
+ * @param engineId - Engine identifier
303
+ * @param eventId - Event identifier
304
+ * @param options - Optional filter function and schema override
305
+ * @returns Document or null if not found or filtered out
306
+ */
307
+ async function getStorageDocumentByEventId(client, engineId, eventId, options) {
308
+ try {
309
+ // Make GET request
310
+ const response = await client._makeRequest("GET", `/api/Engine/${engineId}/Storage`, {
311
+ queryParams: {
312
+ EventId: eventId
313
+ }
314
+ });
315
+ // Parse response
316
+ const data = await response.json().catch(() => null);
317
+ if (!data) {
318
+ return null;
319
+ }
320
+ // Handle single document or array
321
+ const document = Array.isArray(data) ? data[0] : data;
322
+ if (!document || typeof document !== "object") {
323
+ return null;
324
+ }
325
+ // If raw mode, return full wrapper object
326
+ if (options?.raw) {
327
+ const schema = options?.schema ?? client._getSchema();
328
+ let validatedDocument = document;
329
+ // Validate against schema if provided
330
+ if (schema) {
331
+ try {
332
+ validatedDocument = schema.parse(document);
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ }
338
+ // Apply filter if provided
339
+ if (options?.filterFn && !options.filterFn(validatedDocument)) {
340
+ return null;
341
+ }
342
+ return validatedDocument;
343
+ }
344
+ // Default behavior: Extract and deserialize Body field
345
+ const schema = options?.schema ?? client._getSchema();
346
+ const deserializedBody = extractAndDeserializeBody(document, schema);
347
+ if (deserializedBody === null) {
348
+ return null;
349
+ }
350
+ // Apply filter if provided
351
+ if (options?.filterFn && !options.filterFn(deserializedBody)) {
352
+ return null;
353
+ }
354
+ return deserializedBody;
355
+ }
356
+ catch {
357
+ // Return null on error (graceful degradation)
358
+ return null;
359
+ }
360
+ }
361
+ /**
362
+ * Get storage documents by CustomerKey.
363
+ *
364
+ * @param client - RailEngine client instance
365
+ * @param engineId - Engine identifier
366
+ * @param customerKey - Customer key
367
+ * @param options - Optional pagination, filter function, and schema override
368
+ * @returns Async iterable of documents (paginated results automatically flattened and filtered)
369
+ */
370
+ async function* getStorageDocumentByCustomerKey(client, engineId, customerKey, options) {
371
+ const pageNumber = options?.pageNumber ?? 1;
372
+ const pageSize = Math.min(options?.pageSize ?? 25, 100); // Max 100
373
+ const schema = options?.schema ?? client._getSchema();
374
+ let currentPage = pageNumber;
375
+ let hasMorePages = true;
376
+ while (hasMorePages) {
377
+ try {
378
+ // Make GET request for current page
379
+ const response = await client._makeRequest("GET", `/api/Engine/${engineId}/Storage`, {
380
+ queryParams: {
381
+ CustomerKey: customerKey,
382
+ PageNumber: currentPage.toString(),
383
+ PageSize: pageSize.toString()
384
+ }
385
+ });
386
+ // Parse response
387
+ const data = await response.json().catch(() => null);
388
+ if (!data) {
389
+ break;
390
+ }
391
+ // Handle array of documents or paginated response
392
+ const dataRecord = data;
393
+ const documents = Array.isArray(data)
394
+ ? data
395
+ : (Array.isArray(dataRecord.results)
396
+ ? dataRecord.results
397
+ : Array.isArray(dataRecord.items)
398
+ ? dataRecord.items
399
+ : []);
400
+ const totalPages = (typeof dataRecord.totalPages === "number" ? dataRecord.totalPages : undefined) ||
401
+ (typeof dataRecord.pageCount === "number" ? dataRecord.pageCount : undefined) ||
402
+ 1;
403
+ // Process each document
404
+ for (const item of documents) {
405
+ if (!item || typeof item !== "object") {
406
+ continue;
407
+ }
408
+ // If raw mode, yield full wrapper object
409
+ if (options?.raw) {
410
+ // In raw mode, skip schema validation since the wrapper structure differs from Body content
411
+ // The schema is meant for deserialized Body, not for the raw wrapper object
412
+ let validatedItem = item;
413
+ // Apply filter if provided
414
+ if (options?.filterFn && !options.filterFn(validatedItem)) {
415
+ continue;
416
+ }
417
+ yield validatedItem;
418
+ }
419
+ else {
420
+ // Default behavior: Extract and deserialize Body field
421
+ const deserializedBody = extractAndDeserializeBody(item, schema);
422
+ if (deserializedBody === null) {
423
+ continue;
424
+ }
425
+ // Apply filter if provided
426
+ if (options?.filterFn && !options.filterFn(deserializedBody)) {
427
+ continue;
428
+ }
429
+ yield deserializedBody;
430
+ }
431
+ }
432
+ // Check if there are more pages
433
+ hasMorePages = currentPage < totalPages && documents.length === pageSize;
434
+ currentPage++;
435
+ }
436
+ catch {
437
+ // Stop pagination on error
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ /**
443
+ * Query storage documents using JSONPath.
444
+ *
445
+ * @param client - RailEngine client instance
446
+ * @param engineId - Engine identifier
447
+ * @param jsonPathQuery - JSONPath query string (e.g., "$.direct_report_id:1")
448
+ * @param options - Optional filter function and schema override
449
+ * @returns Async iterable of matching documents
450
+ */
451
+ async function* queryStorageByJsonPath(client, engineId, jsonPathQuery, options) {
452
+ try {
453
+ const schema = options?.schema ?? client._getSchema();
454
+ // Make GET request
455
+ const response = await client._makeRequest("GET", `/api/Engine/${engineId}/Storage`, {
456
+ queryParams: {
457
+ JsonPathQuery: jsonPathQuery
458
+ }
459
+ });
460
+ // Parse response
461
+ const data = await response.json().catch(() => null);
462
+ if (!data) {
463
+ return;
464
+ }
465
+ // Handle array of documents or paginated response
466
+ const dataRecord = data;
467
+ const documents = Array.isArray(data)
468
+ ? data
469
+ : (Array.isArray(dataRecord.results)
470
+ ? dataRecord.results
471
+ : Array.isArray(dataRecord.items)
472
+ ? dataRecord.items
473
+ : []);
474
+ // Process each document
475
+ for (const item of documents) {
476
+ if (!item || typeof item !== "object") {
477
+ continue;
478
+ }
479
+ // If raw mode, yield full wrapper object
480
+ if (options?.raw) {
481
+ // In raw mode, skip schema validation since the wrapper structure differs from Body content
482
+ // The schema is meant for deserialized Body, not for the raw wrapper object
483
+ let validatedItem = item;
484
+ // Apply filter if provided
485
+ if (options?.filterFn && !options.filterFn(validatedItem)) {
486
+ continue;
487
+ }
488
+ yield validatedItem;
489
+ }
490
+ else {
491
+ // Default behavior: Extract and deserialize Body field
492
+ const deserializedBody = extractAndDeserializeBody(item, schema);
493
+ if (deserializedBody === null) {
494
+ continue;
495
+ }
496
+ // Apply filter if provided
497
+ if (options?.filterFn && !options.filterFn(deserializedBody)) {
498
+ continue;
499
+ }
500
+ yield deserializedBody;
501
+ }
502
+ }
503
+ }
504
+ catch {
505
+ // Return empty iterable on error (graceful degradation)
506
+ return;
507
+ }
508
+ }
509
+ /**
510
+ * List storage documents.
511
+ *
512
+ * @param client - RailEngine client instance
513
+ * @param engineId - Engine identifier
514
+ * @param options - Optional pagination, customer key filter, filter function, and schema override
515
+ * @returns Async iterable of documents (paginated results automatically flattened and filtered)
516
+ */
517
+ async function* listStorageDocuments(client, engineId, options) {
518
+ const pageNumber = options?.pageNumber ?? 1;
519
+ const pageSize = Math.min(options?.pageSize ?? 100, 100); // Max 100
520
+ const schema = options?.schema ?? client._getSchema();
521
+ let currentPage = pageNumber;
522
+ let hasMorePages = true;
523
+ while (hasMorePages) {
524
+ try {
525
+ // Build query parameters
526
+ const queryParams = {
527
+ PageNumber: currentPage.toString(),
528
+ PageSize: pageSize.toString()
529
+ };
530
+ if (options?.customerKey) {
531
+ queryParams.CustomerKey = options.customerKey;
532
+ }
533
+ // Make GET request for current page
534
+ const response = await client._makeRequest("GET", `/api/Engine/${engineId}/Storage`, {
535
+ queryParams
536
+ });
537
+ // Parse response
538
+ const data = await response.json().catch(() => null);
539
+ if (!data) {
540
+ break;
541
+ }
542
+ // Handle array of documents or paginated response
543
+ const dataRecord = data;
544
+ const documents = Array.isArray(data)
545
+ ? data
546
+ : (Array.isArray(dataRecord.results)
547
+ ? dataRecord.results
548
+ : Array.isArray(dataRecord.items)
549
+ ? dataRecord.items
550
+ : []);
551
+ const totalPages = (typeof dataRecord.totalPages === "number" ? dataRecord.totalPages : undefined) ||
552
+ (typeof dataRecord.pageCount === "number" ? dataRecord.pageCount : undefined) ||
553
+ 1;
554
+ // Process each document
555
+ for (const item of documents) {
556
+ if (!item || typeof item !== "object") {
557
+ continue;
558
+ }
559
+ // If raw mode, yield full wrapper object
560
+ if (options?.raw) {
561
+ // In raw mode, skip schema validation since the wrapper structure differs from Body content
562
+ // The schema is meant for deserialized Body, not for the raw wrapper object
563
+ let validatedItem = item;
564
+ // Apply filter if provided
565
+ if (options?.filterFn && !options.filterFn(validatedItem)) {
566
+ continue;
567
+ }
568
+ yield validatedItem;
569
+ }
570
+ else {
571
+ // Default behavior: Extract and deserialize Body field
572
+ const deserializedBody = extractAndDeserializeBody(item, schema);
573
+ if (deserializedBody === null) {
574
+ continue;
575
+ }
576
+ // Apply filter if provided
577
+ if (options?.filterFn && !options.filterFn(deserializedBody)) {
578
+ continue;
579
+ }
580
+ yield deserializedBody;
581
+ }
582
+ }
583
+ // Check if there are more pages
584
+ hasMorePages = currentPage < totalPages && documents.length === pageSize;
585
+ currentPage++;
586
+ }
587
+ catch {
588
+ // Stop pagination on error
589
+ break;
590
+ }
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Search index using Azure Search.
596
+ *
597
+ * @param client - RailEngine client instance
598
+ * @param projectId - Project identifier
599
+ * @param engineId - Engine identifier
600
+ * @param query - Search query object (e.g., { search: "query" })
601
+ * @param options - Optional filter function and schema override
602
+ * @returns Async iterable of search results (validated against schema if provided)
603
+ */
604
+ async function* searchIndex(client, projectId, engineId, query, options) {
605
+ try {
606
+ // Determine which schema to use (override or default)
607
+ const schema = options?.schema ?? client._getSchema();
608
+ // Make POST request to search endpoint
609
+ const response = await client._makeRequest("POST", `/Engine/Indexing/Search`, {
610
+ body: {
611
+ ProjectId: projectId,
612
+ EngineId: engineId,
613
+ ...query
614
+ }
615
+ });
616
+ // Parse response
617
+ const data = await response.json().catch(() => null);
618
+ if (!data) {
619
+ return;
620
+ }
621
+ // Handle array of results or paginated response
622
+ const dataRecord = data;
623
+ const results = Array.isArray(data)
624
+ ? data
625
+ : (Array.isArray(dataRecord.results)
626
+ ? dataRecord.results
627
+ : Array.isArray(dataRecord.items)
628
+ ? dataRecord.items
629
+ : Array.isArray(dataRecord.value)
630
+ ? dataRecord.value
631
+ : []);
632
+ // Process each result
633
+ for (const item of results) {
634
+ if (!item || typeof item !== "object") {
635
+ continue;
636
+ }
637
+ let validatedItem = item;
638
+ // Validate against schema if provided
639
+ if (schema) {
640
+ try {
641
+ validatedItem = schema.parse(item);
642
+ }
643
+ catch {
644
+ // Skip invalid items
645
+ continue;
646
+ }
647
+ }
648
+ // Apply filter if provided
649
+ if (options?.filterFn && !options.filterFn(validatedItem)) {
650
+ continue;
651
+ }
652
+ yield validatedItem;
653
+ }
654
+ }
655
+ catch {
656
+ // Return empty iterable on error (graceful degradation)
657
+ return;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Client for retrieving and searching data from Rail Engine.
663
+ */
664
+ class RailEngine {
665
+ /**
666
+ * Initialize the Rail Engine retrieval client.
667
+ *
668
+ * @param options - Configuration options
669
+ * @throws {RailtownBadRequestError} If PAT or engineId is missing
670
+ */
671
+ constructor(options = {}) {
672
+ const config = validateRailEngineConfig(options.pat, options.engineId, options.apiUrl);
673
+ this.pat = config.pat;
674
+ this.engineId = config.engineId;
675
+ this.apiUrl = config.apiUrl; // Original URL for display
676
+ this.normalizedApiUrl = config.normalizedApiUrl; // Normalized URL for internal use
677
+ this.schema = options.schema;
678
+ this.timeout = options.timeout ?? 30000;
679
+ }
680
+ /**
681
+ * Makes an authenticated HTTP request.
682
+ *
683
+ * @param method - HTTP method
684
+ * @param path - API path (will be appended to base URL)
685
+ * @param options - Request options
686
+ * @returns Response object
687
+ * @throws {RailtownError} If the request fails
688
+ */
689
+ async _request(method, path, options = {}) {
690
+ // Construct URL using normalized base URL
691
+ const url = new URL(`${this.normalizedApiUrl}${path}`);
692
+ if (options.queryParams) {
693
+ for (const [key, value] of Object.entries(options.queryParams)) {
694
+ url.searchParams.append(key, value);
695
+ }
696
+ }
697
+ // Prepare request body
698
+ let body;
699
+ if (options.body !== undefined) {
700
+ body = JSON.stringify(options.body);
701
+ }
702
+ // Make request
703
+ try {
704
+ const response = await makeHttpRequest(url.toString(), {
705
+ method,
706
+ headers: {
707
+ "Content-Type": "application/json",
708
+ Authorization: this.pat
709
+ },
710
+ body,
711
+ timeout: this.timeout
712
+ });
713
+ // Handle errors (but don't throw - let calling methods handle gracefully)
714
+ if (!response.ok) {
715
+ await handleHttpError(response, `Request failed: ${response.statusText}`);
716
+ }
717
+ return response;
718
+ }
719
+ catch (error) {
720
+ return handleRequestError(error, this.timeout, "Request failed");
721
+ }
722
+ }
723
+ /**
724
+ * Search a vector store using semantic similarity.
725
+ *
726
+ * @param engineId - Engine identifier
727
+ * @param vectorStore - Vector store name (e.g., "VectorStore1")
728
+ * @param query - Search query string
729
+ * @param options - Optional filter function and schema override
730
+ * @returns Async iterable of search results
731
+ */
732
+ searchVectorStore(engineId, vectorStore, query, options) {
733
+ return searchVectorStore(this, engineId, vectorStore, query, options);
734
+ }
735
+ /**
736
+ * Get a storage document by EventId.
737
+ *
738
+ * @param params - Parameters object
739
+ * @param params.eventId - Event identifier
740
+ * @param params.engineId - Optional engine identifier (defaults to client.engineId)
741
+ * @param params.filterFn - Optional filter function
742
+ * @param params.schema - Optional schema override
743
+ * @param params.raw - If true, returns full wrapper object with metadata instead of deserialized Body
744
+ * @returns Document or null if not found. When a schema is provided (via constructor or options), returns T | null.
745
+ * When no schema is provided, returns Record<string, unknown> | null.
746
+ */
747
+ async getStorageDocumentByEventId(params) {
748
+ const engineId = params.engineId ?? this.engineId;
749
+ return getStorageDocumentByEventId(this, engineId, params.eventId, {
750
+ filterFn: params.filterFn,
751
+ schema: params.schema,
752
+ raw: params.raw
753
+ });
754
+ }
755
+ /**
756
+ * Get storage documents by CustomerKey.
757
+ *
758
+ * @param params - Parameters object
759
+ * @param params.customerKey - Customer key
760
+ * @param params.engineId - Optional engine identifier (defaults to client.engineId)
761
+ * @param params.pageNumber - Optional page number (defaults to 1)
762
+ * @param params.pageSize - Optional page size (defaults to 25, max 100)
763
+ * @param params.filterFn - Optional filter function
764
+ * @param params.schema - Optional schema override
765
+ * @param params.raw - If true, returns full wrapper object with metadata instead of deserialized Body
766
+ * @returns Promise that resolves to an array of documents. Pagination is handled automatically.
767
+ * When a schema is provided (via constructor or options), returns Promise<T[]>.
768
+ */
769
+ async getStorageDocumentByCustomerKey(params) {
770
+ const engineId = params.engineId ?? this.engineId;
771
+ const iterable = getStorageDocumentByCustomerKey(this, engineId, params.customerKey, {
772
+ pageNumber: params.pageNumber,
773
+ pageSize: params.pageSize,
774
+ filterFn: params.filterFn,
775
+ schema: params.schema,
776
+ raw: params.raw
777
+ });
778
+ const results = [];
779
+ for await (const item of iterable) {
780
+ results.push(item);
781
+ }
782
+ return results;
783
+ }
784
+ /**
785
+ * Query storage documents using JSONPath.
786
+ *
787
+ * @param params - Parameters object
788
+ * @param params.jsonPathQuery - JSONPath query string (e.g., "$.direct_report_id:1")
789
+ * @param params.engineId - Optional engine identifier (defaults to client.engineId)
790
+ * @param params.filterFn - Optional filter function
791
+ * @param params.schema - Optional schema override
792
+ * @param params.raw - If true, returns full wrapper object with metadata instead of deserialized Body
793
+ * @returns Promise that resolves to an array of matching documents.
794
+ * When a schema is provided (via constructor or options), returns Promise<T[]>.
795
+ */
796
+ async queryStorageByJsonPath(params) {
797
+ const engineId = params.engineId ?? this.engineId;
798
+ const iterable = queryStorageByJsonPath(this, engineId, params.jsonPathQuery, {
799
+ filterFn: params.filterFn,
800
+ schema: params.schema,
801
+ raw: params.raw
802
+ });
803
+ const results = [];
804
+ for await (const item of iterable) {
805
+ results.push(item);
806
+ }
807
+ return results;
808
+ }
809
+ /**
810
+ * List storage documents.
811
+ *
812
+ * @param params - Parameters object
813
+ * @param params.engineId - Optional engine identifier (defaults to client.engineId)
814
+ * @param params.customerKey - Optional customer key filter
815
+ * @param params.pageNumber - Optional page number (defaults to 1)
816
+ * @param params.pageSize - Optional page size (defaults to 100, max 100)
817
+ * @param params.filterFn - Optional filter function
818
+ * @param params.schema - Optional schema override
819
+ * @param params.raw - If true, returns full wrapper object with metadata instead of deserialized Body
820
+ * @returns Promise that resolves to an array of documents. Pagination is handled automatically.
821
+ * When a schema is provided (via constructor or options), returns Promise<T[]>.
822
+ */
823
+ async listStorageDocuments(params) {
824
+ const engineId = params?.engineId ?? this.engineId;
825
+ const iterable = listStorageDocuments(this, engineId, {
826
+ customerKey: params?.customerKey,
827
+ pageNumber: params?.pageNumber,
828
+ pageSize: params?.pageSize,
829
+ filterFn: params?.filterFn,
830
+ schema: params?.schema,
831
+ raw: params?.raw
832
+ });
833
+ const results = [];
834
+ for await (const item of iterable) {
835
+ results.push(item);
836
+ }
837
+ return results;
838
+ }
839
+ /**
840
+ * Search index using Azure Search.
841
+ *
842
+ * @param projectId - Project identifier
843
+ * @param engineId - Engine identifier
844
+ * @param query - Search query object (e.g., { search: "query" })
845
+ * @param options - Optional filter function and schema override
846
+ * @returns Async iterable of search results
847
+ */
848
+ searchIndex(projectId, engineId, query, options) {
849
+ return searchIndex(this, projectId, engineId, query, options);
850
+ }
851
+ /**
852
+ * Delete an event from the engine.
853
+ *
854
+ * @param params - Parameters object
855
+ * @param params.eventId - Event identifier to delete
856
+ * @param params.engineId - Optional engine identifier (defaults to client.engineId)
857
+ * @returns Promise that resolves when deletion is successful or accepted
858
+ * @throws {RailtownNotFoundError} If engine is not found (404)
859
+ * @throws {RailtownServerError} If service is unavailable (503)
860
+ * @throws {RailtownUnauthorizedError} If authentication fails (401)
861
+ *
862
+ * The operation is idempotent - calling deleteEvent multiple times with the same eventId
863
+ * will succeed. The API returns:
864
+ * - 204: Event deleted immediately or already deleted
865
+ * - 202: Deletion accepted, may take time (eventually consistent)
866
+ */
867
+ async deleteEvent(params) {
868
+ const engineId = params.engineId ?? this.engineId;
869
+ await this._makeRequest("DELETE", `/api/Engine/${engineId}/Event/${params.eventId}`);
870
+ }
871
+ /**
872
+ * Internal method to make authenticated requests (used by API modules).
873
+ *
874
+ * @internal
875
+ */
876
+ async _makeRequest(method, path, options = {}) {
877
+ return this._request(method, path, options);
878
+ }
879
+ /**
880
+ * Get the default schema for this client.
881
+ *
882
+ * @internal
883
+ */
884
+ _getSchema() {
885
+ return this.schema;
886
+ }
887
+ }
888
+
889
+ export { RailEngine };
890
+ //# sourceMappingURL=index.js.map