lissa 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/lib/core/defaults.js +2 -0
- package/lib/core/request.js +34 -10
- package/lib/index.d.ts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,6 +135,8 @@ Checkout https://developer.mozilla.org/en-US/docs/Web/API/RequestInit for availa
|
|
|
135
135
|
| paramsSerializer | "simple", "extended" or Function | "simple" | How to serialize the query params
|
|
136
136
|
| urlBuilder | "simple", "extended" or Function | "simple" | How to build the final fetch url from the defined "baseURL" and "url"
|
|
137
137
|
| responseType | "json", "text", "file" or "raw" | "json" | The type of data that the server will respond with
|
|
138
|
+
| replacer | Function or Array | undefined | A function or array to transform values during JSON.stringify
|
|
139
|
+
| reviver | Function | undefined | A function to transform values during JSON.parse
|
|
138
140
|
| timeout | number | undefined | Specify the number of milliseconds before the request gets aborted
|
|
139
141
|
| signal | AbortSignal | undefined | Cancel/Abort running requests
|
|
140
142
|
| body | object, buffer, stream, file, etc. | undefined | Request body
|
|
@@ -150,6 +152,41 @@ Set the urlBuilder option to "simple", "extended" or a custom build function.
|
|
|
150
152
|
|
|
151
153
|
Make sure to not forget a needed slash using "simple". If using "extended" be careful with leading and trailing slashes in urls, the baseURL and also with sub paths in the baseURL. For example `new URL("todos", "http://api.example.com/v2")` and `new URL("/todos", "http://api.example.com/v2/")` both results in a fetch to `"http://api.example.com/todos"`. Only `new URL("todos", "http://api.example.com/v2/")` will result in a fetch to the expected `"http://api.example.com/v2/todos"`.
|
|
152
154
|
|
|
155
|
+
#### replacer option
|
|
156
|
+
The replacer option allows you to control how values are stringified when sending JSON data. It works exactly like the second parameter of JSON.stringify(). You can provide either a function to transform values or an array to filter properties.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
// Using a function to filter out private properties
|
|
160
|
+
const lissa = Lissa.create({
|
|
161
|
+
baseURL: "https://api.example.com",
|
|
162
|
+
replacer: (key, value) => {
|
|
163
|
+
if (key.startsWith('_')) return undefined; // Exclude private properties
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### reviver option
|
|
170
|
+
The reviver option allows you to transform values when parsing JSON responses. It works exactly like the second parameter of JSON.parse(). This is particularly useful for converting ISO date strings back into Date objects or handling other custom data types.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
// Using a reviver to convert ISO date strings to Date objects
|
|
174
|
+
const lissa = Lissa.create({
|
|
175
|
+
baseURL: "https://api.example.com",
|
|
176
|
+
reviver: (key, value) => {
|
|
177
|
+
// Check if value is an ISO 8601 date string
|
|
178
|
+
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value)) {
|
|
179
|
+
return new Date(value);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const { data } = await lissa.get("/events");
|
|
186
|
+
// data.createdAt is now a Date object instead of a string
|
|
187
|
+
console.log(data.createdAt instanceof Date); // true
|
|
188
|
+
```
|
|
189
|
+
|
|
153
190
|
### `result` Object/Error
|
|
154
191
|
|
|
155
192
|
Every request returns a promise which gets fulfilled into a result object or rejected into a result error.
|
|
@@ -682,6 +719,69 @@ publicClient.use(Lissa.dedupe());
|
|
|
682
719
|
|
|
683
720
|
See the `examples/` directory for more usage examples, including browser-specific code and advanced plugin usage.
|
|
684
721
|
|
|
722
|
+
### Using Reviver to Convert Date Strings
|
|
723
|
+
|
|
724
|
+
A common use case for the reviver function is to automatically convert ISO date strings back into JavaScript Date objects when receiving API responses.
|
|
725
|
+
|
|
726
|
+
```js
|
|
727
|
+
import Lissa from "lissa";
|
|
728
|
+
|
|
729
|
+
// Create a reviver function that detects ISO 8601 date strings
|
|
730
|
+
function dateReviver(key, value) {
|
|
731
|
+
// Pattern to match ISO 8601 date strings like "2026-07-01T12:34:56.789Z"
|
|
732
|
+
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
733
|
+
|
|
734
|
+
if (typeof value === 'string' && isoDatePattern.test(value)) {
|
|
735
|
+
return new Date(value);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Create a Lissa instance with the reviver
|
|
742
|
+
const lissa = Lissa.create({
|
|
743
|
+
baseURL: "https://api.example.com",
|
|
744
|
+
reviver: dateReviver
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// Example API response that contains date strings:
|
|
748
|
+
// {
|
|
749
|
+
// "id": 123,
|
|
750
|
+
// "title": "Meeting",
|
|
751
|
+
// "createdAt": "2026-07-01T10:00:00.000Z",
|
|
752
|
+
// "updatedAt": "2026-07-01T15:30:00.000Z",
|
|
753
|
+
// "participants": [
|
|
754
|
+
// { "name": "Alice", "joinedAt": "2026-07-01T10:05:00.000Z" },
|
|
755
|
+
// { "name": "Bob", "joinedAt": "2026-07-01T10:10:00.000Z" }
|
|
756
|
+
// ]
|
|
757
|
+
// }
|
|
758
|
+
|
|
759
|
+
const { data: meeting } = await lissa.get("/meetings/123");
|
|
760
|
+
|
|
761
|
+
// All date strings are automatically converted to Date objects
|
|
762
|
+
console.log(meeting.createdAt instanceof Date); // true
|
|
763
|
+
console.log(meeting.updatedAt instanceof Date); // true
|
|
764
|
+
console.log(meeting.createdAt.toLocaleDateString()); // "7/1/2026"
|
|
765
|
+
|
|
766
|
+
// Works with nested objects too
|
|
767
|
+
console.log(meeting.participants[0].joinedAt instanceof Date); // true
|
|
768
|
+
|
|
769
|
+
// You can now use Date methods directly
|
|
770
|
+
const duration = meeting.updatedAt - meeting.createdAt;
|
|
771
|
+
console.log(`Meeting lasted ${duration / 1000 / 60} minutes`);
|
|
772
|
+
|
|
773
|
+
// The reviver can also be set per-request
|
|
774
|
+
const { data: event } = await lissa.get("/events/456", {
|
|
775
|
+
reviver: (key, value) => {
|
|
776
|
+
// Custom reviver for this specific request
|
|
777
|
+
if (key === "startDate" || key === "endDate") {
|
|
778
|
+
return new Date(value);
|
|
779
|
+
}
|
|
780
|
+
return value;
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
```
|
|
784
|
+
|
|
685
785
|
## Browser Support
|
|
686
786
|
|
|
687
787
|
Lissa works in all modern browsers that support the following APIs:
|
package/lib/core/defaults.js
CHANGED
package/lib/core/request.js
CHANGED
|
@@ -72,6 +72,14 @@ export default class LissaRequest extends OpenPromise {
|
|
|
72
72
|
return this.#addOptions({ body });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
reviver(reviver) {
|
|
76
|
+
return this.#addOptions({ reviver });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
replacer(replacer) {
|
|
80
|
+
return this.#addOptions({ replacer });
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
timeout(timeout) {
|
|
76
84
|
return this.#addOptions({ timeout });
|
|
77
85
|
}
|
|
@@ -95,7 +103,7 @@ export default class LissaRequest extends OpenPromise {
|
|
|
95
103
|
#addOptions(options) {
|
|
96
104
|
if (this.#locked) {
|
|
97
105
|
console.warn(new Error('Request options cannot be changed anymore after execution start!'));
|
|
98
|
-
return;
|
|
106
|
+
return this;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
if (options.headers) options.headers = new Headers(options.headers);
|
|
@@ -162,13 +170,15 @@ export default class LissaRequest extends OpenPromise {
|
|
|
162
170
|
options.headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
|
163
171
|
}
|
|
164
172
|
else if (options.body.constructor === Object) {
|
|
165
|
-
options.body = JSON.stringify(options.body);
|
|
173
|
+
options.body = JSON.stringify(options.body, options.replacer);
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
else {
|
|
169
177
|
options.headers.delete('Content-Type');
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
delete options.replacer;
|
|
181
|
+
|
|
172
182
|
if (options.timeout) {
|
|
173
183
|
if (options.signal) {
|
|
174
184
|
options.signal = AbortSignal.any([
|
|
@@ -183,8 +193,8 @@ export default class LissaRequest extends OpenPromise {
|
|
|
183
193
|
delete options.timeout;
|
|
184
194
|
}
|
|
185
195
|
|
|
186
|
-
let baseURL, url, params, urlBuilder, adapter;
|
|
187
|
-
({ baseURL, url, params, urlBuilder, adapter, ...options } = options);
|
|
196
|
+
let baseURL, url, params, urlBuilder, adapter, reviver;
|
|
197
|
+
({ baseURL, url, params, urlBuilder, adapter, reviver, ...options } = options);
|
|
188
198
|
|
|
189
199
|
const fetchUrl = ((
|
|
190
200
|
baseURL,
|
|
@@ -204,8 +214,8 @@ export default class LissaRequest extends OpenPromise {
|
|
|
204
214
|
|
|
205
215
|
// Actual fetch
|
|
206
216
|
let returnValue = adapter === 'xhr'
|
|
207
|
-
? await this.#executeXhr({ url: new URL(fetchUrl), ...options })
|
|
208
|
-
: await this.#executeFetch({ url: new URL(fetchUrl), ...options });
|
|
217
|
+
? await this.#executeXhr({ url: new URL(fetchUrl), reviver, ...options })
|
|
218
|
+
: await this.#executeFetch({ url: new URL(fetchUrl), reviver, ...options });
|
|
209
219
|
|
|
210
220
|
returnValue.options = $options;
|
|
211
221
|
|
|
@@ -228,7 +238,7 @@ export default class LissaRequest extends OpenPromise {
|
|
|
228
238
|
return returnValue;
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
async #executeFetch({ url, responseType, onUploadProgress, onDownloadProgress, ...options }) {
|
|
241
|
+
async #executeFetch({ url, responseType, onUploadProgress, onDownloadProgress, reviver, ...options }) {
|
|
232
242
|
let request = { url, options };
|
|
233
243
|
|
|
234
244
|
for (const hook of this.#lissa.beforeFetchHooks) {
|
|
@@ -297,7 +307,7 @@ export default class LissaRequest extends OpenPromise {
|
|
|
297
307
|
if (!data) break;
|
|
298
308
|
|
|
299
309
|
try {
|
|
300
|
-
data = JSON.parse(data);
|
|
310
|
+
data = JSON.parse(data, reviver);
|
|
301
311
|
}
|
|
302
312
|
catch (error) {
|
|
303
313
|
data = null;
|
|
@@ -326,7 +336,7 @@ export default class LissaRequest extends OpenPromise {
|
|
|
326
336
|
return returnValue;
|
|
327
337
|
}
|
|
328
338
|
|
|
329
|
-
async #executeXhr({ url, ...options }) {
|
|
339
|
+
async #executeXhr({ url, reviver, ...options }) {
|
|
330
340
|
let request = { url, options };
|
|
331
341
|
|
|
332
342
|
for (const hook of this.#lissa.beforeFetchHooks) {
|
|
@@ -377,7 +387,11 @@ export default class LissaRequest extends OpenPromise {
|
|
|
377
387
|
|
|
378
388
|
if (options.signal) options.signal.addEventListener('abort', () => xhr.abort());
|
|
379
389
|
|
|
380
|
-
xhr.responseType = options.responseType === '
|
|
390
|
+
xhr.responseType = options.responseType === 'json'
|
|
391
|
+
? 'text'
|
|
392
|
+
: options.responseType === 'raw' || options.responseType === 'file'
|
|
393
|
+
? 'blob'
|
|
394
|
+
: options.responseType;
|
|
381
395
|
|
|
382
396
|
if (options.onUploadProgress) {
|
|
383
397
|
xhr.upload.addEventListener('progress', (evt) => {
|
|
@@ -404,6 +418,16 @@ export default class LissaRequest extends OpenPromise {
|
|
|
404
418
|
returnValue.status = response.status;
|
|
405
419
|
returnValue.data = response.body;
|
|
406
420
|
|
|
421
|
+
if (options.responseType === 'json' && response.body) {
|
|
422
|
+
try {
|
|
423
|
+
returnValue.data = JSON.parse(response.body, reviver);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
returnValue.data = null;
|
|
427
|
+
console.error(error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
407
431
|
if (options.responseType === 'file') {
|
|
408
432
|
const type = response.headers.get('Content-Type');
|
|
409
433
|
const filename = getFilenameFromContentDisposition(response.headers.get('Content-Disposition'));
|
package/lib/index.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type LissaOptionsInit = Omit<RequestInit, 'method' | 'body'> & {
|
|
|
53
53
|
paramsSerializer?: 'simple' | 'extended' | ((params: Params) => string);
|
|
54
54
|
urlBuilder?: 'simple' | 'extended' | ((url: string, baseURL: string) => string | URL);
|
|
55
55
|
responseType?: 'json' | 'text' | 'file' | 'raw';
|
|
56
|
+
replacer?: (this: any, key: string, value: any) => any | (string | number)[];
|
|
57
|
+
reviver?: (this: any, key: string, value: any) => any;
|
|
56
58
|
timeout?: number;
|
|
57
59
|
onUploadProgress?: (uploaded: number, total: number) => void;
|
|
58
60
|
onDownloadProgress?: (downloaded: number, total: number) => void;
|
|
@@ -193,6 +195,20 @@ export declare class LissaRequest<RT = ResultValue<ResultData>> extends Promise<
|
|
|
193
195
|
*/
|
|
194
196
|
body(body: BodyInit | JsonStringifyableObject): LissaRequest<RT>;
|
|
195
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Set a replacer function or array to transform values during JSON.stringify
|
|
200
|
+
*
|
|
201
|
+
* Works like the second parameter of JSON.stringify()
|
|
202
|
+
*/
|
|
203
|
+
replacer(replacer: (this: any, key: string, value: any) => any | (string | number)[]): LissaRequest<RT>;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Set a reviver function to transform values during JSON.parse
|
|
207
|
+
*
|
|
208
|
+
* Works like the second parameter of JSON.parse()
|
|
209
|
+
*/
|
|
210
|
+
reviver(reviver: (this: any, key: string, value: any) => any): LissaRequest<RT>;
|
|
211
|
+
|
|
196
212
|
/**
|
|
197
213
|
* Set request timeout in milliseconds
|
|
198
214
|
*
|
package/package.json
CHANGED