@randajan/api-kit 2.0.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.2.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" } };
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,44 +53,34 @@ 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, httpStatusCode = 400, remoteStack = void 0) {
59
- return new _ApiError(code, message, httpStatusCode, remoteStack);
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, httpStatusCode = 500) {
65
- if (_ApiError.is(any)) {
66
- return any;
67
- }
68
- const msg = toStr(any?.message || any) || "Unknown";
69
- const apierr = _ApiError.create(code, msg, httpStatusCode);
70
- return (0, import_props.solid)(apierr, "stack", any?.stack);
61
+ static code(code, message, opt = {}) {
62
+ opt.code = code;
63
+ return new _FetchError(message, opt);
71
64
  }
72
- constructor(code, message, httpStatusCode = 400, remoteStack = void 0) {
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
- (0, import_props.solid)(this, "httpStatusCode", httpStatusCode ?? 400, true);
76
- (0, import_props.solid)(this, "remoteStack", remoteStack);
77
69
  this.rise(code);
78
70
  }
79
- rise(code = 0) {
80
- this.code = code || 0;
81
- return this;
82
- }
83
- exposeStack(exposeStack = true) {
84
- (0, import_props.solid)(this, "_exposeStack", !!exposeStack, false, true);
71
+ rise(code) {
72
+ if (code == null) {
73
+ return this;
74
+ }
75
+ this.code = code;
85
76
  return this;
86
77
  }
87
78
  toJSON() {
88
- const { message, stack, _exposeStack } = this;
79
+ const { message, cause } = this;
89
80
  const body = { message, ...this };
90
- if (_exposeStack) {
91
- body.stack = stack;
92
- } else {
93
- delete body.stack;
81
+ if (cause) {
82
+ const { message: message2, stack } = cause;
83
+ body.cause = { message: message2, stack };
94
84
  }
95
85
  return body;
96
86
  }
@@ -123,30 +113,12 @@ var info = lockObject(define_slib_info_default);
123
113
  var info_default = info;
124
114
 
125
115
  // src/client/class/static.js
126
- var import_props3 = require("@randajan/props");
127
-
128
- // src/arc/url.js
129
- var _regexp = /^https?:\/\//i;
130
- var buildUrl = (inputUrl, params) => {
131
- if (!params) {
132
- return inputUrl;
133
- }
134
- const hasHost = _regexp.test(inputUrl);
135
- const url = new URL(inputUrl, hasHost ? void 0 : "http://thisisonlyplaceholder.xyz");
136
- url.hash = "";
137
- for (const [key, value] of Object.entries(params)) {
138
- if (value != null) {
139
- url.searchParams.append(key, value);
140
- }
141
- }
142
- ;
143
- return hasHost ? url.toString() : url.pathname + url.search;
144
- };
116
+ var import_props4 = require("@randajan/props");
145
117
 
146
- // src/arc/main.js
118
+ // src/arc/opt.js
147
119
  var import_props2 = require("@randajan/props");
148
120
  var start = (opt) => {
149
- if (opt.timestamp) {
121
+ if (opt.timetrack) {
150
122
  return opt.startAt = Date.now();
151
123
  }
152
124
  };
@@ -178,28 +150,61 @@ var end = (body, opt) => {
178
150
  if (isFn(opt.onError)) {
179
151
  opt.onError(body, opt);
180
152
  }
181
- if (opt.throwError || opt.resultOnly) {
153
+ if (opt.throwError) {
182
154
  throw body.error;
183
155
  }
184
156
  }
185
- return opt.resultOnly ? body.result : Object.freeze(body);
157
+ return Object.freeze(body);
186
158
  };
187
- var diffVersion = (version) => {
188
- const v1 = toStr(info.version);
189
- const v2 = toStr(version);
190
- if (v1 === v2) {
191
- return;
159
+ var createTrait = (trait) => {
160
+ if (!trait) {
161
+ return (opt) => opt;
192
162
  }
193
- const f1 = v1.split(".");
194
- const f2 = v2.split(".");
195
- if (f1[0] !== f2[0]) {
196
- return "major";
163
+ if (isFn(trait)) {
164
+ return trait;
197
165
  }
198
- if (f1[1] !== f2[1]) {
199
- 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;
200
195
  }
201
- if (f1[2] !== f2[2]) {
202
- 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}]`;
203
208
  }
204
209
  };
205
210
 
@@ -226,159 +231,218 @@ var decode2 = (body) => Object.fromEntries(new URLSearchParams(body));
226
231
  var encode2 = (body) => new URLSearchParams(body);
227
232
 
228
233
  // src/arc/types/index.js
229
- var types = { json: json_exports, form: form_exports };
230
- var getType = (typeName) => {
231
- 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) {
232
243
  return json_exports;
233
244
  }
234
- const type = types[typeName];
235
- 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) {
236
251
  return type;
237
252
  }
238
- 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;
239
260
  };
240
261
  var encodeBody = (opt) => {
241
- const type = getType(opt.requestType || opt.type);
262
+ const type = getType("requestType", "encode", opt);
242
263
  opt.headers["Content-Type"] = type.mime;
243
264
  opt.body = type.encode(opt.body);
244
265
  };
245
266
  var attachBodyDecoder = (opt) => {
246
- const type = getType(opt.responseType || opt.type);
267
+ const type = getType("responseType", "decode", opt);
247
268
  opt.headers["Accept"] = type.mime;
248
269
  opt.decodeBody = type.decode;
249
270
  };
250
271
 
251
- // src/client/class/static.js
252
- var prepareOpt = (url, opt, method) => {
253
- start(opt);
254
- opt.url = buildUrl(mrgStr(opt.url, url), opt.query);
255
- if (method) {
256
- opt.method = method;
272
+ // src/client/url.js
273
+ var _regexp = /^https?:\/\//i;
274
+ var buildUrl = (inputUrl, params) => {
275
+ if (!params) {
276
+ return inputUrl;
257
277
  }
258
- if (opt.body) {
259
- encodeBody(opt);
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
+ }
260
285
  }
261
- attachBodyDecoder(opt);
262
- const hasTimeout = opt.timeout > 0;
263
- if (opt.abortable || hasTimeout) {
264
- opt.abortController = new AbortController();
265
- opt.signal = opt.abortController.signal;
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;
266
296
  }
267
- if (hasTimeout) {
268
- opt.timeoutId = setTimeout(() => opt.abortController?.abort(ApiError.create(1, "Timeout", 408)), opt.timeout);
297
+ const f1 = v1.split(".");
298
+ const f2 = v2.split(".");
299
+ if (f1[0] !== f2[0]) {
300
+ return "major";
269
301
  }
270
- if (isFn(opt.trait)) {
271
- opt = opt.trait(opt) || opt;
302
+ if (f1[1] !== f2[1]) {
303
+ return "minor";
304
+ }
305
+ if (f1[2] !== f2[2]) {
306
+ return "patch";
272
307
  }
273
- return opt;
274
308
  };
275
- var localReject = (opt, error) => {
276
- const { httpStatusCode } = error;
277
- error.rise(0).rise(opt.code || 0);
278
- return end({ isOk: false, isRemote: false, isApiKit: false, statusCode: httpStatusCode, error }, opt);
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()));
279
320
  };
280
- var statusError = ({ status, statusText }) => ApiError.create(4, statusText || status, status);
281
- var unreadableError = (resp) => resp.ok ? ApiError.create(3, "Unreadable", 415) : statusError(resp);
282
- var foreignError = (resp) => ({ isApiKit: false, error: statusError(resp) });
283
321
  var readText = async (resp) => {
284
322
  try {
285
323
  return await resp.text();
286
- } catch {
287
- throw unreadableError(resp);
324
+ } catch (err) {
325
+ throw unreadableError(resp, err);
288
326
  }
289
327
  };
290
- var decodeText = (text, opt, resp) => {
328
+ var decodeText = (resp, opt, text) => {
291
329
  try {
292
330
  return opt.decodeBody(text);
293
- } catch {
294
- throw unreadableError(resp);
331
+ } catch (err) {
332
+ throw undecodableError(resp, err, text);
295
333
  }
296
334
  };
297
- var resolveEmpty = (resp) => {
298
- if (!resp.ok && resp.status !== 304) {
299
- return foreignError(resp);
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
+ }
300
349
  }
301
- return { result: null, isApiKit: false };
350
+ return err.rise(0).rise(0);
302
351
  };
303
- var resolveApiKit = (raw, opt) => {
304
- const apv = raw[info_default.name];
305
- const diff = diffVersion(apv);
306
- const msg = `Detected @randajan/api-kit ${diff} version difference at '${opt.url}'. Server '${apv}' vs. client '${info_default.version}'`;
307
- if (diff === "major") {
308
- console.error(msg);
309
- }
310
- if (diff === "minor") {
311
- console.warn(msg);
312
- }
313
- raw.isApiKit = true;
314
- return raw;
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 });
315
358
  };
316
- var resolveForeign = (raw, opt, resp) => {
317
- if (!resp.ok) {
318
- return foreignError(resp);
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
+ };
366
+ var prepareOpt = (url, opt, method) => {
367
+ start(opt);
368
+ opt.url = buildUrl(mrgStr(opt.url, url), opt.params);
369
+ if (method) {
370
+ opt.method = method;
319
371
  }
320
- const body = { isApiKit: false };
321
- try {
322
- body.result = opt.parseBody(raw);
323
- } catch (err) {
324
- body.error = err.message || err;
372
+ if (opt.body) {
373
+ encodeBody(opt);
325
374
  }
326
- return body;
327
- };
328
- var resolveDecoded = (raw, opt, resp) => {
329
- if (raw?.[info_default.name]) {
330
- return resolveApiKit(raw, opt);
375
+ attachBodyDecoder(opt);
376
+ const hasTimeout = opt.timeout > 0;
377
+ if (opt.isAbortable || hasTimeout) {
378
+ opt.abortController = new AbortController();
379
+ opt.signal = opt.abortController.signal;
331
380
  }
332
- return resolveForeign(raw, opt, resp);
333
- };
334
- var normalizeError = (body, opt, resp) => {
335
- if (!body.error || ApiError.is(body.error)) {
336
- return;
381
+ if (hasTimeout) {
382
+ opt.timeoutId = setTimeout(() => opt.abortController?.abort(timeoutError()), opt.timeout);
383
+ }
384
+ if (isFn(opt.trait)) {
385
+ opt = opt.trait(opt) || opt;
337
386
  }
338
- const { code, message, stack, httpStatusCode } = body.error;
339
- body.error = ApiError.create(
340
- code || mrgStr(opt.code || 0, "1.0", "."),
341
- message || body.error,
342
- httpStatusCode ?? body.statusCode ?? resp.status,
343
- stack
344
- );
387
+ return opt;
345
388
  };
346
- var remoteResolve = (body, opt, resp) => {
347
- normalizeError(body, opt, resp);
348
- body.isOk = !body.error;
349
- body.isRemote = true;
350
- body.isApiKit = !!body.isApiKit;
351
- body.statusCode = body.statusCode ?? body.error?.httpStatusCode ?? resp.status;
352
- if (opt.parseHeaders) {
353
- body.headers = Object.freeze(Object.fromEntries(resp.headers.entries()));
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);
354
408
  }
355
- return end(body, opt);
409
+ body.isRemote = true;
410
+ body.isApiKit = true;
411
+ body.apiKitDiff = diffVersion(ver);
412
+ body.error = reconstructApiError(body.error);
413
+ return body;
356
414
  };
357
- var remoteReject = (opt, resp, error) => remoteResolve({ isApiKit: false, error }, opt, resp);
358
- var fetchExe = async (_fetch, opt) => {
415
+ var fetchBody = async (_fetch, opt) => {
359
416
  let resp, body;
360
417
  try {
361
418
  resp = await _fetch(opt.url, opt);
362
- } catch (err) {
363
- return localReject(opt, ApiError.is(err) ? err : ApiError.to(0, "Failed", 503));
419
+ } catch (err2) {
420
+ body = localReject(err2, opt);
364
421
  }
365
422
  try {
366
- const text = await readText(resp);
367
- body = text ? resolveDecoded(decodeText(text, opt, resp), opt, resp) : resolveEmpty(resp);
368
- } catch (err) {
369
- return remoteReject(opt, resp, ApiError.is(err) ? err : ApiError.to(0, err, 500));
423
+ body = body || await remoteResolve(resp, opt);
424
+ } catch (err2) {
425
+ body = remoteReject(err2, opt);
370
426
  }
371
- return remoteResolve(body, opt, resp);
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;
372
435
  };
373
- var fetchResolve = (_fetch, url, opt, method) => {
374
- 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);
375
439
  if (opt.timeoutId) {
376
440
  prom.finally((_) => clearTimeout(opt.timeoutId));
377
441
  }
378
- if (!opt.abortable) {
442
+ if (!opt.isAbortable) {
379
443
  return prom;
380
444
  }
381
- return (0, import_props3.solid)(prom, "abort", (_) => opt.abortController?.abort(ApiError.create(2, "Aborted", 499)));
445
+ return (0, import_props4.solid)(prom, "abort", (_) => opt.abortController?.abort(abortError()));
382
446
  };
383
447
 
384
448
  // src/arc/class/Functionable.js
@@ -390,33 +454,7 @@ var Functionable = class extends Function {
390
454
  };
391
455
 
392
456
  // src/client/class/Fetch.js
393
- var import_props4 = require("@randajan/props");
394
-
395
- // src/client/tool.js
396
- var mrgQuery = (opt) => mrgObj(opt?.params, opt?.query);
397
- var mrgOpt = (a, b) => {
398
- const opt = mrgObj(a, b);
399
- opt.body = mrgObj(a?.body, b?.body);
400
- opt.query = mrgObj(mrgQuery(a), mrgQuery(b));
401
- opt.headers = mrgObj(a?.headers, b?.headers, {});
402
- return opt;
403
- };
404
-
405
- // src/arc/opt.js
406
- var createTrait = (trait) => {
407
- if (!trait) {
408
- return (opt) => opt;
409
- }
410
- if (isFn(trait)) {
411
- return trait;
412
- }
413
- throw new Error("config.trait should be a function");
414
- };
415
- var configTrait = (config) => {
416
- config.trait = createTrait(config.trait);
417
- };
418
-
419
- // src/client/class/Fetch.js
457
+ var import_props5 = require("@randajan/props");
420
458
  var Fetch = class _Fetch extends Functionable {
421
459
  static create(config = {}) {
422
460
  return new _Fetch(config);
@@ -424,14 +462,13 @@ var Fetch = class _Fetch extends Functionable {
424
462
  constructor(config = {}) {
425
463
  const _fetch = config.fetch || globalThis.fetch;
426
464
  if (!_fetch) {
427
- throw new Error("Missing fetch function. Please provide it inconfig");
465
+ throw new Error("Missing fetch function. Please provide it in config");
428
466
  }
429
467
  configTrait(config);
430
- config.parseBody = createTrait(config.parseBody);
431
468
  delete config.fetch;
432
- super((url, opt, method) => fetchResolve(_fetch, url, mrgOpt(config, opt), method));
469
+ super((url, opt, method) => executeFetch(_fetch, url, mrgOpt(config, opt), method));
433
470
  Object.freeze(config);
434
- (0, import_props4.solid)(this, "config", config);
471
+ (0, import_props5.solid)(this, "config", config);
435
472
  }
436
473
  extend(config) {
437
474
  return new _Fetch(mrgOpt(this.config, config));