s4kit 0.0.1 → 0.1.0

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