@randajan/api-kit 1.2.0 → 3.0.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.
@@ -19,17 +19,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  // src/client/index.js
20
20
  var client_exports = {};
21
21
  __export(client_exports, {
22
- ApiError: () => ApiError,
23
22
  Fetch: () => Fetch,
23
+ FetchError: () => FetchError,
24
24
  createFetch: () => createFetch,
25
25
  default: () => client_default
26
26
  });
27
27
  module.exports = __toCommonJS(client_exports);
28
28
 
29
29
  // <define:__slib_info>
30
- var define_slib_info_default = { isBuild: true, name: "@randajan/api-kit", description: "A lightweight wrapper around fetch that always returns a structured JSON response, handling errors and network issues gracefully", version: "1.1.1", author: { name: "Jan Randa", email: "jnranda@gmail.com", url: "https://www.linkedin.com/in/randajan/" }, env: "development", mode: "web", port: 3e3, dir: { root: "C:\\dev\\lib\\api-kit", dist: "dist" } };
30
+ var define_slib_info_default = { isBuild: true, name: "@randajan/api-kit", description: "A lightweight wrapper around fetch that always returns a structured JSON response, handling errors and network issues gracefully", version: "2.0.0", author: { name: "Jan Randa", email: "jnranda@gmail.com", url: "https://www.linkedin.com/in/randajan/" }, env: "development", mode: "web", port: 3e3, dir: { root: "C:\\dev\\lib\\api-kit", dist: "dist" } };
31
31
 
32
- // src/arc/class/ApiError.js
32
+ // src/arc/class/FetchError.js
33
33
  var import_props = require("@randajan/props");
34
34
 
35
35
  // src/arc/tool.js
@@ -53,34 +53,36 @@ var mrgStr = (a, b, sep = "") => {
53
53
  };
54
54
  var isFn = (any) => typeof any === "function";
55
55
 
