s4kit 0.0.1 → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1600 @@
1
+ // src/http-client.ts
2
+ import ky from "ky";
3
+
4
+ // src/errors.ts
5
+ var S4KitError = class extends Error {
6
+ status;
7
+ code;
8
+ odataError;
9
+ request;
10
+ suggestion;
11
+ constructor(message, options) {
12
+ super(message, { cause: options?.cause });
13
+ this.name = "S4KitError";
14
+ this.status = options?.status;
15
+ this.code = options?.code;
16
+ this.odataError = options?.odataError;
17
+ this.request = options?.request;
18
+ this.suggestion = options?.suggestion;
19
+ }
20
+ /**
21
+ * Get a user-friendly error message with context
22
+ */
23
+ get friendlyMessage() {
24
+ const parts = [];
25
+ if (this.request?.url) {
26
+ const entity = this.extractEntityFromUrl(this.request.url);
27
+ if (entity) {
28
+ parts.push(`[${entity}]`);
29
+ }
30
+ }
31
+ parts.push(this.odataError?.message ?? this.message);
32
+ return parts.join(" ");
33
+ }
34
+ /**
35
+ * Get actionable help text
36
+ */
37
+ get help() {
38
+ if (this.suggestion) return this.suggestion;
39
+ return getSuggestionForError(this);
40
+ }
41
+ extractEntityFromUrl(url) {
42
+ const match = url.match(/\/([A-Z][A-Za-z_]+)(?:\(|$|\?)/);
43
+ return match?.[1] ?? null;
44
+ }
45
+ /**
46
+ * Get all error details (for validation errors with multiple issues)
47
+ */
48
+ get details() {
49
+ return this.odataError?.details ?? [];
50
+ }
51
+ /**
52
+ * Check if this is a specific OData error code
53
+ */
54
+ hasCode(code) {
55
+ return this.code === code || this.odataError?.code === code;
56
+ }
57
+ /**
58
+ * Convert to a plain object for logging
59
+ */
60
+ toJSON() {
61
+ return {
62
+ name: this.name,
63
+ message: this.message,
64
+ status: this.status,
65
+ code: this.code,
66
+ odataError: this.odataError,
67
+ request: this.request ? {
68
+ method: this.request.method,
69
+ url: this.request.url
70
+ } : void 0
71
+ };
72
+ }
73
+ };
74
+ var NetworkError = class extends S4KitError {
75
+ constructor(message, options) {
76
+ super(message, { ...options, code: "NETWORK_ERROR" });
77
+ this.name = "NetworkError";
78
+ }
79
+ };
80
+ var TimeoutError = class extends S4KitError {
81
+ constructor(timeout, options) {
82
+ super(`Request timed out after ${timeout}ms`, { ...options, code: "TIMEOUT" });
83
+ this.name = "TimeoutError";
84
+ }
85
+ };
86
+ var AuthenticationError = class extends S4KitError {
87
+ constructor(message = "Authentication failed", options) {
88
+ super(message, { ...options, status: 401, code: "UNAUTHORIZED" });
89
+ this.name = "AuthenticationError";
90
+ }
91
+ };
92
+ var AuthorizationError = class extends S4KitError {
93
+ constructor(message = "Access denied", options) {
94
+ super(message, { ...options, status: 403, code: "FORBIDDEN" });
95
+ this.name = "AuthorizationError";
96
+ }
97
+ };
98
+ var NotFoundError = class extends S4KitError {
99
+ constructor(entity, id, options) {
100
+ const message = entity && id ? `${entity}(${id}) not found` : entity ? `${entity} not found` : "Resource not found";
101
+ super(message, { ...options, status: 404, code: "NOT_FOUND" });
102
+ this.name = "NotFoundError";
103
+ }
104
+ };
105
+ var ValidationError = class extends S4KitError {
106
+ fieldErrors;
107
+ constructor(message, options) {
108
+ super(message, { ...options, status: 400, code: "VALIDATION_ERROR" });
109
+ this.name = "ValidationError";
110
+ this.fieldErrors = /* @__PURE__ */ new Map();
111
+ if (options?.odataError?.details) {
112
+ for (const detail of options.odataError.details) {
113
+ if (detail.target) {
114
+ this.fieldErrors.set(detail.target, detail.message);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Get error for a specific field
121
+ */
122
+ getFieldError(field) {
123
+ return this.fieldErrors.get(field);
124
+ }
125
+ /**
126
+ * Check if a specific field has an error
127
+ */
128
+ hasFieldError(field) {
129
+ return this.fieldErrors.has(field);
130
+ }
131
+ };
132
+ var ConflictError = class extends S4KitError {
133
+ constructor(message = "Resource conflict", options) {
134
+ super(message, { ...options, status: 409, code: "CONFLICT" });
135
+ this.name = "ConflictError";
136
+ }
137
+ };
138
+ var RateLimitError = class extends S4KitError {
139
+ retryAfter;
140
+ constructor(retryAfter, options) {
141
+ super(
142
+ retryAfter ? `Rate limit exceeded. Retry after ${retryAfter} seconds` : "Rate limit exceeded",
143
+ { ...options, status: 429, code: "RATE_LIMIT_EXCEEDED" }
144
+ );
145
+ this.name = "RateLimitError";
146
+ this.retryAfter = retryAfter;
147
+ }
148
+ };
149
+ var ServerError = class extends S4KitError {
150
+ constructor(message = "Internal server error", status = 500, options) {
151
+ super(message, { ...options, status, code: "SERVER_ERROR" });
152
+ this.name = "ServerError";
153
+ }
154
+ };
155
+ var BatchError = class extends S4KitError {
156
+ results;
157
+ constructor(message, results) {
158
+ super(message, { code: "BATCH_ERROR" });
159
+ this.name = "BatchError";
160
+ this.results = results;
161
+ }
162
+ /**
163
+ * Get all failed operations
164
+ */
165
+ get failures() {
166
+ return this.results.filter((r) => !r.success);
167
+ }
168
+ /**
169
+ * Get all successful operations
170
+ */
171
+ get successes() {
172
+ return this.results.filter((r) => r.success);
173
+ }
174
+ };
175
+ function parseHttpError(status, body, request) {
176
+ const odataError = parseODataError(body);
177
+ const message = odataError?.message ?? getDefaultMessage(status);
178
+ switch (status) {
179
+ case 400:
180
+ return new ValidationError(message, { odataError, request });
181
+ case 401:
182
+ return new AuthenticationError(message, { odataError, request });
183
+ case 403:
184
+ return new AuthorizationError(message, { odataError, request });
185
+ case 404:
186
+ return new NotFoundError(void 0, void 0, { odataError, request });
187
+ case 409:
188
+ return new ConflictError(message, { odataError, request });
189
+ case 429:
190
+ return new RateLimitError(void 0, { odataError, request });
191
+ default:
192
+ if (status >= 500) {
193
+ return new ServerError(message, status, { odataError, request });
194
+ }
195
+ return new S4KitError(message, { status, odataError, request });
196
+ }
197
+ }
198
+ function parseODataError(body) {
199
+ if (!body) return void 0;
200
+ if (body.error?.message?.value) {
201
+ return {
202
+ code: body.error.code ?? "UNKNOWN",
203
+ message: body.error.message.value
204
+ };
205
+ }
206
+ if (body.error) {
207
+ return {
208
+ code: body.error.code ?? "UNKNOWN",
209
+ message: body.error.message ?? "Unknown error",
210
+ target: body.error.target,
211
+ details: body.error.details,
212
+ innererror: body.error.innererror
213
+ };
214
+ }
215
+ if (body["odata.error"]) {
216
+ return {
217
+ code: body["odata.error"].code ?? "UNKNOWN",
218
+ message: body["odata.error"].message?.value ?? "Unknown error"
219
+ };
220
+ }
221
+ return void 0;
222
+ }
223
+ function getDefaultMessage(status) {
224
+ const messages = {
225
+ 400: "Bad request",
226
+ 401: "Authentication required",
227
+ 403: "Access denied",
228
+ 404: "Resource not found",
229
+ 405: "Method not allowed",
230
+ 409: "Resource conflict",
231
+ 429: "Too many requests",
232
+ 500: "Internal server error",
233
+ 502: "Bad gateway",
234
+ 503: "Service unavailable",
235
+ 504: "Gateway timeout"
236
+ };
237
+ return messages[status] ?? `HTTP error ${status}`;
238
+ }
239
+ function isRetryable(error) {
240
+ if (error instanceof NetworkError) return true;
241
+ if (error instanceof TimeoutError) return true;
242
+ if (error instanceof RateLimitError) return true;
243
+ if (error instanceof ServerError && error.status !== 501) return true;
244
+ return false;
245
+ }
246
+ function isS4KitError(error) {
247
+ return error instanceof S4KitError;
248
+ }
249
+ function getSuggestionForError(error) {
250
+ if (error instanceof AuthenticationError) {
251
+ return "Check that your API key is valid and not expired. Get a new key from the S4Kit dashboard.";
252
+ }
253
+ if (error instanceof AuthorizationError) {
254
+ return "Your API key may not have permission for this operation. Check your key permissions in the dashboard.";
255
+ }
256
+ if (error instanceof NotFoundError) {
257
+ const entity = error.request?.url?.match(/\/([A-Z][A-Za-z_]+)/)?.[1];
258
+ if (entity) {
259
+ return `Verify that "${entity}" exists and the ID is correct. Use .list() to see available entities.`;
260
+ }
261
+ return "The requested resource was not found. Verify the entity name and ID.";
262
+ }
263
+ if (error instanceof ValidationError) {
264
+ if (error.fieldErrors.size > 0) {
265
+ const fields = Array.from(error.fieldErrors.keys()).join(", ");
266
+ return `Fix the following fields: ${fields}`;
267
+ }
268
+ return "Check the request data for missing or invalid fields.";
269
+ }
270
+ if (error instanceof RateLimitError) {
271
+ if (error.retryAfter) {
272
+ return `Wait ${error.retryAfter} seconds before retrying. Consider implementing request throttling.`;
273
+ }
274
+ return "You have exceeded the rate limit. Implement exponential backoff or reduce request frequency.";
275
+ }
276
+ if (error instanceof NetworkError) {
277
+ return "Check your network connection. If the problem persists, the S4Kit API may be temporarily unavailable.";
278
+ }
279
+ if (error instanceof TimeoutError) {
280
+ return "The request timed out. Try increasing the timeout or reducing the amount of data requested.";
281
+ }
282
+ if (error instanceof ServerError) {
283
+ return "This is a server-side issue. If it persists, contact S4Kit support with the error details.";
284
+ }
285
+ return "See the error details for more information.";
286
+ }
287
+
288
+ // src/http-client.ts
289
+ function generateUUID() {
290
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
291
+ const r = Math.random() * 16 | 0;
292
+ const v = c === "x" ? r : r & 3 | 8;
293
+ return v.toString(16);
294
+ });
295
+ }
296
+ var HttpClient = class {
297
+ client;
298
+ config;
299
+ requestInterceptors = [];
300
+ responseInterceptors = [];
301
+ errorInterceptors = [];
302
+ debug;
303
+ constructor(config) {
304
+ this.config = config;
305
+ this.debug = config.debug ?? false;
306
+ if (config.onRequest) this.requestInterceptors = [...config.onRequest];
307
+ if (config.onResponse) this.responseInterceptors = [...config.onResponse];
308
+ if (config.onError) this.errorInterceptors = [...config.onError];
309
+ this.client = ky.create({
310
+ prefixUrl: config.baseUrl || "https://api.s4kit.com/api/proxy",
311
+ timeout: config.timeout ?? 3e4,
312
+ retry: config.retries ?? 0,
313
+ headers: {
314
+ "Authorization": `Bearer ${config.apiKey}`,
315
+ "Content-Type": "application/json"
316
+ },
317
+ hooks: {
318
+ beforeError: [
319
+ async (error) => {
320
+ const request = {
321
+ url: error.request?.url ?? "",
322
+ method: error.request?.method ?? "UNKNOWN",
323
+ headers: Object.fromEntries(error.request?.headers?.entries() ?? [])
324
+ };
325
+ let s4kitError;
326
+ if (error.name === "TimeoutError") {
327
+ s4kitError = new TimeoutError(this.config.timeout ?? 3e4, { request });
328
+ } else if (error.response) {
329
+ const body = await error.response.json().catch(() => ({}));
330
+ s4kitError = parseHttpError(error.response.status, body, request);
331
+ } else {
332
+ s4kitError = new NetworkError(error.message, { cause: error, request });
333
+ }
334
+ let finalError = s4kitError;
335
+ for (const interceptor of this.errorInterceptors) {
336
+ const result = await interceptor(finalError);
337
+ if (result instanceof S4KitError) {
338
+ finalError = result;
339
+ }
340
+ }
341
+ throw finalError;
342
+ }
343
+ ]
344
+ }
345
+ });
346
+ }
347
+ // ==========================================================================
348
+ // Interceptor Management
349
+ // ==========================================================================
350
+ /**
351
+ * Add a request interceptor
352
+ * @example
353
+ * ```ts
354
+ * client.onRequest((req) => {
355
+ * console.log(`${req.method} ${req.url}`);
356
+ * return req;
357
+ * });
358
+ * ```
359
+ */
360
+ onRequest(interceptor) {
361
+ this.requestInterceptors.push(interceptor);
362
+ return this;
363
+ }
364
+ /**
365
+ * Add a response interceptor
366
+ * @example
367
+ * ```ts
368
+ * client.onResponse((res) => {
369
+ * console.log(`Response: ${res.status}`);
370
+ * return res;
371
+ * });
372
+ * ```
373
+ */
374
+ onResponse(interceptor) {
375
+ this.responseInterceptors.push(interceptor);
376
+ return this;
377
+ }
378
+ /**
379
+ * Add an error interceptor
380
+ * @example
381
+ * ```ts
382
+ * client.onError((err) => {
383
+ * if (err.status === 401) {
384
+ * // Refresh token logic
385
+ * }
386
+ * return err;
387
+ * });
388
+ * ```
389
+ */
390
+ onError(interceptor) {
391
+ this.errorInterceptors.push(interceptor);
392
+ return this;
393
+ }
394
+ // ==========================================================================
395
+ // Debug Logging
396
+ // ==========================================================================
397
+ log(message, data) {
398
+ if (!this.debug) return;
399
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
400
+ const prefix = `[s4kit ${timestamp}]`;
401
+ if (data !== void 0) {
402
+ console.log(prefix, message, data);
403
+ } else {
404
+ console.log(prefix, message);
405
+ }
406
+ }
407
+ // ==========================================================================
408
+ // Header Building
409
+ // ==========================================================================
410
+ buildHeaders(options) {
411
+ const headers = {};
412
+ const conn = options?.connection || this.config.connection;
413
+ const svc = options?.service || this.config.service;
414
+ if (conn) headers["X-S4Kit-Connection"] = conn;
415
+ if (svc) headers["X-S4Kit-Service"] = svc;
416
+ if (options?.raw) headers["X-S4Kit-Raw"] = "true";
417
+ if (options?.headers) {
418
+ Object.assign(headers, options.headers);
419
+ }
420
+ return headers;
421
+ }
422
+ // ==========================================================================
423
+ // Request Execution
424
+ // ==========================================================================
425
+ async executeRequest(method, path, options = {}, requestOptions) {
426
+ const startTime = Date.now();
427
+ const headers = this.buildHeaders(requestOptions);
428
+ let request = {
429
+ url: path,
430
+ method,
431
+ headers: { ...headers },
432
+ body: options.json,
433
+ searchParams: options.searchParams
434
+ };
435
+ for (const interceptor of this.requestInterceptors) {
436
+ request = await interceptor(request);
437
+ }
438
+ this.log(`\u2192 ${method} ${path}`, request.searchParams ? `?${new URLSearchParams(request.searchParams)}` : "");
439
+ if (request.body) {
440
+ this.log(" Body:", request.body);
441
+ }
442
+ const response = await this.client(request.url, {
443
+ method: request.method,
444
+ headers: request.headers,
445
+ json: request.body,
446
+ searchParams: request.searchParams
447
+ });
448
+ const duration = Date.now() - startTime;
449
+ const data = await response.json();
450
+ this.log(`\u2190 ${response.status} (${duration}ms)`);
451
+ if (this.debug && data) {
452
+ const preview = JSON.stringify(data).slice(0, 200);
453
+ this.log(" Data:", preview + (preview.length >= 200 ? "..." : ""));
454
+ }
455
+ let interceptedResponse = {
456
+ status: response.status,
457
+ headers: Object.fromEntries(response.headers.entries()),
458
+ data
459
+ };
460
+ for (const interceptor of this.responseInterceptors) {
461
+ interceptedResponse = await interceptor(interceptedResponse);
462
+ }
463
+ return interceptedResponse.data;
464
+ }
465
+ // ==========================================================================
466
+ // HTTP Methods
467
+ // ==========================================================================
468
+ async get(path, searchParams, options) {
469
+ return this.executeRequest("GET", path, { searchParams }, options);
470
+ }
471
+ async post(path, json, options) {
472
+ return this.executeRequest("POST", path, { json }, options);
473
+ }
474
+ async put(path, json, options) {
475
+ return this.executeRequest("PUT", path, { json }, options);
476
+ }
477
+ async patch(path, json, options) {
478
+ return this.executeRequest("PATCH", path, { json }, options);
479
+ }
480
+ async delete(path, options) {
481
+ await this.executeRequest("DELETE", path, {}, options);
482
+ }
483
+ // ==========================================================================
484
+ // Batch Operations
485
+ // ==========================================================================
486
+ /**
487
+ * Execute multiple operations in a single batch request
488
+ * @example
489
+ * ```ts
490
+ * const results = await client.batch([
491
+ * { method: 'GET', entity: 'Products', id: 1 },
492
+ * { method: 'POST', entity: 'Products', data: { Name: 'New' } },
493
+ * { method: 'DELETE', entity: 'Products', id: 2 },
494
+ * ]);
495
+ * ```
496
+ */
497
+ async batch(operations, options) {
498
+ const boundary = `batch_${generateUUID()}`;
499
+ const body = this.buildBatchBody(operations, boundary);
500
+ const headers = {
501
+ ...this.buildHeaders(options),
502
+ "Content-Type": `multipart/mixed; boundary=${boundary}`
503
+ };
504
+ const response = await this.client.post("$batch", {
505
+ body,
506
+ headers
507
+ });
508
+ return this.parseBatchResponse(await response.text());
509
+ }
510
+ /**
511
+ * Execute operations in an atomic changeset (all succeed or all fail)
512
+ * @example
513
+ * ```ts
514
+ * const results = await client.changeset({
515
+ * operations: [
516
+ * { method: 'POST', entity: 'Orders', data: orderData },
517
+ * { method: 'POST', entity: 'OrderItems', data: itemData },
518
+ * ]
519
+ * });
520
+ * ```
521
+ */
522
+ async changeset(changeset, options) {
523
+ const batchBoundary = `batch_${generateUUID()}`;
524
+ const changesetBoundary = `changeset_${generateUUID()}`;
525
+ const body = this.buildChangesetBody(changeset, batchBoundary, changesetBoundary);
526
+ const headers = {
527
+ ...this.buildHeaders(options),
528
+ "Content-Type": `multipart/mixed; boundary=${batchBoundary}`
529
+ };
530
+ const response = await this.client.post("$batch", {
531
+ body,
532
+ headers
533
+ });
534
+ return this.parseBatchResponse(await response.text());
535
+ }
536
+ buildBatchBody(operations, boundary) {
537
+ const parts = [];
538
+ for (const op of operations) {
539
+ parts.push(`--${boundary}`);
540
+ parts.push("Content-Type: application/http");
541
+ parts.push("Content-Transfer-Encoding: binary");
542
+ parts.push("");
543
+ parts.push(this.buildBatchPart(op));
544
+ }
545
+ parts.push(`--${boundary}--`);
546
+ return parts.join("\r\n");
547
+ }
548
+ buildChangesetBody(changeset, batchBoundary, changesetBoundary) {
549
+ const parts = [];
550
+ parts.push(`--${batchBoundary}`);
551
+ parts.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);
552
+ parts.push("");
553
+ for (const op of changeset.operations) {
554
+ parts.push(`--${changesetBoundary}`);
555
+ parts.push("Content-Type: application/http");
556
+ parts.push("Content-Transfer-Encoding: binary");
557
+ parts.push("");
558
+ parts.push(this.buildBatchPart(op));
559
+ }
560
+ parts.push(`--${changesetBoundary}--`);
561
+ parts.push(`--${batchBoundary}--`);
562
+ return parts.join("\r\n");
563
+ }
564
+ buildBatchPart(op) {
565
+ const lines = [];
566
+ let path;
567
+ switch (op.method) {
568
+ case "GET":
569
+ path = op.id ? `${op.entity}(${this.formatKey(op.id)})` : op.entity;
570
+ lines.push(`GET ${path} HTTP/1.1`);
571
+ lines.push("Accept: application/json");
572
+ break;
573
+ case "POST":
574
+ lines.push(`POST ${op.entity} HTTP/1.1`);
575
+ lines.push("Content-Type: application/json");
576
+ lines.push("");
577
+ lines.push(JSON.stringify(op.data));
578
+ break;
579
+ case "PATCH":
580
+ case "PUT":
581
+ path = `${op.entity}(${this.formatKey(op.id)})`;
582
+ lines.push(`${op.method} ${path} HTTP/1.1`);
583
+ lines.push("Content-Type: application/json");
584
+ lines.push("");
585
+ lines.push(JSON.stringify(op.data));
586
+ break;
587
+ case "DELETE":
588
+ path = `${op.entity}(${this.formatKey(op.id)})`;
589
+ lines.push(`DELETE ${path} HTTP/1.1`);
590
+ break;
591
+ }
592
+ return lines.join("\r\n");
593
+ }
594
+ formatKey(key) {
595
+ if (typeof key === "number") return String(key);
596
+ if (typeof key === "string") return `'${key}'`;
597
+ return Object.entries(key).map(([k, v]) => `${k}=${typeof v === "string" ? `'${v}'` : v}`).join(",");
598
+ }
599
+ parseBatchResponse(responseText) {
600
+ const results = [];
601
+ const parts = responseText.split(/--batch_[a-f0-9-]+/);
602
+ for (const part of parts) {
603
+ if (!part.trim() || part.trim() === "--") continue;
604
+ const statusMatch = part.match(/HTTP\/1\.1 (\d+)/);
605
+ if (!statusMatch || !statusMatch[1]) continue;
606
+ const status = parseInt(statusMatch[1], 10);
607
+ const jsonMatch = part.match(/\{[\s\S]*\}/);
608
+ if (status >= 200 && status < 300) {
609
+ results.push({
610
+ success: true,
611
+ status,
612
+ data: jsonMatch ? JSON.parse(jsonMatch[0]) : void 0
613
+ });
614
+ } else {
615
+ results.push({
616
+ success: false,
617
+ status,
618
+ error: jsonMatch ? { code: "BATCH_ERROR", message: JSON.parse(jsonMatch[0])?.error?.message ?? "Unknown error" } : { code: "BATCH_ERROR", message: "Unknown error" }
619
+ });
620
+ }
621
+ }
622
+ return results;
623
+ }
624
+ };
625
+
626
+ // src/query-builder.ts
627
+ function getExpandSelectPaths(expand) {
628
+ const paths = [];
629
+ if (Array.isArray(expand)) {
630
+ for (const item of expand) {
631
+ paths.push(item);
632
+ }
633
+ return paths;
634
+ }
635
+ for (const [property, value] of Object.entries(expand)) {
636
+ if (value === true) {
637
+ paths.push(property);
638
+ } else if (isExpandNestedOptions(value)) {
639
+ if (value.select?.length) {
640
+ for (const field of value.select) {
641
+ paths.push(`${property}/${field}`);
642
+ }
643
+ } else {
644
+ paths.push(property);
645
+ }
646
+ }
647
+ }
648
+ return paths;
649
+ }
650
+ function buildQuery(options) {
651
+ if (!options) return {};
652
+ const params = {};
653
+ let selectFields = options.select ? [...options.select] : [];
654
+ if (options.expand) {
655
+ const isArrayWithLength = Array.isArray(options.expand) && options.expand.length > 0;
656
+ const isObjectWithKeys = !Array.isArray(options.expand) && typeof options.expand === "object" && Object.keys(options.expand).length > 0;
657
+ if (isArrayWithLength || isObjectWithKeys) {
658
+ params["$expand"] = buildExpand(options.expand);
659
+ if (selectFields.length > 0) {
660
+ const expandPaths = getExpandSelectPaths(options.expand);
661
+ for (const path of expandPaths) {
662
+ if (!selectFields.includes(path)) {
663
+ selectFields.push(path);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ }
669
+ if (selectFields.length > 0) {
670
+ params["$select"] = selectFields.join(",");
671
+ }
672
+ if (options.filter !== void 0 && options.filter !== null) {
673
+ const filterStr = buildFilter(options.filter);
674
+ if (filterStr) {
675
+ params["$filter"] = filterStr;
676
+ }
677
+ }
678
+ if (options.top !== void 0) {
679
+ params["$top"] = String(options.top);
680
+ }
681
+ if (options.skip !== void 0) {
682
+ params["$skip"] = String(options.skip);
683
+ }
684
+ if (options.orderBy) {
685
+ params["$orderby"] = buildOrderBy(options.orderBy);
686
+ }
687
+ if (options.count) {
688
+ params["$count"] = "true";
689
+ }
690
+ if (options.search) {
691
+ params["$search"] = options.search;
692
+ }
693
+ return params;
694
+ }
695
+ function buildOrderBy(orderBy) {
696
+ if (typeof orderBy === "string") {
697
+ return orderBy;
698
+ }
699
+ if (Array.isArray(orderBy)) {
700
+ return orderBy.map((obj) => buildOrderByObject(obj)).join(",");
701
+ }
702
+ return buildOrderByObject(orderBy);
703
+ }
704
+ function buildOrderByObject(obj) {
705
+ return Object.entries(obj).map(([field, direction]) => `${field}${direction ? " " + direction : ""}`).join(",");
706
+ }
707
+ function formatFilterValue(value) {
708
+ if (value === null) return "null";
709
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
710
+ if (typeof value === "boolean") return value ? "true" : "false";
711
+ if (typeof value === "number") return String(value);
712
+ if (value instanceof Date) return value.toISOString();
713
+ return String(value);
714
+ }
715
+ function isFilterCondition(value) {
716
+ if (value === null || typeof value !== "object" || value instanceof Date) {
717
+ return false;
718
+ }
719
+ const operators = ["eq", "ne", "gt", "ge", "lt", "le", "contains", "startswith", "endswith", "in", "between"];
720
+ return Object.keys(value).some((key) => operators.includes(key));
721
+ }
722
+ function buildFieldConditions(field, value) {
723
+ if (!isFilterCondition(value)) {
724
+ return [`${field} eq ${formatFilterValue(value)}`];
725
+ }
726
+ const conditions = [];
727
+ const condition = value;
728
+ for (const [op, val] of Object.entries(condition)) {
729
+ if (val === void 0) continue;
730
+ switch (op) {
731
+ case "eq":
732
+ case "ne":
733
+ case "gt":
734
+ case "ge":
735
+ case "lt":
736
+ case "le":
737
+ conditions.push(`${field} ${op} ${formatFilterValue(val)}`);
738
+ break;
739
+ case "contains":
740
+ conditions.push(`contains(${field},${formatFilterValue(val)})`);
741
+ break;
742
+ case "startswith":
743
+ conditions.push(`startswith(${field},${formatFilterValue(val)})`);
744
+ break;
745
+ case "endswith":
746
+ conditions.push(`endswith(${field},${formatFilterValue(val)})`);
747
+ break;
748
+ case "in":
749
+ if (Array.isArray(val)) {
750
+ const inValues = val.map((v) => formatFilterValue(v)).join(",");
751
+ conditions.push(`${field} in (${inValues})`);
752
+ }
753
+ break;
754
+ case "between":
755
+ if (Array.isArray(val) && val.length === 2) {
756
+ conditions.push(`${field} ge ${formatFilterValue(val[0])} and ${field} le ${formatFilterValue(val[1])}`);
757
+ }
758
+ break;
759
+ }
760
+ }
761
+ return conditions;
762
+ }
763
+ var LOGICAL_OPERATORS = ["$or", "$and", "$not"];
764
+ function isLogicalOperator(key) {
765
+ return LOGICAL_OPERATORS.includes(key);
766
+ }
767
+ function buildFilterExpression(filter) {
768
+ const parts = [];
769
+ const fieldConditions = [];
770
+ for (const [key, value] of Object.entries(filter)) {
771
+ if (value === void 0) continue;
772
+ if (isLogicalOperator(key)) continue;
773
+ fieldConditions.push(...buildFieldConditions(key, value));
774
+ }
775
+ if (fieldConditions.length > 0) {
776
+ if (fieldConditions.length === 1) {
777
+ parts.push(fieldConditions[0]);
778
+ } else {
779
+ parts.push(fieldConditions.join(" and "));
780
+ }
781
+ }
782
+ if (filter.$and && Array.isArray(filter.$and)) {
783
+ const andParts = filter.$and.map((expr) => buildFilterExpression(expr)).filter((s) => Boolean(s));
784
+ if (andParts.length > 0) {
785
+ const andStr = andParts.length === 1 ? andParts[0] : `(${andParts.join(" and ")})`;
786
+ parts.push(andStr);
787
+ }
788
+ }
789
+ if (filter.$or && Array.isArray(filter.$or)) {
790
+ const orParts = filter.$or.map((expr) => buildFilterExpression(expr)).filter((s) => Boolean(s));
791
+ if (orParts.length > 0) {
792
+ const orStr = orParts.length === 1 ? orParts[0] : `(${orParts.join(" or ")})`;
793
+ parts.push(orStr);
794
+ }
795
+ }
796
+ if (filter.$not) {
797
+ const notExpr = buildFilterExpression(filter.$not);
798
+ if (notExpr) {
799
+ parts.push(`not (${notExpr})`);
800
+ }
801
+ }
802
+ if (parts.length === 0) return "";
803
+ if (parts.length === 1) return parts[0];
804
+ return parts.join(" and ");
805
+ }
806
+ function buildFilter(filter) {
807
+ if (typeof filter === "string") {
808
+ return filter;
809
+ }
810
+ if (Array.isArray(filter)) {
811
+ const parts = filter.map((f) => buildFilterExpression(f)).filter((s) => Boolean(s));
812
+ if (parts.length === 0) return "";
813
+ if (parts.length === 1) return parts[0];
814
+ return parts.join(" and ");
815
+ }
816
+ return buildFilterExpression(filter);
817
+ }
818
+ function isExpandNestedOptions(value) {
819
+ if (value === true || value === null || typeof value !== "object") {
820
+ return false;
821
+ }
822
+ const optionKeys = ["select", "filter", "top", "skip", "orderBy", "expand"];
823
+ return Object.keys(value).some((key) => optionKeys.includes(key));
824
+ }
825
+ function buildNestedExpandOptions(options) {
826
+ const parts = [];
827
+ if (options.filter !== void 0 && options.filter !== null) {
828
+ const filterStr = typeof options.filter === "string" ? options.filter : buildFilter(options.filter);
829
+ if (filterStr) {
830
+ parts.push(`$filter=${filterStr}`);
831
+ }
832
+ }
833
+ if (options.top !== void 0) {
834
+ parts.push(`$top=${options.top}`);
835
+ }
836
+ if (options.skip !== void 0) {
837
+ parts.push(`$skip=${options.skip}`);
838
+ }
839
+ if (options.orderBy) {
840
+ const orderByStr = typeof options.orderBy === "string" ? options.orderBy : buildOrderBy(options.orderBy);
841
+ parts.push(`$orderby=${orderByStr}`);
842
+ }
843
+ if (options.expand) {
844
+ parts.push(`$expand=${buildExpand(options.expand)}`);
845
+ }
846
+ return parts.length > 0 ? `(${parts.join(";")})` : "";
847
+ }
848
+ function buildExpand(expand) {
849
+ if (Array.isArray(expand)) {
850
+ return expand.join(",");
851
+ }
852
+ return Object.entries(expand).map(([property, value]) => {
853
+ if (value === true) {
854
+ return property;
855
+ }
856
+ if (isExpandNestedOptions(value)) {
857
+ return property + buildNestedExpandOptions(value);
858
+ }
859
+ return property;
860
+ }).filter(Boolean).join(",");
861
+ }
862
+ function createFilter() {
863
+ return new FilterBuilder();
864
+ }
865
+ var FilterBuilder = class _FilterBuilder {
866
+ parts = [];
867
+ /**
868
+ * Add a filter condition
869
+ */
870
+ where(field, operator, value) {
871
+ this.parts.push(this.buildCondition(String(field), operator, value));
872
+ return this;
873
+ }
874
+ /**
875
+ * Add AND condition
876
+ */
877
+ and(field, operator, value) {
878
+ if (this.parts.length > 0) {
879
+ this.parts.push("and");
880
+ }
881
+ this.parts.push(this.buildCondition(String(field), operator, value));
882
+ return this;
883
+ }
884
+ /**
885
+ * Add OR condition
886
+ */
887
+ or(field, operator, value) {
888
+ if (this.parts.length > 0) {
889
+ this.parts.push("or");
890
+ }
891
+ this.parts.push(this.buildCondition(String(field), operator, value));
892
+ return this;
893
+ }
894
+ /**
895
+ * Add NOT condition
896
+ */
897
+ not(field, operator, value) {
898
+ this.parts.push(`not ${this.buildCondition(String(field), operator, value)}`);
899
+ return this;
900
+ }
901
+ /**
902
+ * Group conditions with parentheses
903
+ */
904
+ group(fn) {
905
+ const nested = fn(new _FilterBuilder());
906
+ const nestedFilter = nested.build();
907
+ if (nestedFilter) {
908
+ this.parts.push(`(${nestedFilter})`);
909
+ }
910
+ return this;
911
+ }
912
+ /**
913
+ * Add raw filter expression
914
+ */
915
+ raw(expression) {
916
+ this.parts.push(expression);
917
+ return this;
918
+ }
919
+ /**
920
+ * Build the final filter string
921
+ */
922
+ build() {
923
+ return this.parts.join(" ");
924
+ }
925
+ /**
926
+ * Build a single condition
927
+ */
928
+ buildCondition(field, operator, value) {
929
+ const formattedValue = this.formatValue(value);
930
+ switch (operator) {
931
+ case "eq":
932
+ case "ne":
933
+ case "gt":
934
+ case "ge":
935
+ case "lt":
936
+ case "le":
937
+ return `${field} ${operator} ${formattedValue}`;
938
+ case "contains":
939
+ return `contains(${field},${formattedValue})`;
940
+ case "startswith":
941
+ return `startswith(${field},${formattedValue})`;
942
+ case "endswith":
943
+ return `endswith(${field},${formattedValue})`;
944
+ case "in":
945
+ if (!Array.isArray(value)) {
946
+ throw new Error('Value for "in" operator must be an array');
947
+ }
948
+ const inValues = value.map((v) => this.formatValue(v)).join(",");
949
+ return `${field} in (${inValues})`;
950
+ case "has":
951
+ return `${field} has ${formattedValue}`;
952
+ default:
953
+ throw new Error(`Unknown operator: ${operator}`);
954
+ }
955
+ }
956
+ /**
957
+ * Format a value for OData filter
958
+ */
959
+ formatValue(value) {
960
+ if (value === null) return "null";
961
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
962
+ if (typeof value === "boolean") return value ? "true" : "false";
963
+ if (typeof value === "number") return String(value);
964
+ if (value instanceof Date) return value.toISOString();
965
+ if (typeof value === "object") return JSON.stringify(value);
966
+ return String(value);
967
+ }
968
+ };
969
+ function query(handler) {
970
+ return new QueryBuilder(handler);
971
+ }
972
+ var QueryBuilder = class _QueryBuilder {
973
+ handler;
974
+ options = {};
975
+ filterParts = [];
976
+ constructor(handler) {
977
+ this.handler = handler;
978
+ }
979
+ /**
980
+ * Select specific fields
981
+ */
982
+ select(...fields) {
983
+ this.options.select = fields;
984
+ return this;
985
+ }
986
+ /**
987
+ * Add raw filter expression
988
+ */
989
+ filter(expression) {
990
+ this.filterParts.push(expression);
991
+ return this;
992
+ }
993
+ /**
994
+ * Add type-safe filter condition
995
+ */
996
+ where(field, operator, value) {
997
+ const condition = new FilterBuilder().where(field, operator, value).build();
998
+ this.filterParts.push(condition);
999
+ return this;
1000
+ }
1001
+ /**
1002
+ * Add AND condition
1003
+ */
1004
+ and(field, operator, value) {
1005
+ if (this.filterParts.length === 0) {
1006
+ return this.where(field, operator, value);
1007
+ }
1008
+ const condition = new FilterBuilder().where(field, operator, value).build();
1009
+ const last = this.filterParts.pop();
1010
+ this.filterParts.push(`${last} and ${condition}`);
1011
+ return this;
1012
+ }
1013
+ /**
1014
+ * Add OR condition
1015
+ */
1016
+ or(field, operator, value) {
1017
+ if (this.filterParts.length === 0) {
1018
+ return this.where(field, operator, value);
1019
+ }
1020
+ const condition = new FilterBuilder().where(field, operator, value).build();
1021
+ const last = this.filterParts.pop();
1022
+ this.filterParts.push(`${last} or ${condition}`);
1023
+ return this;
1024
+ }
1025
+ /**
1026
+ * Order by field
1027
+ */
1028
+ orderBy(field, direction = "asc") {
1029
+ const current = this.options.orderBy;
1030
+ if (typeof current === "string" && current) {
1031
+ this.options.orderBy = `${current},${String(field)} ${direction}`;
1032
+ } else {
1033
+ this.options.orderBy = `${String(field)} ${direction}`;
1034
+ }
1035
+ return this;
1036
+ }
1037
+ /**
1038
+ * Expand navigation property with optional nested query
1039
+ */
1040
+ expand(property, nested) {
1041
+ if (!this.options.expand || Array.isArray(this.options.expand)) {
1042
+ const existing = this.options.expand;
1043
+ this.options.expand = {};
1044
+ if (existing) {
1045
+ for (const prop of existing) {
1046
+ this.options.expand[prop] = true;
1047
+ }
1048
+ }
1049
+ }
1050
+ if (nested) {
1051
+ const nestedBuilder = new _QueryBuilder({ list: async () => [] });
1052
+ nested(nestedBuilder);
1053
+ const nestedOptions = nestedBuilder.buildOptions();
1054
+ this.options.expand[property] = {
1055
+ select: nestedOptions.select,
1056
+ filter: nestedOptions.filter,
1057
+ top: nestedOptions.top,
1058
+ orderBy: nestedOptions.orderBy
1059
+ };
1060
+ } else {
1061
+ this.options.expand[property] = true;
1062
+ }
1063
+ return this;
1064
+ }
1065
+ /**
1066
+ * Limit results
1067
+ */
1068
+ top(count) {
1069
+ this.options.top = count;
1070
+ return this;
1071
+ }
1072
+ /**
1073
+ * Skip results (pagination)
1074
+ */
1075
+ skip(count) {
1076
+ this.options.skip = count;
1077
+ return this;
1078
+ }
1079
+ /**
1080
+ * Full-text search
1081
+ */
1082
+ search(term) {
1083
+ this.options.search = term;
1084
+ return this;
1085
+ }
1086
+ /**
1087
+ * Request inline count
1088
+ */
1089
+ count() {
1090
+ this.options.count = true;
1091
+ return this;
1092
+ }
1093
+ /**
1094
+ * Execute query and return results
1095
+ */
1096
+ async execute() {
1097
+ return this.handler.list(this.buildOptions());
1098
+ }
1099
+ /**
1100
+ * Execute query with inline count
1101
+ */
1102
+ async executeWithCount() {
1103
+ return this.handler.listWithCount(this.buildOptions());
1104
+ }
1105
+ /**
1106
+ * Execute and return first result (or undefined)
1107
+ */
1108
+ async first() {
1109
+ this.options.top = 1;
1110
+ const results = await this.execute();
1111
+ return results[0];
1112
+ }
1113
+ /**
1114
+ * Execute and return single result (throws if not exactly one)
1115
+ */
1116
+ async single() {
1117
+ this.options.top = 2;
1118
+ const results = await this.execute();
1119
+ if (results.length === 0) {
1120
+ throw new Error("No results found");
1121
+ }
1122
+ if (results.length > 1) {
1123
+ throw new Error("Multiple results found, expected exactly one");
1124
+ }
1125
+ return results[0];
1126
+ }
1127
+ /**
1128
+ * Build final query options
1129
+ */
1130
+ buildOptions() {
1131
+ if (this.filterParts.length > 0) {
1132
+ this.options.filter = this.filterParts.join(" and ");
1133
+ }
1134
+ return { ...this.options };
1135
+ }
1136
+ };
1137
+ function formatKey(key) {
1138
+ if (typeof key === "number") {
1139
+ return String(key);
1140
+ }
1141
+ if (typeof key === "string") {
1142
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(key)) {
1143
+ return key;
1144
+ }
1145
+ return `'${key.replace(/'/g, "''")}'`;
1146
+ }
1147
+ const parts = Object.entries(key).map(([k, v]) => `${k}=${typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : v}`).join(",");
1148
+ return parts;
1149
+ }
1150
+ function buildFunctionParams(params) {
1151
+ if (!params || Object.keys(params).length === 0) {
1152
+ return "";
1153
+ }
1154
+ const formatted = Object.entries(params).map(([key, value]) => {
1155
+ if (typeof value === "string") {
1156
+ return `${key}='${value.replace(/'/g, "''")}'`;
1157
+ }
1158
+ if (value === null) {
1159
+ return `${key}=null`;
1160
+ }
1161
+ if (typeof value === "boolean") {
1162
+ return `${key}=${value}`;
1163
+ }
1164
+ if (value instanceof Date) {
1165
+ return `${key}=${value.toISOString()}`;
1166
+ }
1167
+ return `${key}=${value}`;
1168
+ }).join(",");
1169
+ return formatted;
1170
+ }
1171
+
1172
+ // src/proxy.ts
1173
+ function extractRequestOptions(options) {
1174
+ if (!options) return void 0;
1175
+ const { connection, service, raw } = options;
1176
+ if (!connection && !service && !raw) return void 0;
1177
+ return { connection, service, raw };
1178
+ }
1179
+ function formatId(id) {
1180
+ return formatKey(id);
1181
+ }
1182
+ function extractData(response) {
1183
+ if (Array.isArray(response)) return response;
1184
+ if (response?.data && Array.isArray(response.data)) return response.data;
1185
+ if (response?.value && Array.isArray(response.value)) return response.value;
1186
+ if (response?.d?.results && Array.isArray(response.d.results)) return response.d.results;
1187
+ return [];
1188
+ }
1189
+ function extractSingle(response) {
1190
+ if (response?.data && !Array.isArray(response.data)) return response.data;
1191
+ if (response?.d) return response.d;
1192
+ return response;
1193
+ }
1194
+ function extractCount(response) {
1195
+ if (response?.count !== void 0) return Number(response.count);
1196
+ if (response?.["@odata.count"] !== void 0) return response["@odata.count"];
1197
+ if (response?.["odata.count"] !== void 0) return parseInt(response["odata.count"], 10);
1198
+ if (response?.__count !== void 0) return parseInt(response.__count, 10);
1199
+ if (response?.d?.__count !== void 0) return parseInt(response.d.__count, 10);
1200
+ return void 0;
1201
+ }
1202
+ function createEntityHandler(client, entityName) {
1203
+ const basePath = entityName;
1204
+ return {
1205
+ // ==========================================================================
1206
+ // READ Operations
1207
+ // ==========================================================================
1208
+ /**
1209
+ * List entities with optional query options
1210
+ */
1211
+ async list(options) {
1212
+ const response = await client.get(
1213
+ basePath,
1214
+ buildQuery(options),
1215
+ extractRequestOptions(options)
1216
+ );
1217
+ return extractData(response);
1218
+ },
1219
+ /**
1220
+ * List entities with count
1221
+ */
1222
+ async listWithCount(options) {
1223
+ const queryOptions = { ...options, count: true };
1224
+ const response = await client.get(
1225
+ basePath,
1226
+ buildQuery(queryOptions),
1227
+ extractRequestOptions(options)
1228
+ );
1229
+ return {
1230
+ value: extractData(response),
1231
+ count: extractCount(response)
1232
+ };
1233
+ },
1234
+ /**
1235
+ * Get single entity by key
1236
+ */
1237
+ async get(id, options) {
1238
+ const response = await client.get(
1239
+ `${basePath}(${formatId(id)})`,
1240
+ buildQuery(options),
1241
+ extractRequestOptions(options)
1242
+ );
1243
+ return extractSingle(response);
1244
+ },
1245
+ /**
1246
+ * Count matching entities
1247
+ */
1248
+ async count(options) {
1249
+ const response = await client.get(
1250
+ `${basePath}/$count`,
1251
+ buildQuery(options),
1252
+ extractRequestOptions(options)
1253
+ );
1254
+ return typeof response === "number" ? response : parseInt(String(response), 10);
1255
+ },
1256
+ // ==========================================================================
1257
+ // CREATE Operations
1258
+ // ==========================================================================
1259
+ /**
1260
+ * Create a new entity (POST)
1261
+ */
1262
+ async create(data, options) {
1263
+ const response = await client.post(
1264
+ basePath,
1265
+ data,
1266
+ extractRequestOptions(options)
1267
+ );
1268
+ return extractSingle(response);
1269
+ },
1270
+ /**
1271
+ * Create with related entities (deep insert)
1272
+ */
1273
+ async createDeep(data, options) {
1274
+ const response = await client.post(
1275
+ basePath,
1276
+ data,
1277
+ extractRequestOptions(options)
1278
+ );
1279
+ return extractSingle(response);
1280
+ },
1281
+ // ==========================================================================
1282
+ // UPDATE Operations
1283
+ // ==========================================================================
1284
+ /**
1285
+ * Partial update (PATCH) - only updates specified fields
1286
+ */
1287
+ async update(id, data, options) {
1288
+ const response = await client.patch(
1289
+ `${basePath}(${formatId(id)})`,
1290
+ data,
1291
+ extractRequestOptions(options)
1292
+ );
1293
+ return extractSingle(response);
1294
+ },
1295
+ /**
1296
+ * Full replacement (PUT) - replaces entire entity
1297
+ */
1298
+ async replace(id, data, options) {
1299
+ const response = await client.put(
1300
+ `${basePath}(${formatId(id)})`,
1301
+ data,
1302
+ extractRequestOptions(options)
1303
+ );
1304
+ return extractSingle(response);
1305
+ },
1306
+ /**
1307
+ * Upsert - create or update based on key
1308
+ * Uses PUT to create or replace
1309
+ */
1310
+ async upsert(data, options) {
1311
+ const key = data.ID ?? data.id ?? data.Id;
1312
+ if (!key) {
1313
+ throw new Error("Upsert requires an ID field in the data");
1314
+ }
1315
+ return this.replace(key, data, options);
1316
+ },
1317
+ // ==========================================================================
1318
+ // DELETE Operations
1319
+ // ==========================================================================
1320
+ /**
1321
+ * Delete entity by key
1322
+ */
1323
+ async delete(id, options) {
1324
+ await client.delete(
1325
+ `${basePath}(${formatId(id)})`,
1326
+ extractRequestOptions(options)
1327
+ );
1328
+ },
1329
+ // ==========================================================================
1330
+ // Navigation Properties
1331
+ // ==========================================================================
1332
+ /**
1333
+ * Access navigation property (related entities)
1334
+ */
1335
+ nav(id, property) {
1336
+ const navPath = `${basePath}(${formatId(id)})/${property}`;
1337
+ return createEntityHandler(client, navPath);
1338
+ },
1339
+ // ==========================================================================
1340
+ // OData Functions & Actions
1341
+ // ==========================================================================
1342
+ /**
1343
+ * Call an OData function (GET operation)
1344
+ * Unbound function on the entity set
1345
+ */
1346
+ async func(name, params) {
1347
+ const paramStr = buildFunctionParams(params);
1348
+ const path = paramStr ? `${basePath}/${name}(${paramStr})` : `${basePath}/${name}()`;
1349
+ return client.get(path);
1350
+ },
1351
+ /**
1352
+ * Call an OData action (POST operation)
1353
+ * Unbound action on the entity set
1354
+ */
1355
+ async action(name, params) {
1356
+ return client.post(`${basePath}/${name}`, params ?? {});
1357
+ },
1358
+ /**
1359
+ * Call bound function on specific entity
1360
+ */
1361
+ async boundFunc(id, name, params) {
1362
+ const paramStr = buildFunctionParams(params);
1363
+ const path = paramStr ? `${basePath}(${formatId(id)})/${name}(${paramStr})` : `${basePath}(${formatId(id)})/${name}()`;
1364
+ return client.get(path);
1365
+ },
1366
+ /**
1367
+ * Call bound action on specific entity
1368
+ */
1369
+ async boundAction(id, name, params) {
1370
+ return client.post(`${basePath}(${formatId(id)})/${name}`, params ?? {});
1371
+ },
1372
+ // ==========================================================================
1373
+ // Pagination
1374
+ // ==========================================================================
1375
+ /**
1376
+ * Paginate through all entities with automatic page handling
1377
+ */
1378
+ paginate(options) {
1379
+ const pageSize = options?.pageSize ?? options?.top ?? 100;
1380
+ const maxItems = options?.maxItems;
1381
+ const queryOptions = { ...options, top: pageSize };
1382
+ const self = this;
1383
+ return {
1384
+ [Symbol.asyncIterator]() {
1385
+ let skip = 0;
1386
+ let totalFetched = 0;
1387
+ let done = false;
1388
+ return {
1389
+ async next() {
1390
+ if (done) {
1391
+ return { done: true, value: void 0 };
1392
+ }
1393
+ let pageTop = pageSize;
1394
+ if (maxItems !== void 0) {
1395
+ const remaining = maxItems - totalFetched;
1396
+ if (remaining <= 0) {
1397
+ done = true;
1398
+ return { done: true, value: void 0 };
1399
+ }
1400
+ pageTop = Math.min(pageSize, remaining);
1401
+ }
1402
+ const response = await self.listWithCount({
1403
+ ...queryOptions,
1404
+ top: pageTop,
1405
+ skip
1406
+ });
1407
+ const items = response.value;
1408
+ totalFetched += items.length;
1409
+ skip += items.length;
1410
+ if (items.length < pageTop) {
1411
+ done = true;
1412
+ }
1413
+ if (maxItems !== void 0 && totalFetched >= maxItems) {
1414
+ done = true;
1415
+ }
1416
+ return {
1417
+ done: false,
1418
+ value: {
1419
+ value: items,
1420
+ count: response.count
1421
+ }
1422
+ };
1423
+ }
1424
+ };
1425
+ }
1426
+ };
1427
+ },
1428
+ /**
1429
+ * Get all entities (automatically handles pagination)
1430
+ */
1431
+ async all(options) {
1432
+ const result = [];
1433
+ for await (const page of this.paginate(options)) {
1434
+ result.push(...page.value);
1435
+ }
1436
+ return result;
1437
+ }
1438
+ };
1439
+ }
1440
+
1441
+ // src/client.ts
1442
+ var S4KitBase = class {
1443
+ httpClient;
1444
+ constructor(config) {
1445
+ this.httpClient = new HttpClient(config);
1446
+ }
1447
+ // ==========================================================================
1448
+ // Interceptors
1449
+ // ==========================================================================
1450
+ /**
1451
+ * Add a request interceptor
1452
+ * @example
1453
+ * ```ts
1454
+ * client.onRequest((req) => {
1455
+ * console.log(`${req.method} ${req.url}`);
1456
+ * return req;
1457
+ * });
1458
+ * ```
1459
+ */
1460
+ onRequest(interceptor) {
1461
+ this.httpClient.onRequest(interceptor);
1462
+ return this;
1463
+ }
1464
+ /**
1465
+ * Add a response interceptor
1466
+ * @example
1467
+ * ```ts
1468
+ * client.onResponse((res) => {
1469
+ * console.log(`Response: ${res.status}`);
1470
+ * return res;
1471
+ * });
1472
+ * ```
1473
+ */
1474
+ onResponse(interceptor) {
1475
+ this.httpClient.onResponse(interceptor);
1476
+ return this;
1477
+ }
1478
+ /**
1479
+ * Add an error interceptor
1480
+ * @example
1481
+ * ```ts
1482
+ * client.onError((err) => {
1483
+ * console.error(`Error: ${err.message}`);
1484
+ * return err;
1485
+ * });
1486
+ * ```
1487
+ */
1488
+ onError(interceptor) {
1489
+ this.httpClient.onError(interceptor);
1490
+ return this;
1491
+ }
1492
+ // ==========================================================================
1493
+ // Batch Operations
1494
+ // ==========================================================================
1495
+ /**
1496
+ * Execute multiple operations in a single batch request
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * const results = await client.batch([
1501
+ * { method: 'GET', entity: 'Products', id: 1 },
1502
+ * { method: 'POST', entity: 'Products', data: { Name: 'New Product' } },
1503
+ * { method: 'PATCH', entity: 'Products', id: 2, data: { Price: 29.99 } },
1504
+ * { method: 'DELETE', entity: 'Products', id: 3 },
1505
+ * ]);
1506
+ *
1507
+ * // Check results
1508
+ * for (const result of results) {
1509
+ * if (result.success) {
1510
+ * console.log('Success:', result.data);
1511
+ * } else {
1512
+ * console.error('Failed:', result.error);
1513
+ * }
1514
+ * }
1515
+ * ```
1516
+ */
1517
+ async batch(operations) {
1518
+ return this.httpClient.batch(operations);
1519
+ }
1520
+ /**
1521
+ * Execute operations in an atomic changeset (all succeed or all fail)
1522
+ *
1523
+ * @example
1524
+ * ```ts
1525
+ * // All operations succeed or all fail together
1526
+ * const results = await client.changeset({
1527
+ * operations: [
1528
+ * { method: 'POST', entity: 'Orders', data: orderData },
1529
+ * { method: 'POST', entity: 'OrderItems', data: item1Data },
1530
+ * { method: 'POST', entity: 'OrderItems', data: item2Data },
1531
+ * ]
1532
+ * });
1533
+ * ```
1534
+ */
1535
+ async changeset(changeset) {
1536
+ return this.httpClient.changeset(changeset);
1537
+ }
1538
+ };
1539
+ var RESERVED_METHODS = /* @__PURE__ */ new Set([
1540
+ "onRequest",
1541
+ "onResponse",
1542
+ "onError",
1543
+ "batch",
1544
+ "changeset",
1545
+ "constructor",
1546
+ "httpClient",
1547
+ // Internal properties
1548
+ "then",
1549
+ "catch",
1550
+ "finally"
1551
+ ]);
1552
+ function S4Kit(config) {
1553
+ const base = new S4KitBase(config);
1554
+ return new Proxy(base, {
1555
+ get(target, prop) {
1556
+ if (typeof prop === "symbol") {
1557
+ return target[prop];
1558
+ }
1559
+ if (RESERVED_METHODS.has(prop) || prop in target) {
1560
+ const value = target[prop];
1561
+ if (typeof value === "function") {
1562
+ return value.bind(target);
1563
+ }
1564
+ return value;
1565
+ }
1566
+ return createEntityHandler(target["httpClient"], prop);
1567
+ }
1568
+ });
1569
+ }
1570
+ S4Kit.prototype = S4KitBase.prototype;
1571
+ function createClient(config) {
1572
+ return S4Kit(config);
1573
+ }
1574
+ export {
1575
+ AuthenticationError,
1576
+ AuthorizationError,
1577
+ BatchError,
1578
+ ConflictError,
1579
+ FilterBuilder,
1580
+ NetworkError,
1581
+ NotFoundError,
1582
+ QueryBuilder,
1583
+ RateLimitError,
1584
+ S4Kit,
1585
+ S4KitError,
1586
+ ServerError,
1587
+ TimeoutError,
1588
+ ValidationError,
1589
+ buildFilter,
1590
+ buildFunctionParams,
1591
+ buildQuery,
1592
+ createClient,
1593
+ createFilter,
1594
+ formatKey,
1595
+ isRetryable,
1596
+ isS4KitError,
1597
+ parseHttpError,
1598
+ parseODataError,
1599
+ query
1600
+ };