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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/cli.cjs +211 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +188 -0
- package/dist/index.cjs +1661 -0
- package/dist/index.d.cts +1175 -0
- package/dist/index.d.ts +1175 -0
- package/dist/index.js +1600 -0
- package/package.json +60 -6
- package/index.js +0 -1
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
|
+
};
|