56
- // src/arc/class/ApiError.js
57
- var ApiError = class _ApiError extends Error {
58
- static create(code, message) {
59
- return new _ApiError(code, message);
60
- }
56
+ // src/arc/class/FetchError.js
57
+ var FetchError = class _FetchError extends Error {
61
58
  static is(any) {
62
- return any instanceof _ApiError;
59
+ return any && any instanceof _FetchError;
63
60
  }
64
- static to(code, any) {
65
- if (_ApiError.is(any)) {
66
- return any;
67
- }
68
- const msg = toStr(any?.message || any) || "Unknown";
69
- const apierr = _ApiError.create(code, msg);
70
- return (0, import_props.solid)(apierr, "stack", any.stack, false);
61
+ static code(code, message, opt = {}) {
62
+ opt.code = code;
63
+ return new _FetchError(message, opt);
71
64
  }
72
- constructor(code, message) {
73
- super(message);
65
+ constructor(message, options = {}) {
66
+ const { code, ...opt } = options || {};
67
+ super(message, opt);
74
68
  (0, import_props.safe)(this, {}, "code", (t, f) => mrgStr(t, f, "."));
75
69
  this.rise(code);
76
70
  }
77
- rise(code = 0) {
78
- this.code = code || 0;
71
+ rise(code) {
72
+ if (code == null) {
73
+ return this;
74
+ }
75
+ this.code = code;
79
76
  return this;
80
77
  }
81
78
  toJSON() {
82
- const { message } = this;
83
- return { message, ...this };
79
+ const { message, cause } = this;
80
+ const body = { message, ...this };
81
+ if (cause) {
82
+ const { message: message2, stack } = cause;
83
+ body.cause = { message: message2, stack };
84
+ }
85
+ return body;
84
86
  }
85
87
  toString() {
86
88
  const { message, code } = this;
@@ -111,30 +113,12 @@ var info = lockObject(define_slib_info_default);
111
113
  var info_default = info;
112
114
 
113
115
  // src/client/class/static.js
114
- var import_props3 = require("@randajan/props");
115
-
116
- // src/arc/url.js
117
- var _regexp = /^https?:\/\//i;
118
- var buildUrl = (inputUrl, params) => {
119
- if (!params) {
120
- return inputUrl;
121
- }
122
- const hasHost = _regexp.test(inputUrl);
123
- const url = new URL(inputUrl, hasHost ? void 0 : "http://thisisonlyplaceholder.xyz");
124
- url.hash = "";
125
- for (const [key, value] of Object.entries(params)) {
126
- if (value != null) {
127
- url.searchParams.append(key, value);
128
- }
129
- }
130
- ;
131
- return hasHost ? url.toString() : url.pathname + url.search;
132
- };
116
+ var import_props4 = require("@randajan/props");
133
117
 
134
- // src/arc/main.js
118
+ // src/arc/opt.js
135
119
  var import_props2 = require("@randajan/props");
136
120
  var start = (opt) => {
137
- if (opt.timestamp) {
121
+ if (opt.timetrack) {
138
122
  return opt.startAt = Date.now();
139
123
  }
140
124
  };
@@ -156,7 +140,9 @@ var endTime = (body, startAt) => {
156
140
  body.time = (0, import_props2.solids)({}, time);
157
141
  };
158
142
  var end = (body, opt) => {
159
- if (!body.error) {
143
+ const hasError = !!body.error;
144
+ endTime(body, opt.startAt);
145
+ if (!hasError) {
160
146
  if (isFn(opt.onOk)) {
161
147
  opt.onOk(body, opt);
162
148
  }
@@ -168,25 +154,57 @@ var end = (body, opt) => {
168
154
  throw body.error;
169
155
  }
170
156
  }
171
- endTime(body, opt.startAt);
172
- return opt.resultOnly ? body.result : Object.freeze(body);
157
+ return Object.freeze(body);
173
158
  };
174
- var diffVersion = (version) => {
175
- const v1 = toStr(info.version);
176
- const v2 = toStr(version);
177
- if (v1 === v2) {
178
- return;
159
+ var createTrait = (trait) => {
160
+ if (!trait) {
161
+ return (opt) => opt;
179
162
  }
180
- const f1 = v1.split(".");
181
- const f2 = v2.split(".");
182
- if (f1[0] !== f2[0]) {
183
- return "major";
163
+ if (isFn(trait)) {
164
+ return trait;
184
165
  }
185
- if (f1[1] !== f2[1]) {
186
- return "minor";
166
+ throw new Error("config.trait should be a function");
167
+ };
168
+ var configTrait = (config) => {
169
+ config.trait = createTrait(config.trait);
170
+ };
171
+
172
+ // src/arc/class/HttpError.js
173
+ var import_props3 = require("@randajan/props");
174
+ var HttpError = class _HttpError extends FetchError {
175
+ static is(any) {
176
+ return any && any instanceof _HttpError;
177
+ }
178
+ static code(code, message, httpStatusCode = 400, opt = {}) {
179
+ opt.code = code;
180
+ return new _HttpError(message, httpStatusCode, opt);
181
+ }
182
+ constructor(message, httpStatusCode = 400, options = {}) {
183
+ const { detail, exposeCause, ...opt } = options || {};
184
+ const http = httpStatusCode ?? 400;
185
+ const msg = message || `HTTP:${http}`;
186
+ super(msg, opt);
187
+ (0, import_props3.safe)(this, {}, "_exposeCause", (t) => !!t, void 0, false);
188
+ this.detail = detail;
189
+ this.httpStatusCode = http;
190
+ this._exposeCause = exposeCause;
191
+ }
192
+ exposeCause(exposeCause = true) {
193
+ this._exposeCause = exposeCause;
194
+ return this;
187
195
  }
188
- if (f1[2] !== f2[2]) {
189
- return "patch";
196
+ toJSON() {
197
+ const { message, cause, _exposeCause } = this;
198
+ const body = { message, ...this };
199
+ if (_exposeCause && cause) {
200
+ const { message: message2, stack } = cause;
201
+ body.cause = { message: message2, stack, ...cause };
202
+ }
203
+ return body;
204
+ }
205
+ toString() {
206
+ const { httpStatusCode } = this;
207
+ return super.toString() + ` [${httpStatusCode}]`;
190
208
  }
191
209
  };
192
210
 
@@ -213,32 +231,141 @@ var decode2 = (body) => Object.fromEntries(new URLSearchParams(body));
213
231
  var encode2 = (body) => new URLSearchParams(body);
214
232
 
215
233
  // src/arc/types/index.js
216
- var types = { json: json_exports, form: form_exports };
217
- var getType = (typeName) => {
218
- if (!typeName) {
234
+ var _types = { json: json_exports, form: form_exports };
235
+ var joinTypeNames = (types = {}) => {
236
+ const native = Object.keys(_types);
237
+ const custom = typeof types === "object" ? Object.keys(types) : [];
238
+ return `'${[.../* @__PURE__ */ new Set([...native, ...custom])].join("', '")}'`;
239
+ };
240
+ var getType = (propertyName, reqFnName, opt) => {
241
+ const tn = opt[propertyName] || opt.type;
242
+ if (!tn) {
219
243
  return json_exports;
220
244
  }
221
- const type = types[typeName];
222
- if (type) {
245
+ const customType = opt.types?.[tn];
246
+ const type = customType || _types[tn];
247
+ if (!type) {
248
+ throw new Error(`Unknown config.${propertyName} '${tn}'. Must be one of: ${joinTypeNames(opt.types)}`);
249
+ }
250
+ if (!customType) {
223
251
  return type;
224
252
  }
225
- throw new Error(`Unknow config.type '${typeName}'. Must be one of: '${[...Object.keys(types)].join("', '")}'`);
253
+ if (!type.mime || typeof type.mime !== "string") {
254
+ throw new Error(`Type definition '${tn}' requires 'mime' to be a string`);
255
+ }
256
+ if (!isFn(type[reqFnName])) {
257
+ throw new Error(`Type definition '${tn}' requires '${reqFnName}' to be a function`);
258
+ }
259
+ return type;
226
260
  };
227
261
  var encodeBody = (opt) => {
228
- const type = getType(opt.requestType || opt.type);
262
+ const type = getType("requestType", "encode", opt);
229
263
  opt.headers["Content-Type"] = type.mime;
230
264
  opt.body = type.encode(opt.body);
231
265
  };
232
266
  var attachBodyDecoder = (opt) => {
233
- const type = getType(opt.responseType || opt.type);
267
+ const type = getType("responseType", "decode", opt);
234
268
  opt.headers["Accept"] = type.mime;
235
269
  opt.decodeBody = type.decode;
236
270
  };
237
271
 
238
- // src/client/class/static.js
272
+ // src/client/url.js
273
+ var _regexp = /^https?:\/\//i;
274
+ var buildUrl = (inputUrl, params) => {
275
+ if (!params) {
276
+ return inputUrl;
277
+ }
278
+ const hasHost = _regexp.test(inputUrl);
279
+ const url = new URL(inputUrl, hasHost ? void 0 : "http://thisisonlyplaceholder.xyz");
280
+ url.hash = "";
281
+ for (const [key, value] of Object.entries(params)) {
282
+ if (value != null) {
283
+ url.searchParams.append(key, value);
284
+ }
285
+ }
286
+ ;
287
+ return hasHost ? url.toString() : url.pathname + url.search;
288
+ };
289
+
290
+ // src/client/tool.js
291
+ var diffVersion = (version) => {
292
+ const v1 = toStr(info.version);
293
+ const v2 = toStr(version);
294
+ if (v1 === v2) {
295
+ return;
296
+ }
297
+ const f1 = v1.split(".");
298
+ const f2 = v2.split(".");
299
+ if (f1[0] !== f2[0]) {
300
+ return "major";
301
+ }
302
+ if (f1[1] !== f2[1]) {
303
+ return "minor";
304
+ }
305
+ if (f1[2] !== f2[2]) {
306
+ return "patch";
307
+ }
308
+ };
309
+ var failedError = (cause) => FetchError.code(1, "Failed", { cause });
310
+ var timeoutError = (_) => FetchError.code(2, "Timeout");
311
+ var abortError = (_) => FetchError.code(3, "Aborted");
312
+ var statusError = (resp) => HttpError.code(4, resp.statusText, resp.status, { exposeCause: true });
313
+ var unreadableError = (resp, cause) => resp.ok ? FetchError.code(5, "Unreadable", { cause }) : statusError(resp);
314
+ var undecodableError = (resp, cause, detail) => resp.ok ? FetchError.code(6, "Undecodable", { cause, detail }) : statusError(resp);
315
+ var parseHeaders = (resp, opt) => {
316
+ if (!resp || !opt.parseHeaders) {
317
+ return;
318
+ }
319
+ return Object.freeze(Object.fromEntries(resp.headers.entries()));
320
+ };
321
+ var readText = async (resp) => {
322
+ try {
323
+ return await resp.text();
324
+ } catch (err) {
325
+ throw unreadableError(resp, err);
326
+ }
327
+ };
328
+ var decodeText = (resp, opt, text) => {
329
+ try {
330
+ return opt.decodeBody(text);
331
+ } catch (err) {
332
+ throw undecodableError(resp, err, text);
333
+ }
334
+ };
335
+ var normalizeClientError = (any, opt) => {
336
+ let err = any;
337
+ if (!FetchError.is(err)) {
338
+ let custom = isFn(opt.normalizeError);
339
+ if (custom) {
340
+ try {
341
+ err = opt.normalizeError(err, opt);
342
+ } catch {
343
+ custom = false;
344
+ }
345
+ }
346
+ if (!custom || !FetchError.is(err)) {
347
+ err = FetchError.code(0, "Unknown", { cause: any });
348
+ }
349
+ }
350
+ return err.rise(0).rise(0);
351
+ };
352
+ var reconstructApiError = (remoteErr) => {
353
+ if (!remoteErr) {
354
+ return;
355
+ }
356
+ const { message, code, httpStatusCode, cause, detail } = remoteErr;
357
+ return HttpError.code(code, message, httpStatusCode, { cause, detail, exposeCause: true });
358
+ };
359
+ var mrgOpt = (a, b) => {
360
+ const opt = mrgObj(a, b);
361
+ opt.body = mrgObj(a?.body, b?.body);
362
+ opt.params = mrgObj(a?.params, b?.params);
363
+ opt.headers = mrgObj(a?.headers, b?.headers, {});
364
+ return opt;
365
+ };
239
366
  var prepareOpt = (url, opt, method) => {
240
367
  start(opt);
241
- opt.url = buildUrl(mrgStr(opt.url, url), opt.query);
368
+ opt.url = buildUrl(mrgStr(opt.url, url), opt.params);
242
369
  if (method) {
243
370
  opt.method = method;
244
371
  }
@@ -247,86 +374,75 @@ var prepareOpt = (url, opt, method) => {
247
374
  }
248
375
  attachBodyDecoder(opt);
249
376
  const hasTimeout = opt.timeout > 0;
250
- if (opt.abortable || hasTimeout) {
377
+ if (opt.isAbortable || hasTimeout) {
251
378
  opt.abortController = new AbortController();
252
379
  opt.signal = opt.abortController.signal;
253
380
  }
254
381
  if (hasTimeout) {
255
- opt.timeoutId = setTimeout(() => opt.abortController?.abort(new ApiError(1, "Timeout")), opt.timeout);
382
+ opt.timeoutId = setTimeout(() => opt.abortController?.abort(timeoutError()), opt.timeout);
256
383
  }
257
384
  if (isFn(opt.trait)) {
258
385
  opt = opt.trait(opt) || opt;
259
386
  }
260
387
  return opt;
261
388
  };
262
- var localReject = (opt, error, http) => {
263
- const { code } = error;
264
- if (code == 0) {
265
- http = 503;
266
- } else if (code == 1) {
267
- http = 408;
268
- } else if (code == 2) {
269
- http = 499;
270
- } else if (code == 3) {
271
- http = 415;
272
- } else if (!http) {
273
- http = 400;
274
- }
275
- error.rise(0).rise(opt.code || 0);
276
- return end({ ok: false, http, error }, opt);
389
+
390
+ // src/client/class/static.js
391
+ var localReject = (err, opt) => ({ error: normalizeClientError(FetchError.is(err) ? err : failedError(err), opt) });
392
+ var remoteReject = (err, opt) => ({ isRemote: true, error: normalizeClientError(err, opt) });
393
+ var remoteResolve = async (resp, opt) => {
394
+ const text = await readText(resp);
395
+ if (!text) {
396
+ if (resp.ok || resp.status === 304) {
397
+ return { isRemote: true };
398
+ }
399
+ throw statusError(resp);
400
+ }
401
+ const body = decodeText(resp, opt, text);
402
+ const ver = body?.[info_default.name];
403
+ if (!ver) {
404
+ if (resp.ok) {
405
+ return { isRemote: true, result: body };
406
+ }
407
+ throw statusError(resp);
408
+ }
409
+ body.isRemote = true;
410
+ body.isApiKit = true;
411
+ body.apiKitDiff = diffVersion(ver);
412
+ body.error = reconstructApiError(body.error);
413
+ return body;
277
414
  };
278
- var fetchExe = async (_fetch, opt) => {
279
- let resp, raw, body;
415
+ var fetchBody = async (_fetch, opt) => {
416
+ let resp, body;
280
417
  try {
281
418
  resp = await _fetch(opt.url, opt);
282
- } catch (err) {
283
- return localReject(opt, ApiError.is(err) ? err : ApiError.to(0, "Failed"));
419
+ } catch (err2) {
420
+ body = localReject(err2, opt);
284
421
  }
285
- const { ok, status, headers, statusText } = resp;
286
422
  try {
287
- raw = opt.decodeBody(await resp.text());
288
- } catch {
289
- return localReject(opt, new ApiError(ok ? 3 : 4, ok ? "Unreadable" : statusText || status), status);
290
- }
291
- const apv = raw[info_default.name];
292
- if (apv) {
293
- body = raw;
294
- const diff = diffVersion(apv);
295
- const msg = `Detected @randajan/api-kit ${diff} version difference at '${opt.url}'. Server '${apv}' vs. client '${info_default.version}'`;
296
- if (diff === "major") {
297
- console.error(msg);
298
- }
299
- if (diff === "minor") {
300
- console.warn(msg);
301
- }
302
- } else {
303
- body = {};
304
- try {
305
- body.result = opt.parseBody(raw);
306
- } catch (err) {
307
- body.error = err.message || err;
308
- }
309
- }
310
- if (body.error) {
311
- const { code, message } = body.error;
312
- body.error = new ApiError(code || mrgStr(opt.code || 0, "1.0", "."), message || body.error);
313
- }
314
- body.ok = !body.error;
315
- body.http = status;
316
- if (opt.parseHeaders) {
317
- body.headers = Object.freeze(Object.fromEntries(headers.entries()));
318
- }
319
- return end(body, opt);
423
+ body = body || await remoteResolve(resp, opt);
424
+ } catch (err2) {
425
+ body = remoteReject(err2, opt);
426
+ }
427
+ const err = body.error;
428
+ body.isOk = !err;
429
+ body.isRemote = !!body.isRemote;
430
+ body.isApiKit = !!body.isApiKit;
431
+ body.statusCode = err?.httpStatusCode || resp?.status;
432
+ body.headers = parseHeaders(resp, opt);
433
+ body = end(body, opt);
434
+ return opt.resultOnly ? body.result : body;
320
435
  };
321
- var fetchResolve = (_fetch, url, opt, method) => {
322
- const prom = fetchExe(_fetch, prepareOpt(url, opt, method));
436
+ var executeFetch = (_fetch, url, opt, method) => {
437
+ opt = prepareOpt(url, opt, method);
438
+ const prom = fetchBody(_fetch, opt);
323
439
  if (opt.timeoutId) {
324
440
  prom.finally((_) => clearTimeout(opt.timeoutId));
325
441
  }
326
- if (!opt.abortable) {
442
+ if (!opt.isAbortable) {
327
443
  return prom;
328
444
  }
329
- return (0, import_props3.solid)(prom, "abort", (_) => opt.abortController?.abort(new ApiError(2, "Aborted")));
445
+ return (0, import_props4.solid)(prom, "abort", (_) => opt.abortController?.abort(abortError()));
330
446
  };
331
447
 
332
448
  // src/arc/class/Functionable.js
@@ -338,32 +454,7 @@ var Functionable = class extends Function {
338
454
  };
339
455
 
340
456
  // src/client/class/Fetch.js
341
- var import_props4 = require("@randajan/props");
342
-
343
- // src/client/tool.js
344
- var mrgOpt = (a, b) => {
345
- const opt = mrgObj(a, b);
346
- opt.body = mrgObj(a?.body, b?.body);
347
- opt.params = mrgObj(a?.params, b?.params);
348
- opt.headers = mrgObj(a?.headers, b?.headers, {});
349
- return opt;
350
- };
351
-
352
- // src/arc/opt.js
353
- var createTrait = (trait) => {
354
- if (!trait) {
355
- return (opt) => opt;
356
- }
357
- if (isFn(trait)) {
358
- return trait;
359
- }
360
- throw new Error("config.trait should be a function");
361
- };
362
- var configTrait = (config) => {
363
- config.trait = createTrait(config.trait);
364
- };
365
-
366
- // src/client/class/Fetch.js
457
+ var import_props5 = require("@randajan/props");
367
458
  var Fetch = class _Fetch extends Functionable {
368
459
  static create(config = {}) {
369
460
  return new _Fetch(config);
@@ -371,37 +462,36 @@ var Fetch = class _Fetch extends Functionable {
371
462
  constructor(config = {}) {
372
463
  const _fetch = config.fetch || globalThis.fetch;
373
464
  if (!_fetch) {
374
- throw new Error("Missing fetch function. Please provide it inconfig");
465
+ throw new Error("Missing fetch function. Please provide it in config");
375
466
  }
376
467
  configTrait(config);
377
- config.parseBody = createTrait(config.parseBody);
378
468
  delete config.fetch;
379
- super((url, opt, method) => fetchResolve(_fetch, url, mrgOpt(config, opt), method));
469
+ super((url, opt, method) => executeFetch(_fetch, url, mrgOpt(config, opt), method));
380
470
  Object.freeze(config);
381
- (0, import_props4.solid)(this, "config", config);
471
+ (0, import_props5.solid)(this, "config", config);
382
472
  }
383
473
  extend(config) {
384
474
  return new _Fetch(mrgOpt(this.config, config));
385
475
  }
386
- async get(url, opt) {
476
+ get(url, opt) {
387
477
  return this(url, opt, "GET");
388
478
  }
389
- async post(url, opt) {
479
+ post(url, opt) {
390
480
  return this(url, opt, "POST");
391
481
  }
392
- async put(url, opt) {
482
+ put(url, opt) {
393
483
  return this(url, opt, "PUT");
394
484
  }
395
- async delete(url, opt) {
485
+ delete(url, opt) {
396
486
  return this(url, opt, "DELETE");
397
487
  }
398
- async patch(url, opt) {
488
+ patch(url, opt) {
399
489
  return this(url, opt, "PATCH");
400
490
  }
401
- async head(url, opt) {
491
+ head(url, opt) {
402
492
  return this(url, opt, "HEAD");
403
493
  }
404
- async options(url, opt) {
494
+ options(url, opt) {
405
495
  return this(url, opt, "OPTIONS");
406
496
  }
407
497
  };