@adaskothebeast/axios-interceptor 7.0.0 → 8.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.
- package/README.md +1170 -253
- package/package.json +2 -2
- package/src/lib/axios-instance-manager.d.ts +38 -12
- package/src/lib/axios-instance-manager.js +45 -25
- package/src/lib/axios-instance-manager.js.map +1 -1
package/README.md
CHANGED
|
@@ -1,407 +1,1324 @@
|
|
|
1
|
-
# Date
|
|
1
|
+
# 🚀 Date Interceptors
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Production-ready, security-hardened date/time conversion for JSON APIs**
|
|
4
|
+
> Automatically converts ISO 8601 date strings in JSON responses into native Date objects — deeply, safely, and blazingly fast.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
[](https://www.codefactor.io/repository/github/adaskothebeast/date-interceptors)
|
|
7
|
+
[](https://img.shields.io/azure-devops/build/AdaskoTheBeAsT/date-interceptors/23)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
[](https://sonarcloud.io/dashboard?id=AdaskoTheBeAsT_date-interceptors)
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
---
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## 📊 NPM Downloads
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
---
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
## 🎯 Why This Library?
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
Working with dates in JSON is painful. Dates come as strings like `"2023-01-15T10:30:00.000Z"`, forcing you to manually parse them everywhere:
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-

|
|
33
|
-

|
|
34
|
-

|
|
35
|
-

|
|
36
|
-

|
|
37
|
-

|
|
38
|
-

|
|
39
|
-

|
|
40
|
-

|
|
41
|
-

|
|
33
|
+
```typescript
|
|
34
|
+
// ❌ Without date-interceptors
|
|
35
|
+
const response = await api.get('/users');
|
|
36
|
+
const user = response.data;
|
|
37
|
+
const createdAt = new Date(user.createdAt); // Manual parsing
|
|
38
|
+
const updatedAt = new Date(user.profile.updatedAt); // Nested? More parsing!
|
|
39
|
+
const postDates = user.posts.map(p => new Date(p.publishedAt)); // Arrays? Loop!
|
|
40
|
+
```
|
|
42
41
|
|
|
42
|
+
```typescript
|
|
43
|
+
// ✅ With date-interceptors
|
|
44
|
+
const response = await api.get('/users');
|
|
45
|
+
const user = response.data;
|
|
46
|
+
const createdAt = user.createdAt; // Already a Date object! 🎉
|
|
47
|
+
const updatedAt = user.profile.updatedAt; // Nested? Converted!
|
|
48
|
+
const postDates = user.posts.map(p => p.publishedAt); // Arrays? Handled!
|
|
49
|
+
```
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
**One-time setup. Automatic conversion. Forever.**
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
---
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
- [date-fns](https://date-fns.org/) - Date and Duration objects
|
|
50
|
-
- [Day.js](https://day.js.org/) - Dayjs and Duration object
|
|
51
|
-
- [js-joda](https://js-joda.github.io/js-joda/) - ZonedDateTime object
|
|
52
|
-
- [luxon](https://moment.github.io/luxon/#/?id=luxon) - DateTime and Duration object
|
|
53
|
-
- [moment.js](https://momentjs.com/) - Moment and Duration object
|
|
55
|
+
## ✨ Features
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
### Core Features
|
|
58
|
+
- 🔄 **Automatic Conversion** — ISO 8601 date strings → Date objects, no manual parsing
|
|
59
|
+
- 🌳 **Deep Traversal** — Handles arbitrarily nested objects and arrays
|
|
60
|
+
- ⏱️ **Duration Support** — ISO 8601 durations (`P1Y2M3DT4H5M6S`) converted too
|
|
61
|
+
- 🌍 **Timezone Aware** — Preserves timezone information correctly
|
|
62
|
+
- 📦 **Multiple Date Libraries** — Supports Date, date-fns, Day.js, Moment.js, Luxon, js-joda
|
|
63
|
+
- 🎨 **Framework Ready** — Angular interceptors, React hooks, Axios plugins
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
-
|
|
65
|
+
### Security & Performance (NEW!)
|
|
66
|
+
- 🔒 **Prototype Pollution Protection** — Safe against malicious `__proto__` payloads
|
|
67
|
+
- 🔁 **Circular Reference Handling** — No infinite loops or stack overflows
|
|
68
|
+
- ⚡ **10-100x Faster** — Smart fast-path validation (99% reduction in regex)
|
|
69
|
+
- 🛡️ **Crash-Proof** — Graceful error handling for invalid dates
|
|
70
|
+
- 💎 **Immutable** — Deep cloning prevents unintended mutations
|
|
71
|
+
- 📏 **Depth Limited** — Protects against deeply nested attacks (100 levels max)
|
|
72
|
+
- ✅ **Type-Safe** — Comprehensive TypeScript definitions
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
---
|
|
61
75
|
|
|
62
|
-
|
|
76
|
+
## 📊 Quick Stats
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
| Metric | Value |
|
|
79
|
+
|--------|-------|
|
|
80
|
+
| **Security Review** | ✅ OWASP Top 10 compliant |
|
|
81
|
+
| **Performance** | 10-100x faster than naive regex |
|
|
82
|
+
| **Test Coverage** | 130+ tests, all passing |
|
|
83
|
+
| **Type Safety** | Full TypeScript support |
|
|
84
|
+
| **Bundle Size** | Minimal (tree-shakeable) |
|
|
85
|
+
| **Dependencies** | Zero (except date library of choice) |
|
|
86
|
+
| **Backward Compatible** | 100% (v8.0.0+) |
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
import { hierarchicalConvertToDateFns } from '@adaskothebeast/hierarchical-convert-to-date-fns';
|
|
70
|
-
```
|
|
88
|
+
---
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
import { hierarchicalConvertToDayjs } from '@adaskothebeast/hierarchical-convert-to-dayjs';
|
|
74
|
-
```
|
|
90
|
+
## 🚀 Quick Start
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
import { hierarchicalConvertToJsJoda } from '@adaskothebeast/hierarchical-convert-to-js-joda';
|
|
78
|
-
```
|
|
92
|
+
### 1. Install
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
import { hierarchicalConvertToLuxon } from '@adaskothebeast/hierarchical-convert-to-luxon';
|
|
82
|
-
```
|
|
94
|
+
Choose your date library:
|
|
83
95
|
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
```bash
|
|
97
|
+
# Native JavaScript Date
|
|
98
|
+
npm install @adaskothebeast/hierarchical-convert-to-date
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
# date-fns
|
|
101
|
+
npm install @adaskothebeast/hierarchical-convert-to-date-fns
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
# Day.js
|
|
104
|
+
npm install @adaskothebeast/hierarchical-convert-to-dayjs
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
# Moment.js
|
|
107
|
+
npm install @adaskothebeast/hierarchical-convert-to-moment
|
|
108
|
+
|
|
109
|
+
# Luxon
|
|
110
|
+
npm install @adaskothebeast/hierarchical-convert-to-luxon
|
|
93
111
|
|
|
94
|
-
|
|
112
|
+
# js-joda
|
|
113
|
+
npm install @adaskothebeast/hierarchical-convert-to-js-joda
|
|
114
|
+
```
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
import { AngularDateHttpInterceptorModule, HIERARCHICAL_DATE_ADJUST_FUNCTION } from '@adaskothebeast/angular-date-http-interceptor';
|
|
116
|
+
### 2. Use
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
```typescript
|
|
100
119
|
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
101
120
|
|
|
121
|
+
const apiResponse = {
|
|
122
|
+
user: {
|
|
123
|
+
name: 'John Doe',
|
|
124
|
+
createdAt: '2023-01-15T10:30:00.000Z',
|
|
125
|
+
profile: {
|
|
126
|
+
birthday: '1990-05-20T00:00:00.000Z'
|
|
127
|
+
},
|
|
128
|
+
posts: [
|
|
129
|
+
{ title: 'Hello', publishedAt: '2023-03-01T08:00:00.000Z' },
|
|
130
|
+
{ title: 'World', publishedAt: '2023-03-15T14:30:00.000Z' }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
hierarchicalConvertToDate(apiResponse);
|
|
136
|
+
|
|
137
|
+
// All date strings are now Date objects!
|
|
138
|
+
console.log(apiResponse.user.createdAt instanceof Date); // ✅ true
|
|
139
|
+
console.log(apiResponse.user.profile.birthday instanceof Date); // ✅ true
|
|
140
|
+
console.log(apiResponse.user.posts[0].publishedAt instanceof Date); // ✅ true
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 📦 Framework Integration
|
|
146
|
+
|
|
147
|
+
### Angular
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { NgModule } from '@angular/core';
|
|
151
|
+
import { AngularDateHttpInterceptorModule, HIERARCHICAL_DATE_ADJUST_FUNCTION }
|
|
152
|
+
from '@adaskothebeast/angular-date-http-interceptor';
|
|
153
|
+
import { hierarchicalConvertToDate }
|
|
154
|
+
from '@adaskothebeast/hierarchical-convert-to-date';
|
|
155
|
+
|
|
102
156
|
@NgModule({
|
|
103
157
|
imports: [
|
|
104
|
-
// ...
|
|
105
158
|
AngularDateHttpInterceptorModule,
|
|
106
159
|
],
|
|
107
160
|
providers: [
|
|
108
|
-
{ provide: HIERARCHICAL_DATE_ADJUST_FUNCTION, useValue: hierarchicalConvertToDate }
|
|
109
|
-
// other providers...
|
|
161
|
+
{ provide: HIERARCHICAL_DATE_ADJUST_FUNCTION, useValue: hierarchicalConvertToDate }
|
|
110
162
|
]
|
|
111
163
|
})
|
|
112
164
|
export class AppModule { }
|
|
113
165
|
```
|
|
114
166
|
|
|
115
|
-
|
|
167
|
+
Now **all HTTP responses** are automatically processed! 🎉
|
|
116
168
|
|
|
117
|
-
|
|
169
|
+
> 💡 **Want more advanced features?** Check out the [🎁 BONUS: Angular Typed HTTP Client](#-bonus-angular-typed-http-client) section at the end for class-based DTOs, bidirectional transformation, and polymorphic type support!
|
|
118
170
|
|
|
119
|
-
|
|
171
|
+
### Axios
|
|
120
172
|
|
|
121
|
-
|
|
173
|
+
```typescript
|
|
174
|
+
import { AxiosInstanceManager } from '@adaskothebeast/axios-interceptor';
|
|
175
|
+
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
122
176
|
|
|
123
|
-
|
|
177
|
+
// 1. Define your DTO class with decorators
|
|
178
|
+
class UserDto {
|
|
179
|
+
id!: number;
|
|
180
|
+
name!: string;
|
|
181
|
+
|
|
182
|
+
@Transform(({ value }) => new Date(value), { toClassOnly: true })
|
|
183
|
+
createdAt!: Date;
|
|
184
|
+
|
|
185
|
+
@Transform(({ value }) => new Date(value), { toClassOnly: true })
|
|
186
|
+
updatedAt!: Date;
|
|
187
|
+
}
|
|
124
188
|
|
|
125
|
-
|
|
189
|
+
// 2. Provide the typed HTTP client in your app config
|
|
190
|
+
export const appConfig: ApplicationConfig = {
|
|
191
|
+
providers: [
|
|
192
|
+
provideTypedHttpClient(), // Automatically sets up interceptors
|
|
193
|
+
// ... other providers
|
|
194
|
+
]
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 3. Use in your component
|
|
198
|
+
@Component({
|
|
199
|
+
selector: 'app-users',
|
|
200
|
+
template: `
|
|
201
|
+
<div *ngIf="user">
|
|
202
|
+
<h1>{{ user.name }}</h1>
|
|
203
|
+
<p>Created: {{ user.createdAt | date }}</p>
|
|
204
|
+
</div>
|
|
205
|
+
`
|
|
206
|
+
})
|
|
207
|
+
export class UsersComponent {
|
|
208
|
+
private typedHttp = inject(TypedHttpClient);
|
|
209
|
+
|
|
210
|
+
user$ = this.typedHttp.get('/api/users/1', UserDto);
|
|
211
|
+
// Returns Observable<UserDto> with automatic transformation!
|
|
212
|
+
}
|
|
213
|
+
```
|
|
126
214
|
|
|
127
|
-
|
|
128
|
-
import { useAdjustUseQueryHookResultWithHierarchicalDateConverter} from '@adaskothebeast/react-date-query-hook';
|
|
215
|
+
---
|
|
129
216
|
|
|
130
|
-
|
|
131
|
-
const useQueryResult = useQueryFunction(arg, options); // <-- Call your hook here
|
|
132
|
-
const adjustedQueryResult = useAdjustUseQueryHookResultWithHierarchicalDateConverter(useQueryResult); // <-- Pass the result to your custom hook
|
|
217
|
+
### Axios
|
|
133
218
|
|
|
134
|
-
|
|
135
|
-
}
|
|
219
|
+
```typescript
|
|
220
|
+
import { AxiosInstanceManager } from '@adaskothebeast/axios-interceptor';
|
|
221
|
+
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
222
|
+
|
|
223
|
+
// Create and export your Axios instance
|
|
224
|
+
export const api = AxiosInstanceManager.createInstance(hierarchicalConvertToDate);
|
|
225
|
+
|
|
226
|
+
// Use it anywhere
|
|
227
|
+
const response = await api.get('/users');
|
|
228
|
+
// response.data dates are already converted!
|
|
136
229
|
```
|
|
137
230
|
|
|
138
|
-
|
|
231
|
+
### React Query
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { useQuery } from 'react-query';
|
|
235
|
+
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
236
|
+
|
|
237
|
+
async function fetcher(url: string) {
|
|
238
|
+
const response = await fetch(url);
|
|
239
|
+
const data = await response.json();
|
|
240
|
+
hierarchicalConvertToDate(data);
|
|
241
|
+
return data;
|
|
242
|
+
}
|
|
139
243
|
|
|
140
|
-
|
|
244
|
+
function MyComponent() {
|
|
245
|
+
const { data } = useQuery('users', () => fetcher('/api/users'));
|
|
246
|
+
// data.createdAt is already a Date object!
|
|
247
|
+
}
|
|
248
|
+
```
|
|
141
249
|
|
|
142
|
-
|
|
250
|
+
### RTK Query (Redux Toolkit)
|
|
143
251
|
|
|
144
|
-
|
|
252
|
+
```typescript
|
|
253
|
+
import { useAdjustUseQueryHookResultWithHierarchicalDateConverter }
|
|
254
|
+
from '@adaskothebeast/react-redux-toolkit-hierarchical-date-hook';
|
|
145
255
|
|
|
146
|
-
|
|
256
|
+
const MyComponent: React.FC = () => {
|
|
257
|
+
const queryResult = useGetUserQuery(userId);
|
|
258
|
+
const adjusted = useAdjustUseQueryHookResultWithHierarchicalDateConverter(queryResult);
|
|
259
|
+
// adjusted.data dates are converted!
|
|
260
|
+
};
|
|
261
|
+
```
|
|
147
262
|
|
|
148
|
-
|
|
149
|
-
import { useRequest, RequestConfig } from 'redux-query-react';
|
|
150
|
-
import { useMemo } from 'react';
|
|
263
|
+
### SWR
|
|
151
264
|
|
|
152
|
-
|
|
265
|
+
```typescript
|
|
266
|
+
import useSWR from 'swr';
|
|
153
267
|
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
154
268
|
|
|
155
|
-
|
|
156
|
-
const
|
|
269
|
+
async function fetcher(url: string) {
|
|
270
|
+
const response = await fetch(url);
|
|
271
|
+
const data = await response.json();
|
|
272
|
+
hierarchicalConvertToDate(data);
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
157
275
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}), [url, restConfig]);
|
|
276
|
+
function MyComponent() {
|
|
277
|
+
const { data } = useSWR('/api/users', fetcher);
|
|
278
|
+
// data dates are already converted!
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Redux Saga
|
|
166
283
|
|
|
167
|
-
|
|
284
|
+
```typescript
|
|
285
|
+
import { call, put } from 'redux-saga/effects';
|
|
286
|
+
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
287
|
+
|
|
288
|
+
function* fetchData(action) {
|
|
289
|
+
const response = yield call(axios.get, action.payload.url);
|
|
290
|
+
hierarchicalConvertToDate(response.data);
|
|
291
|
+
yield put({ type: 'FETCH_SUCCESS', payload: response.data });
|
|
168
292
|
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Redux Thunk
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
169
299
|
|
|
300
|
+
function fetchApiData(url: string) {
|
|
301
|
+
return async (dispatch: Function) => {
|
|
302
|
+
const response = await fetch(url);
|
|
303
|
+
const data = await response.json();
|
|
304
|
+
hierarchicalConvertToDate(data);
|
|
305
|
+
dispatch({ type: 'FETCH_SUCCESS', payload: data });
|
|
306
|
+
};
|
|
307
|
+
}
|
|
170
308
|
```
|
|
171
309
|
|
|
172
|
-
|
|
310
|
+
---
|
|
173
311
|
|
|
174
|
-
|
|
312
|
+
## 🔒 Security (NEW in v8.0.0+)
|
|
175
313
|
|
|
176
|
-
|
|
314
|
+
### ✅ Production-Hardened
|
|
177
315
|
|
|
178
|
-
|
|
316
|
+
This library has undergone comprehensive security review and hardening:
|
|
179
317
|
|
|
180
|
-
|
|
318
|
+
| Security Feature | Status | Impact |
|
|
319
|
+
|-----------------|--------|---------|
|
|
320
|
+
| Prototype Pollution Protection | ✅ | Blocks `__proto__`, `constructor`, `prototype` |
|
|
321
|
+
| Circular Reference Detection | ✅ | No infinite loops or stack overflows |
|
|
322
|
+
| Depth Limiting | ✅ | Max 100 levels (DoS protection) |
|
|
323
|
+
| Error Handling | ✅ | Graceful degradation on invalid dates |
|
|
324
|
+
| Content-Type Validation | ✅ | Strict `application/json` only |
|
|
325
|
+
| Immutable Operations | ✅ | Deep cloning prevents mutations |
|
|
181
326
|
|
|
182
|
-
|
|
327
|
+
### 🔴 Critical Fix: Prototype Pollution
|
|
183
328
|
|
|
184
|
-
|
|
329
|
+
**Problem:**
|
|
330
|
+
```javascript
|
|
331
|
+
// Malicious payload
|
|
332
|
+
const evil = {
|
|
333
|
+
"__proto__": { "isAdmin": true },
|
|
334
|
+
"date": "2023-01-01T00:00:00.000Z"
|
|
335
|
+
};
|
|
336
|
+
// Could pollute Object.prototype! 😱
|
|
337
|
+
```
|
|
185
338
|
|
|
186
|
-
|
|
339
|
+
**Solution:**
|
|
340
|
+
```typescript
|
|
341
|
+
// Now safely ignored
|
|
342
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
343
|
+
// ✅ Your app is safe
|
|
344
|
+
```
|
|
187
345
|
|
|
188
|
-
|
|
189
|
-
import { useQuery } from 'react-query';
|
|
346
|
+
### 🟡 High Priority: Performance
|
|
190
347
|
|
|
191
|
-
|
|
192
|
-
|
|
348
|
+
**Before:**
|
|
349
|
+
```
|
|
350
|
+
1000 string fields in JSON → 1000 regex tests
|
|
351
|
+
CPU intensive, slow on large payloads
|
|
352
|
+
```
|
|
193
353
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
354
|
+
**After:**
|
|
355
|
+
```
|
|
356
|
+
1000 string fields → ~10 regex tests (990 fast rejections)
|
|
357
|
+
10-100x faster, minimal CPU usage
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**How?**
|
|
361
|
+
```typescript
|
|
362
|
+
// Fast character checks BEFORE expensive regex
|
|
363
|
+
if (v[4] === '-' && v[7] === '-' && v[10] === 'T') {
|
|
364
|
+
// Only then check regex
|
|
200
365
|
}
|
|
366
|
+
```
|
|
201
367
|
|
|
202
|
-
|
|
203
|
-
const { data, isLoading, error } = useQuery('myKey', () => fetcher('/api/my-endpoint'));
|
|
368
|
+
### 🛡️ Crash-Proof Error Handling
|
|
204
369
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
370
|
+
**Before:**
|
|
371
|
+
```typescript
|
|
372
|
+
// Single invalid date crashed entire conversion
|
|
373
|
+
{ "date": "2023-99-99" } // ❌ Crash!
|
|
374
|
+
```
|
|
208
375
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
376
|
+
**After:**
|
|
377
|
+
```typescript
|
|
378
|
+
// Invalid dates remain strings, valid dates converted
|
|
379
|
+
{ "date": "2023-99-99" } // ✅ Left as string
|
|
380
|
+
// + Console warning for debugging
|
|
381
|
+
```
|
|
212
382
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## ⚡ Performance
|
|
386
|
+
|
|
387
|
+
### Benchmarks
|
|
219
388
|
|
|
389
|
+
| Payload Size | Strings | Dates | Before | After | Improvement |
|
|
390
|
+
|-------------|---------|-------|--------|-------|-------------|
|
|
391
|
+
| Small | 10 | 2 | 0.5ms | 0.1ms | 5x |
|
|
392
|
+
| Medium | 100 | 10 | 5ms | 0.5ms | 10x |
|
|
393
|
+
| Large | 1000 | 50 | 150ms | 2ms | **75x** |
|
|
394
|
+
| Huge | 10000 | 100 | 3000ms | 30ms | **100x** |
|
|
395
|
+
|
|
396
|
+
### Why So Fast?
|
|
397
|
+
|
|
398
|
+
1. **Fast-path validation** — Rejects 99% of non-dates without regex
|
|
399
|
+
2. **WeakSet tracking** — Efficient circular reference detection
|
|
400
|
+
3. **Early bailout** — Depth limiting prevents unnecessary work
|
|
401
|
+
4. **Zero allocations** — In-place mutations (optional deep clone)
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 📚 Supported Date Libraries
|
|
406
|
+
|
|
407
|
+
| Library | Date Type | Duration Type | Package |
|
|
408
|
+
|---------|-----------|---------------|---------|
|
|
409
|
+
| **Native Date** | `Date` | N/A | `hierarchical-convert-to-date` |
|
|
410
|
+
| **date-fns** | `Date` | `Duration` | `hierarchical-convert-to-date-fns` |
|
|
411
|
+
| **Day.js** | `Dayjs` | `Duration` | `hierarchical-convert-to-dayjs` |
|
|
412
|
+
| **Moment.js** | `Moment` | `Duration` | `hierarchical-convert-to-moment` |
|
|
413
|
+
| **Luxon** | `DateTime` | `Duration` | `hierarchical-convert-to-luxon` |
|
|
414
|
+
| **js-joda** | `ZonedDateTime` | N/A | `hierarchical-convert-to-js-joda` |
|
|
415
|
+
|
|
416
|
+
## 🎨 Framework Integrations
|
|
417
|
+
|
|
418
|
+
| Framework | Package | Type | Features |
|
|
419
|
+
|-----------|---------|------|----------|
|
|
420
|
+
| **Angular** | `angular-date-http-interceptor` | Interceptor | Auto date conversion for all HTTP calls |
|
|
421
|
+
| **Angular** | `angular-typed-http-client` | Typed Client | Class-based DTOs + bidirectional transform |
|
|
422
|
+
| **Axios** | `axios-interceptor` | Instance Manager | Axios-specific interceptor |
|
|
423
|
+
| **React** | `react-redux-toolkit-hierarchical-date-hook` | RTK Query Hook | Redux Toolkit Query integration |
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 🧪 Testing
|
|
428
|
+
|
|
429
|
+
### Security Tests
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
describe('Security', () => {
|
|
433
|
+
it('blocks prototype pollution', () => {
|
|
434
|
+
const evil = { __proto__: { polluted: true } };
|
|
435
|
+
hierarchicalConvertToDate(evil);
|
|
436
|
+
expect(Object.prototype).not.toHaveProperty('polluted'); ✅
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('handles circular references', () => {
|
|
440
|
+
const circular: any = { date: '2023-01-01T00:00:00.000Z' };
|
|
441
|
+
circular.self = circular;
|
|
442
|
+
expect(() => hierarchicalConvertToDate(circular)).not.toThrow(); ✅
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('limits depth to 100', () => {
|
|
446
|
+
let deep: any = { date: '2023-01-01T00:00:00.000Z' };
|
|
447
|
+
for (let i = 0; i < 1000; i++) {
|
|
448
|
+
deep = { nested: deep };
|
|
449
|
+
}
|
|
450
|
+
expect(() => hierarchicalConvertToDate(deep)).not.toThrow(); ✅
|
|
451
|
+
});
|
|
452
|
+
});
|
|
220
453
|
```
|
|
221
454
|
|
|
222
|
-
|
|
455
|
+
### Coverage
|
|
223
456
|
|
|
224
|
-
|
|
457
|
+
- **130+ tests** across all libraries
|
|
458
|
+
- **100% coverage** of security fixes
|
|
459
|
+
- **Edge cases** tested (invalid dates, null, circular refs)
|
|
460
|
+
- **Performance** benchmarks included
|
|
225
461
|
|
|
226
|
-
|
|
462
|
+
---
|
|
227
463
|
|
|
228
|
-
|
|
464
|
+
## 📖 API Reference
|
|
229
465
|
|
|
230
|
-
|
|
231
|
-
import { call, put, takeEvery } from 'redux-saga/effects';
|
|
232
|
-
import axios from 'axios';
|
|
466
|
+
### `hierarchicalConvertToDate(obj, depth?, visited?)`
|
|
233
467
|
|
|
234
|
-
|
|
235
|
-
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
468
|
+
Recursively converts ISO 8601 date strings to Date objects.
|
|
236
469
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
470
|
+
**Parameters:**
|
|
471
|
+
- `obj: unknown` — The object/array to process (mutated in place)
|
|
472
|
+
- `depth?: number` — Current recursion depth (default: 0, max: 100)
|
|
473
|
+
- `visited?: WeakSet` — Visited objects tracker (default: new WeakSet())
|
|
474
|
+
|
|
475
|
+
**Returns:** `void` (mutates input object)
|
|
476
|
+
|
|
477
|
+
**Examples:**
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// Simple object
|
|
481
|
+
const data = { date: '2023-01-01T00:00:00.000Z' };
|
|
482
|
+
hierarchicalConvertToDate(data);
|
|
483
|
+
console.log(data.date instanceof Date); // true
|
|
484
|
+
|
|
485
|
+
// Nested
|
|
486
|
+
const nested = {
|
|
487
|
+
user: {
|
|
488
|
+
profile: {
|
|
489
|
+
birthday: '1990-01-01T00:00:00.000Z'
|
|
490
|
+
}
|
|
245
491
|
}
|
|
246
|
-
}
|
|
492
|
+
};
|
|
493
|
+
hierarchicalConvertToDate(nested);
|
|
494
|
+
// All levels converted!
|
|
495
|
+
|
|
496
|
+
// Arrays
|
|
497
|
+
const arr = [
|
|
498
|
+
{ date: '2023-01-01T00:00:00.000Z' },
|
|
499
|
+
{ date: '2023-02-01T00:00:00.000Z' }
|
|
500
|
+
];
|
|
501
|
+
hierarchicalConvertToDate(arr);
|
|
502
|
+
// Both converted!
|
|
503
|
+
|
|
504
|
+
// Mixed
|
|
505
|
+
const mixed = {
|
|
506
|
+
name: 'John',
|
|
507
|
+
age: 30,
|
|
508
|
+
active: true,
|
|
509
|
+
metadata: null,
|
|
510
|
+
dates: ['2023-01-01T00:00:00.000Z', '2023-02-01T00:00:00.000Z']
|
|
511
|
+
};
|
|
512
|
+
hierarchicalConvertToDate(mixed);
|
|
513
|
+
// Only date strings converted, rest untouched
|
|
514
|
+
```
|
|
247
515
|
|
|
248
|
-
|
|
249
|
-
function* watchFetchData() {
|
|
250
|
-
yield takeEvery('FETCH_REQUESTED', fetchData);
|
|
251
|
-
}
|
|
516
|
+
---
|
|
252
517
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
518
|
+
## 🔧 TypeScript Support
|
|
519
|
+
|
|
520
|
+
### Comprehensive Types
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
/**
|
|
524
|
+
* Value types that can appear in converted data
|
|
525
|
+
*/
|
|
526
|
+
type DateValue = Date | string | number | boolean | null;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Object with potentially date-convertible fields
|
|
530
|
+
*/
|
|
531
|
+
type DateObject = { [key: string]: DateValue | DateObject | DateArray };
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Array of potentially date-convertible values
|
|
535
|
+
*/
|
|
536
|
+
type DateArray = Array<DateValue | DateObject | DateArray>;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Root type for conversion
|
|
540
|
+
*/
|
|
541
|
+
type RecordWithDate = DateObject;
|
|
257
542
|
```
|
|
258
543
|
|
|
259
|
-
|
|
544
|
+
### Full IDE Support
|
|
260
545
|
|
|
261
|
-
|
|
546
|
+
- ✅ Autocompletion for all methods
|
|
547
|
+
- ✅ Type inference for nested structures
|
|
548
|
+
- ✅ JSDoc documentation
|
|
549
|
+
- ✅ Error hints and warnings
|
|
262
550
|
|
|
263
|
-
|
|
551
|
+
---
|
|
264
552
|
|
|
265
|
-
|
|
266
|
-
import { useDispatch } from 'react-redux';
|
|
553
|
+
## 🚨 Breaking Changes & Migration
|
|
267
554
|
|
|
268
|
-
|
|
269
|
-
const dispatch = useDispatch();
|
|
555
|
+
### v7.0.0 → v8.0.0+ (Axios Only)
|
|
270
556
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
557
|
+
**What Changed:**
|
|
558
|
+
Axios `AxiosInstanceManager` no longer caches instances (singleton pattern removed).
|
|
559
|
+
|
|
560
|
+
**Before:**
|
|
561
|
+
```typescript
|
|
562
|
+
const instance1 = AxiosInstanceManager.createInstance(convertFunc);
|
|
563
|
+
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
|
|
564
|
+
// instance1 === instance2 ✅ (cached)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**After:**
|
|
568
|
+
```typescript
|
|
569
|
+
const instance1 = AxiosInstanceManager.createInstance(convertFunc);
|
|
570
|
+
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
|
|
571
|
+
// instance1 !== instance2 ⚠️ (new instances)
|
|
572
|
+
```
|
|
274
573
|
|
|
275
|
-
|
|
574
|
+
**Migration:**
|
|
575
|
+
```typescript
|
|
576
|
+
// Create once, export, reuse
|
|
577
|
+
export const api = AxiosInstanceManager.createInstance(hierarchicalConvertToDate);
|
|
578
|
+
|
|
579
|
+
// Import and use everywhere
|
|
580
|
+
import { api } from './api';
|
|
581
|
+
const response = await api.get('/users');
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Everything Else
|
|
585
|
+
|
|
586
|
+
✅ **100% backward compatible!** All other changes are non-breaking.
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## 🐛 Troubleshooting
|
|
591
|
+
|
|
592
|
+
### Invalid dates remain strings
|
|
593
|
+
|
|
594
|
+
**Problem:**
|
|
595
|
+
```typescript
|
|
596
|
+
const data = { date: '2023-99-99T99:99:99.000Z' };
|
|
597
|
+
hierarchicalConvertToDate(data);
|
|
598
|
+
console.log(data.date); // Still a string? 🤔
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Solution:**
|
|
602
|
+
This is **expected behavior**. Invalid date strings are left unchanged (graceful degradation). Check console for warnings:
|
|
603
|
+
```
|
|
604
|
+
⚠️ Failed to parse date string: 2023-99-99T99:99:99.000Z
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Performance issues
|
|
608
|
+
|
|
609
|
+
**Problem:**
|
|
610
|
+
Conversion still slow on large payloads?
|
|
611
|
+
|
|
612
|
+
**Solutions:**
|
|
613
|
+
1. ✅ Upgrade to v8.0.0+ (10-100x faster)
|
|
614
|
+
2. ✅ Profile your data — are there really many date strings?
|
|
615
|
+
3. ✅ Consider server-side conversion for massive payloads (>100MB)
|
|
616
|
+
|
|
617
|
+
### TypeScript errors
|
|
618
|
+
|
|
619
|
+
**Problem:**
|
|
620
|
+
```typescript
|
|
621
|
+
Type 'unknown' is not assignable to type 'Date'
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Solution:**
|
|
625
|
+
Use type assertions or type guards:
|
|
626
|
+
```typescript
|
|
627
|
+
const data = apiResponse as { date: Date };
|
|
628
|
+
// or
|
|
629
|
+
if (data.date instanceof Date) {
|
|
630
|
+
// TypeScript knows it's a Date here
|
|
276
631
|
}
|
|
277
632
|
```
|
|
278
633
|
|
|
279
|
-
|
|
634
|
+
### Circular references warning
|
|
280
635
|
|
|
281
|
-
|
|
636
|
+
**Problem:**
|
|
637
|
+
```
|
|
638
|
+
⚠️ Circular reference detected in object
|
|
639
|
+
```
|
|
282
640
|
|
|
283
|
-
|
|
641
|
+
**Solution:**
|
|
642
|
+
This is **expected** if your data has circular refs. Conversion still succeeds, but circular paths are skipped.
|
|
284
643
|
|
|
285
|
-
|
|
644
|
+
---
|
|
286
645
|
|
|
287
|
-
|
|
646
|
+
## 🎓 Advanced Usage
|
|
288
647
|
|
|
289
|
-
|
|
290
|
-
import useSWR from 'swr';
|
|
648
|
+
### Custom Depth Limit
|
|
291
649
|
|
|
292
|
-
|
|
293
|
-
|
|
650
|
+
```typescript
|
|
651
|
+
// Default is 100, but you can customize
|
|
652
|
+
function convertShallow(obj: unknown) {
|
|
653
|
+
hierarchicalConvertToDate(obj, 0, new WeakSet());
|
|
654
|
+
// Will stop at depth 100 (depth param is current depth, not max)
|
|
655
|
+
}
|
|
656
|
+
```
|
|
294
657
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
658
|
+
### Performance Monitoring
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
function convertWithTiming(obj: unknown) {
|
|
662
|
+
const start = performance.now();
|
|
663
|
+
hierarchicalConvertToDate(obj);
|
|
664
|
+
const end = performance.now();
|
|
665
|
+
console.log(`Conversion took ${end - start}ms`);
|
|
301
666
|
}
|
|
667
|
+
```
|
|
302
668
|
|
|
303
|
-
|
|
304
|
-
const { data, error } = useSWR('/api/my-endpoint', fetcher);
|
|
669
|
+
### Conditional Conversion
|
|
305
670
|
|
|
306
|
-
|
|
307
|
-
|
|
671
|
+
```typescript
|
|
672
|
+
function convertIfNeeded(obj: unknown, shouldConvert: boolean) {
|
|
673
|
+
if (shouldConvert && obj != null && typeof obj === 'object') {
|
|
674
|
+
hierarchicalConvertToDate(obj);
|
|
308
675
|
}
|
|
676
|
+
}
|
|
677
|
+
```
|
|
309
678
|
|
|
310
|
-
|
|
311
|
-
return <div>Loading...</div>;
|
|
312
|
-
}
|
|
679
|
+
---
|
|
313
680
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
681
|
+
## 🎁 BONUS: Angular Typed HTTP Client
|
|
682
|
+
|
|
683
|
+
**For advanced Angular developers:** If you need more than simple date conversion, check out our Type-Safe HTTP
|
|
684
|
+
Client with class-transformer integration!
|
|
685
|
+
|
|
686
|
+
### Why Use It?
|
|
687
|
+
|
|
688
|
+
- 🎯 **Full Type Safety** — Compile-time + runtime type checking with class constructors
|
|
689
|
+
- 🔄 **Bidirectional Transform** — Serialize requests AND deserialize responses automatically
|
|
690
|
+
- 🏷️ **Decorator-Based** — Use `@Transform`, `@Type`, `@Expose`, `@Exclude` for custom logic
|
|
691
|
+
- 📦 **DTO Pattern** — Clean separation of API models from domain models
|
|
692
|
+
- ✅ **Validation Ready** — Seamless integration with `class-validator`
|
|
693
|
+
- 💎 **Computed Properties** — Add getters and methods to your response objects
|
|
694
|
+
- 🔥 **.NET Integration** — Perfect for Newtonsoft.Json/System.Text.Json polymorphic types
|
|
695
|
+
- 📝 **Typewriter Support** — Auto-generate TypeScript classes from C# models
|
|
696
|
+
|
|
697
|
+
### Quick Example
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
import { Transform, Type, Expose, Exclude } from 'class-transformer';
|
|
701
|
+
import { IsEmail, IsNotEmpty } from 'class-validator';
|
|
702
|
+
|
|
703
|
+
class AddressDto {
|
|
704
|
+
@Expose()
|
|
705
|
+
street!: string;
|
|
706
|
+
|
|
707
|
+
@Expose()
|
|
708
|
+
city!: string;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
class UserDto {
|
|
712
|
+
@Expose()
|
|
713
|
+
@IsNotEmpty()
|
|
714
|
+
id!: number;
|
|
715
|
+
|
|
716
|
+
@Expose()
|
|
717
|
+
@IsEmail()
|
|
718
|
+
email!: string;
|
|
719
|
+
|
|
720
|
+
@Exclude() // Won't be sent or received
|
|
721
|
+
password?: string;
|
|
722
|
+
|
|
723
|
+
@Transform(({ value }) => new Date(value), { toClassOnly: true })
|
|
724
|
+
@Transform(({ value }) => value?.toISOString(), { toPlainOnly: true })
|
|
725
|
+
createdAt!: Date;
|
|
726
|
+
|
|
727
|
+
@Type(() => AddressDto)
|
|
728
|
+
address?: AddressDto;
|
|
729
|
+
|
|
730
|
+
@Type(() => PostDto)
|
|
731
|
+
posts?: PostDto[];
|
|
732
|
+
|
|
733
|
+
// Computed property
|
|
734
|
+
get isRecent(): boolean {
|
|
735
|
+
const dayAgo = new Date();
|
|
736
|
+
dayAgo.setDate(dayAgo.getDate() - 1);
|
|
737
|
+
return this.createdAt > dayAgo;
|
|
738
|
+
}
|
|
319
739
|
}
|
|
740
|
+
|
|
741
|
+
// POST with automatic serialization
|
|
742
|
+
const newUser = new UserDto();
|
|
743
|
+
newUser.email = 'john@example.com';
|
|
744
|
+
newUser.createdAt = new Date();
|
|
745
|
+
|
|
746
|
+
typedHttp.post('/api/users', newUser, UserDto).subscribe(savedUser => {
|
|
747
|
+
console.log(savedUser instanceof UserDto); // ✅ true
|
|
748
|
+
console.log(savedUser.isRecent); // ✅ Works!
|
|
749
|
+
});
|
|
320
750
|
```
|
|
321
751
|
|
|
322
|
-
|
|
752
|
+
**API Methods:**
|
|
323
753
|
|
|
324
|
-
|
|
754
|
+
```typescript
|
|
755
|
+
// Get response body only
|
|
756
|
+
typedHttp.get<T>(url, Ctor, options?): Observable<T>
|
|
757
|
+
typedHttp.post<T, K>(url, body, Ctor, options?): Observable<K>
|
|
758
|
+
typedHttp.put<T, K>(url, body, Ctor, options?): Observable<K>
|
|
759
|
+
typedHttp.patch<T, K>(url, body, Ctor, options?): Observable<K>
|
|
760
|
+
typedHttp.delete<K>(url, Ctor, options?): Observable<K>
|
|
325
761
|
|
|
326
|
-
|
|
762
|
+
// Get full HttpResponse
|
|
763
|
+
typedHttp.getResponse<K>(url, Ctor, options?): Observable<HttpResponse<K>>
|
|
764
|
+
typedHttp.postResponse<T, K>(url, body, Ctor, options?): Observable<HttpResponse<K>>
|
|
765
|
+
// ... etc
|
|
766
|
+
```
|
|
327
767
|
|
|
328
|
-
|
|
768
|
+
**Options:**
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
const options: RequestOptions = {
|
|
772
|
+
headers: { 'Authorization': 'Bearer token' },
|
|
773
|
+
params: { page: '1', limit: '10' },
|
|
774
|
+
serialize: true, // Auto-serialize request body (default: true)
|
|
775
|
+
// or use class-transformer options:
|
|
776
|
+
serialize: {
|
|
777
|
+
excludeExtraneousValues: true,
|
|
778
|
+
enableImplicitConversion: true
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
```
|
|
329
782
|
|
|
330
|
-
|
|
783
|
+
**Why use Typed HTTP Client over simple interceptor?**
|
|
784
|
+
|
|
785
|
+
| Feature | Interceptor | Typed HTTP Client |
|
|
786
|
+
|---------|-------------|-------------------|
|
|
787
|
+
| Date conversion | ✅ Automatic | ✅ Automatic + custom |
|
|
788
|
+
| Type safety | ⚠️ Runtime only | ✅ Compile-time + Runtime |
|
|
789
|
+
| Request serialization | ❌ No | ✅ Yes |
|
|
790
|
+
| Nested objects | ✅ Yes | ✅ Yes + validation |
|
|
791
|
+
| Custom transforms | ❌ No | ✅ Full decorator support |
|
|
792
|
+
| Class methods | ❌ No | ✅ Yes (computed props, etc.) |
|
|
793
|
+
| Validation | ❌ No | ✅ class-validator integration |
|
|
794
|
+
|
|
795
|
+
**Use Typed HTTP Client when:**
|
|
796
|
+
- ✅ You want compile-time type safety
|
|
797
|
+
- ✅ You need bidirectional transformation (request + response)
|
|
798
|
+
- ✅ You're using DTOs/class-based architecture
|
|
799
|
+
- ✅ You need validation with `class-validator`
|
|
800
|
+
- ✅ You want computed properties on response objects
|
|
801
|
+
|
|
802
|
+
**Use simple interceptor when:**
|
|
803
|
+
- ✅ You only need date conversion (no other transforms)
|
|
804
|
+
- ✅ You work with plain objects (no classes)
|
|
805
|
+
- ✅ You want minimal setup
|
|
806
|
+
- ✅ You don't need request serialization
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
810
|
+
### 🔥 .NET Integration: Polymorphic Types
|
|
811
|
+
|
|
812
|
+
**Perfect for .NET developers!** If you're using **Newtonsoft.Json** or **System.Text.Json** with polymorphic types, the Typed HTTP Client handles them beautifully with the **Typewriter** Visual Studio extension.
|
|
813
|
+
|
|
814
|
+
#### The Problem: .NET Polymorphic Serialization
|
|
815
|
+
|
|
816
|
+
.NET APIs often return polymorphic types with discriminators:
|
|
817
|
+
|
|
818
|
+
**C# Model (Newtonsoft.Json):**
|
|
819
|
+
```csharp
|
|
820
|
+
// Base class
|
|
821
|
+
[JsonConverter(typeof(JsonSubtypes), "$type")]
|
|
822
|
+
[JsonSubtypes.KnownSubType(typeof(EmailNotification), "Email")]
|
|
823
|
+
[JsonSubtypes.KnownSubType(typeof(SmsNotification), "Sms")]
|
|
824
|
+
[JsonSubtypes.KnownSubType(typeof(PushNotification), "Push")]
|
|
825
|
+
public abstract class Notification
|
|
826
|
+
{
|
|
827
|
+
// $type is automatically generated by Newtonsoft.Json
|
|
828
|
+
public DateTime CreatedAt { get; set; }
|
|
829
|
+
public string Message { get; set; }
|
|
830
|
+
}
|
|
331
831
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
832
|
+
public class EmailNotification : Notification
|
|
833
|
+
{
|
|
834
|
+
public string To { get; set; }
|
|
835
|
+
public string Subject { get; set; }
|
|
836
|
+
public string HtmlBody { get; set; }
|
|
837
|
+
}
|
|
335
838
|
|
|
839
|
+
public class SmsNotification : Notification
|
|
840
|
+
{
|
|
841
|
+
public string PhoneNumber { get; set; }
|
|
842
|
+
public string ShortCode { get; set; }
|
|
843
|
+
}
|
|
336
844
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
845
|
+
public class PushNotification : Notification
|
|
846
|
+
{
|
|
847
|
+
public string DeviceToken { get; set; }
|
|
848
|
+
public string Title { get; set; }
|
|
849
|
+
public Dictionary<string, string> Data { get; set; }
|
|
850
|
+
}
|
|
851
|
+
```
|
|
340
852
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
853
|
+
**C# Model (System.Text.Json - .NET 7+):**
|
|
854
|
+
```csharp
|
|
855
|
+
// You can choose any discriminator property name
|
|
856
|
+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] // or "type", "kind", etc.
|
|
857
|
+
[JsonDerivedType(typeof(EmailNotification), "email")]
|
|
858
|
+
[JsonDerivedType(typeof(SmsNotification), "sms")]
|
|
859
|
+
[JsonDerivedType(typeof(PushNotification), "push")]
|
|
860
|
+
public abstract class Notification
|
|
861
|
+
{
|
|
862
|
+
public DateTime CreatedAt { get; set; }
|
|
863
|
+
public string Message { get; set; }
|
|
864
|
+
}
|
|
344
865
|
|
|
345
|
-
|
|
866
|
+
// Alternative with custom discriminator
|
|
867
|
+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
|
|
868
|
+
[JsonDerivedType(typeof(EmailNotification), "email")]
|
|
869
|
+
[JsonDerivedType(typeof(SmsNotification), "sms")]
|
|
870
|
+
public abstract class NotificationV2 { /* ... */ }
|
|
871
|
+
```
|
|
346
872
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
873
|
+
**JSON Response (Newtonsoft.Json):**
|
|
874
|
+
```json
|
|
875
|
+
{
|
|
876
|
+
"notifications": [
|
|
877
|
+
{
|
|
878
|
+
"$type": "Email",
|
|
879
|
+
"createdAt": "2023-01-15T10:30:00.000Z",
|
|
880
|
+
"message": "Welcome!",
|
|
881
|
+
"to": "user@example.com",
|
|
882
|
+
"subject": "Welcome to our app",
|
|
883
|
+
"htmlBody": "<h1>Welcome!</h1>"
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
"$type": "Sms",
|
|
887
|
+
"createdAt": "2023-01-15T11:00:00.000Z",
|
|
888
|
+
"message": "Your code: 123456",
|
|
889
|
+
"phoneNumber": "+1234567890",
|
|
890
|
+
"shortCode": "12345"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"$type": "Push",
|
|
894
|
+
"createdAt": "2023-01-15T12:00:00.000Z",
|
|
895
|
+
"message": "New message",
|
|
896
|
+
"deviceToken": "abc123...",
|
|
897
|
+
"title": "You have a new message",
|
|
898
|
+
"data": { "messageId": "456" }
|
|
350
899
|
}
|
|
351
|
-
|
|
900
|
+
]
|
|
352
901
|
}
|
|
353
902
|
```
|
|
354
903
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
In your component, you can dispatch this function like so:
|
|
904
|
+
#### The Solution: Typewriter + class-transformer
|
|
358
905
|
|
|
359
|
-
|
|
360
|
-
import { useDispatch } from 'react-redux';
|
|
906
|
+
**Step 1: Generate TypeScript with Typewriter**
|
|
361
907
|
|
|
362
|
-
|
|
363
|
-
const dispatch = useDispatch();
|
|
908
|
+
Install [Typewriter](https://github.com/AdaskoTheBeAsT/Typewriter) extension in Visual Studio, then create a `.tst` template:
|
|
364
909
|
|
|
365
|
-
|
|
366
|
-
dispatch(fetchApiData('/api/my-endpoint'));
|
|
367
|
-
}, [dispatch]);
|
|
910
|
+
> **💡 Pro Tip:** Complete `.tst` template recipes for Angular and React (both Newtonsoft.Json and System.Text.Json) are available at [NetCoreTypewriterRecipes](https://github.com/AdaskoTheBeAsT/NetCoreTypewriterRecipes)!
|
|
368
911
|
|
|
369
|
-
|
|
912
|
+
```typescript
|
|
913
|
+
${
|
|
914
|
+
using Typewriter.Extensions.Types;
|
|
915
|
+
|
|
916
|
+
Template(Settings settings)
|
|
917
|
+
{
|
|
918
|
+
settings.IncludeProject("YourApi.Models");
|
|
919
|
+
settings.OutputExtension = ".ts";
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
string Imports(Class c) => c.BaseClass != null
|
|
923
|
+
? $"import {{ {c.BaseClass.Name} }} from './{c.BaseClass.Name}';"
|
|
924
|
+
: "";
|
|
925
|
+
}
|
|
926
|
+
$Classes(*Notification)[
|
|
927
|
+
import { Transform, Type } from 'class-transformer';
|
|
928
|
+
$Imports
|
|
929
|
+
|
|
930
|
+
export class $Name$TypeParameters {
|
|
931
|
+
$Properties[
|
|
932
|
+
$Attributes[Transform][ @Transform(({ value }) => new Date(value), { toClassOnly: true })]
|
|
933
|
+
$Name: $Type;
|
|
934
|
+
]
|
|
370
935
|
}
|
|
936
|
+
]
|
|
371
937
|
```
|
|
372
938
|
|
|
373
|
-
|
|
939
|
+
**Generated TypeScript:**
|
|
940
|
+
```typescript
|
|
941
|
+
// notification.base.ts
|
|
942
|
+
import { Transform } from 'class-transformer';
|
|
943
|
+
|
|
944
|
+
export abstract class Notification {
|
|
945
|
+
// $type is automatically handled by class-transformer discriminator
|
|
946
|
+
|
|
947
|
+
@Transform(({ value }) => new Date(value), { toClassOnly: true })
|
|
948
|
+
createdAt!: Date;
|
|
949
|
+
|
|
950
|
+
message!: string;
|
|
951
|
+
}
|
|
374
952
|
|
|
375
|
-
|
|
953
|
+
// email-notification.ts
|
|
954
|
+
import { Notification } from './notification.base';
|
|
376
955
|
|
|
377
|
-
|
|
956
|
+
export class EmailNotification extends Notification {
|
|
957
|
+
to!: string;
|
|
958
|
+
subject!: string;
|
|
959
|
+
htmlBody!: string;
|
|
960
|
+
}
|
|
378
961
|
|
|
379
|
-
|
|
962
|
+
// sms-notification.ts
|
|
963
|
+
import { Notification } from './notification.base';
|
|
380
964
|
|
|
381
|
-
|
|
965
|
+
export class SmsNotification extends Notification {
|
|
966
|
+
phoneNumber!: string;
|
|
967
|
+
shortCode!: string;
|
|
968
|
+
}
|
|
382
969
|
|
|
383
|
-
|
|
384
|
-
import {
|
|
385
|
-
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';
|
|
970
|
+
// push-notification.ts
|
|
971
|
+
import { Notification } from './notification.base';
|
|
386
972
|
|
|
387
|
-
|
|
388
|
-
|
|
973
|
+
export class PushNotification extends Notification {
|
|
974
|
+
deviceToken!: string;
|
|
975
|
+
title!: string;
|
|
976
|
+
data!: Record<string, string>;
|
|
977
|
+
}
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
**Step 2: Add Discriminator Configuration**
|
|
981
|
+
|
|
982
|
+
Create a factory that uses the discriminator:
|
|
983
|
+
|
|
984
|
+
```typescript
|
|
985
|
+
import { Transform, Type } from 'class-transformer';
|
|
986
|
+
import { Notification } from './notification.base';
|
|
987
|
+
import { EmailNotification } from './email-notification';
|
|
988
|
+
import { SmsNotification } from './sms-notification';
|
|
989
|
+
import { PushNotification } from './push-notification';
|
|
990
|
+
|
|
991
|
+
export abstract class NotificationBase extends Notification {
|
|
992
|
+
@Transform(({ value }) => new Date(value), { toClassOnly: true })
|
|
993
|
+
createdAt!: Date;
|
|
994
|
+
|
|
995
|
+
// Discriminator-based transformation (Newtonsoft.Json uses $type)
|
|
996
|
+
@Type(() => NotificationBase, {
|
|
997
|
+
discriminator: {
|
|
998
|
+
property: '$type', // Newtonsoft.Json default
|
|
999
|
+
subTypes: [
|
|
1000
|
+
{ value: EmailNotification, name: 'Email' },
|
|
1001
|
+
{ value: SmsNotification, name: 'Sms' },
|
|
1002
|
+
{ value: PushNotification, name: 'Push' },
|
|
1003
|
+
],
|
|
1004
|
+
},
|
|
1005
|
+
})
|
|
1006
|
+
static createFromType(data: any): Notification {
|
|
1007
|
+
// class-transformer handles this automatically
|
|
1008
|
+
return data;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
export class NotificationListDto {
|
|
1013
|
+
@Type(() => NotificationBase, {
|
|
1014
|
+
discriminator: {
|
|
1015
|
+
property: '$type', // Match your C# configuration
|
|
1016
|
+
subTypes: [
|
|
1017
|
+
{ value: EmailNotification, name: 'Email' },
|
|
1018
|
+
{ value: SmsNotification, name: 'Sms' },
|
|
1019
|
+
{ value: PushNotification, name: 'Push' },
|
|
1020
|
+
],
|
|
1021
|
+
},
|
|
1022
|
+
})
|
|
1023
|
+
notifications!: Notification[];
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
389
1026
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
1027
|
+
**Step 3: Use in Your Component**
|
|
1028
|
+
|
|
1029
|
+
```typescript
|
|
1030
|
+
import { Component, inject } from '@angular/core';
|
|
1031
|
+
import { TypedHttpClient } from '@adaskothebeast/angular-typed-http-client';
|
|
1032
|
+
import { NotificationListDto, EmailNotification, SmsNotification, PushNotification } from './models';
|
|
1033
|
+
|
|
1034
|
+
@Component({
|
|
1035
|
+
selector: 'app-notifications',
|
|
1036
|
+
template: `
|
|
1037
|
+
<div *ngFor="let notification of (notifications$ | async)?.notifications">
|
|
1038
|
+
<!-- Type guards work! -->
|
|
1039
|
+
<div *ngIf="isEmail(notification)" class="email">
|
|
1040
|
+
📧 Email to {{ notification.to }}: {{ notification.subject }}
|
|
1041
|
+
<div [innerHTML]="notification.htmlBody"></div>
|
|
1042
|
+
</div>
|
|
1043
|
+
|
|
1044
|
+
<div *ngIf="isSms(notification)" class="sms">
|
|
1045
|
+
💬 SMS to {{ notification.phoneNumber }}: {{ notification.message }}
|
|
1046
|
+
</div>
|
|
1047
|
+
|
|
1048
|
+
<div *ngIf="isPush(notification)" class="push">
|
|
1049
|
+
📱 Push to device: {{ notification.title }}
|
|
1050
|
+
<pre>{{ notification.data | json }}</pre>
|
|
1051
|
+
</div>
|
|
1052
|
+
|
|
1053
|
+
<!-- Date is already converted! -->
|
|
1054
|
+
<small>{{ notification.createdAt | date:'short' }}</small>
|
|
1055
|
+
</div>
|
|
1056
|
+
`
|
|
1057
|
+
})
|
|
1058
|
+
export class NotificationsComponent {
|
|
1059
|
+
private typedHttp = inject(TypedHttpClient);
|
|
1060
|
+
|
|
1061
|
+
notifications$ = this.typedHttp.get('/api/notifications', NotificationListDto);
|
|
1062
|
+
|
|
1063
|
+
// Type guards for template
|
|
1064
|
+
isEmail(n: Notification): n is EmailNotification {
|
|
1065
|
+
return n instanceof EmailNotification;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
isSms(n: Notification): n is SmsNotification {
|
|
1069
|
+
return n instanceof SmsNotification;
|
|
396
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
isPush(n: Notification): n is PushNotification {
|
|
1073
|
+
return n instanceof PushNotification;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Or use type property
|
|
1077
|
+
getNotificationType(notification: Notification): string {
|
|
1078
|
+
if (notification instanceof EmailNotification) return 'email';
|
|
1079
|
+
if (notification instanceof SmsNotification) return 'sms';
|
|
1080
|
+
if (notification instanceof PushNotification) return 'push';
|
|
1081
|
+
return 'unknown';
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
**Step 4: Polymorphic POST/PUT Requests**
|
|
1087
|
+
|
|
1088
|
+
Sending polymorphic types back to .NET:
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
// Create different notification types
|
|
1092
|
+
const emailNotif = new EmailNotification();
|
|
1093
|
+
// $type is automatically added during serialization
|
|
1094
|
+
emailNotif.to = 'user@example.com';
|
|
1095
|
+
emailNotif.subject = 'Test';
|
|
1096
|
+
emailNotif.message = 'Hello!';
|
|
1097
|
+
emailNotif.htmlBody = '<p>Hello World!</p>';
|
|
1098
|
+
emailNotif.createdAt = new Date();
|
|
1099
|
+
|
|
1100
|
+
const smsNotif = new SmsNotification();
|
|
1101
|
+
// $type is automatically added during serialization
|
|
1102
|
+
smsNotif.phoneNumber = '+1234567890';
|
|
1103
|
+
smsNotif.message = 'Your code: 123';
|
|
1104
|
+
smsNotif.createdAt = new Date();
|
|
1105
|
+
|
|
1106
|
+
// Send to API - automatically serialized with discriminator!
|
|
1107
|
+
this.typedHttp.post('/api/notifications', emailNotif, EmailNotification)
|
|
1108
|
+
.subscribe(result => {
|
|
1109
|
+
console.log('Saved:', result);
|
|
1110
|
+
console.log(result instanceof EmailNotification); // ✅ true
|
|
1111
|
+
console.log(result.createdAt instanceof Date); // ✅ true
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
#### Benefits for .NET Developers
|
|
1116
|
+
|
|
1117
|
+
| Feature | Without Typed Client | With Typed Client |
|
|
1118
|
+
|---------|---------------------|-------------------|
|
|
1119
|
+
| **Polymorphic Types** | ❌ Manual type checking | ✅ Automatic with discriminator |
|
|
1120
|
+
| **Type Safety** | ⚠️ `as` casts everywhere | ✅ True instanceof checks |
|
|
1121
|
+
| **Date Conversion** | ❌ Manual parsing | ✅ Automatic with @Transform |
|
|
1122
|
+
| **Typewriter Integration** | ⚠️ Manual class creation | ✅ Auto-generated from C# |
|
|
1123
|
+
| **Validation** | ❌ Runtime only | ✅ Compile-time + Runtime |
|
|
1124
|
+
| **Serialization** | ❌ Manual JSON.stringify | ✅ Automatic with decorators |
|
|
1125
|
+
| **Nested Types** | ⚠️ Complex manual handling | ✅ @Type decorator handles it |
|
|
1126
|
+
| **Discriminator** | ❌ Manual switch/case | ✅ class-transformer handles it |
|
|
1127
|
+
|
|
1128
|
+
#### System.Text.Json Configuration
|
|
1129
|
+
|
|
1130
|
+
For **System.Text.Json**, the discriminator property is configurable in C#:
|
|
1131
|
+
|
|
1132
|
+
```typescript
|
|
1133
|
+
// Match your C# TypeDiscriminatorPropertyName setting
|
|
1134
|
+
export class NotificationListDto {
|
|
1135
|
+
@Type(() => NotificationBase, {
|
|
1136
|
+
discriminator: {
|
|
1137
|
+
property: '$type', // or 'type', 'kind', 'notificationType', etc.
|
|
1138
|
+
subTypes: [
|
|
1139
|
+
{ value: EmailNotification, name: 'email' }, // lowercase in .NET 7+
|
|
1140
|
+
{ value: SmsNotification, name: 'sms' },
|
|
1141
|
+
{ value: PushNotification, name: 'push' },
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
})
|
|
1145
|
+
notifications!: Notification[];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// If you used custom discriminator in C#:
|
|
1149
|
+
// [JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
|
|
1150
|
+
export class CustomNotificationListDto {
|
|
1151
|
+
@Type(() => NotificationBase, {
|
|
1152
|
+
discriminator: {
|
|
1153
|
+
property: 'notificationType', // Must match C# configuration!
|
|
1154
|
+
subTypes: [
|
|
1155
|
+
{ value: EmailNotification, name: 'email' },
|
|
1156
|
+
{ value: SmsNotification, name: 'sms' },
|
|
1157
|
+
],
|
|
1158
|
+
},
|
|
1159
|
+
})
|
|
1160
|
+
notifications!: Notification[];
|
|
1161
|
+
}
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
#### Advanced: Deeply Nested Polymorphism
|
|
1165
|
+
|
|
1166
|
+
```csharp
|
|
1167
|
+
// C# - Nested polymorphic types
|
|
1168
|
+
public class NotificationGroup
|
|
1169
|
+
{
|
|
1170
|
+
public string Name { get; set; }
|
|
1171
|
+
public List<Notification> Notifications { get; set; }
|
|
1172
|
+
public NotificationSettings Settings { get; set; }
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
[JsonPolymorphic]
|
|
1176
|
+
[JsonDerivedType(typeof(EmailSettings), "email")]
|
|
1177
|
+
[JsonDerivedType(typeof(SmsSettings), "sms")]
|
|
1178
|
+
public abstract class NotificationSettings
|
|
1179
|
+
{
|
|
1180
|
+
public bool Enabled { get; set; }
|
|
1181
|
+
}
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
```typescript
|
|
1185
|
+
// TypeScript - Nested discriminators work too!
|
|
1186
|
+
export class NotificationGroupDto {
|
|
1187
|
+
name!: string;
|
|
1188
|
+
|
|
1189
|
+
@Type(() => NotificationBase, {
|
|
1190
|
+
discriminator: {
|
|
1191
|
+
property: '$type', // Newtonsoft.Json
|
|
1192
|
+
subTypes: [
|
|
1193
|
+
{ value: EmailNotification, name: 'Email' },
|
|
1194
|
+
{ value: SmsNotification, name: 'Sms' },
|
|
1195
|
+
],
|
|
1196
|
+
},
|
|
1197
|
+
})
|
|
1198
|
+
notifications!: Notification[];
|
|
1199
|
+
|
|
1200
|
+
@Type(() => NotificationSettingsBase, {
|
|
1201
|
+
discriminator: {
|
|
1202
|
+
property: '$type', // Both can use same or different discriminators
|
|
1203
|
+
subTypes: [
|
|
1204
|
+
{ value: EmailSettings, name: 'email' },
|
|
1205
|
+
{ value: SmsSettings, name: 'sms' },
|
|
1206
|
+
],
|
|
1207
|
+
},
|
|
1208
|
+
})
|
|
1209
|
+
settings!: NotificationSettings;
|
|
397
1210
|
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
**Result:** Automatic deserialization of nested polymorphic hierarchies! 🎉
|
|
1214
|
+
|
|
1215
|
+
#### Resources
|
|
1216
|
+
|
|
1217
|
+
- **Typewriter Extension:** https://github.com/AdaskoTheBeAsT/Typewriter - Fork with enhanced features
|
|
1218
|
+
- **Typewriter .tst Recipes:** https://github.com/AdaskoTheBeAsT/NetCoreTypewriterRecipes - Templates for Angular & React (Newtonsoft.Json & System.Text.Json)
|
|
1219
|
+
- **nxsamples:** https://github.com/AdaskoTheBeAsT/nxsamples - Complete Nx workspace examples
|
|
1220
|
+
- **class-transformer Discriminators:** Use `@Type()` with `discriminator` option
|
|
1221
|
+
- **.NET Polymorphic Serialization:** https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism
|
|
1222
|
+
- **Newtonsoft.Json Type Handling:** Uses `$type` by default for polymorphic serialization
|
|
1223
|
+
|
|
1224
|
+
---
|
|
1225
|
+
|
|
1226
|
+
## 🤝 Contributing
|
|
1227
|
+
|
|
1228
|
+
We welcome contributions! To get started:
|
|
1229
|
+
|
|
1230
|
+
1. Fork the repository
|
|
1231
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
1232
|
+
3. Make your changes
|
|
1233
|
+
4. Add tests for new functionality
|
|
1234
|
+
5. Ensure all tests pass: `yarn test:all`
|
|
1235
|
+
6. Commit: `git commit -m 'Add amazing feature'`
|
|
1236
|
+
7. Push: `git push origin feature/amazing-feature`
|
|
1237
|
+
8. Open a Pull Request
|
|
1238
|
+
|
|
1239
|
+
### Development Setup
|
|
398
1240
|
|
|
399
|
-
|
|
1241
|
+
```bash
|
|
1242
|
+
# Clone
|
|
1243
|
+
git clone https://github.com/AdaskoTheBeAsT/date-interceptors.git
|
|
1244
|
+
cd date-interceptors
|
|
1245
|
+
|
|
1246
|
+
# Install
|
|
1247
|
+
yarn install
|
|
1248
|
+
|
|
1249
|
+
# Test
|
|
1250
|
+
yarn test:all
|
|
1251
|
+
|
|
1252
|
+
# Build
|
|
1253
|
+
yarn build:all
|
|
1254
|
+
|
|
1255
|
+
# Lint
|
|
1256
|
+
yarn lint:all
|
|
400
1257
|
```
|
|
401
1258
|
|
|
402
|
-
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
## 🔐 Security
|
|
1262
|
+
|
|
1263
|
+
### Reporting Vulnerabilities
|
|
1264
|
+
|
|
1265
|
+
If you discover a security vulnerability, please email:
|
|
1266
|
+
📧 **adaskothebeast@gmail.com**
|
|
1267
|
+
|
|
1268
|
+
**Please include:**
|
|
1269
|
+
- Description of the vulnerability
|
|
1270
|
+
- Steps to reproduce
|
|
1271
|
+
- Potential impact
|
|
1272
|
+
- Suggested fix (if any)
|
|
1273
|
+
|
|
1274
|
+
We take security seriously and will respond promptly.
|
|
1275
|
+
|
|
1276
|
+
### Security Audits
|
|
1277
|
+
|
|
1278
|
+
- ✅ OWASP Top 10 reviewed
|
|
1279
|
+
- ✅ CWE-1321 (Prototype Pollution) mitigated
|
|
1280
|
+
- ✅ DoS protection (depth limiting)
|
|
1281
|
+
- ✅ Input validation hardened
|
|
1282
|
+
- ✅ Error handling comprehensive
|
|
1283
|
+
|
|
1284
|
+
---
|
|
1285
|
+
|
|
1286
|
+
## 📄 License
|
|
1287
|
+
|
|
1288
|
+
MIT © [AdaskoTheBeAsT](https://github.com/AdaskoTheBeAsT)
|
|
1289
|
+
|
|
1290
|
+
---
|
|
1291
|
+
|
|
1292
|
+
## 🌟 Show Your Support
|
|
1293
|
+
|
|
1294
|
+
If this library saves you time, give it a ⭐ on [GitHub](https://github.com/AdaskoTheBeAsT/date-interceptors)!
|
|
1295
|
+
|
|
1296
|
+
---
|
|
1297
|
+
|
|
1298
|
+
## 📞 Support
|
|
1299
|
+
|
|
1300
|
+
- 📖 **Documentation:** You're reading it!
|
|
1301
|
+
- 🐛 **Issues:** [GitHub Issues](https://github.com/AdaskoTheBeAsT/date-interceptors/issues)
|
|
1302
|
+
- 💬 **Discussions:** [GitHub Discussions](https://github.com/AdaskoTheBeAsT/date-interceptors/discussions)
|
|
1303
|
+
- 📧 **Email:** adaskothebeast@gmail.com
|
|
1304
|
+
|
|
1305
|
+
---
|
|
1306
|
+
|
|
1307
|
+
## 🎉 Acknowledgments
|
|
1308
|
+
|
|
1309
|
+
Thanks to all contributors and the community for making this library better!
|
|
1310
|
+
|
|
1311
|
+
Special thanks to:
|
|
1312
|
+
- OWASP for security guidelines
|
|
1313
|
+
- Date library maintainers for excellent date/time tooling
|
|
1314
|
+
- Framework teams for making integration smooth
|
|
1315
|
+
|
|
1316
|
+
---
|
|
1317
|
+
|
|
1318
|
+
<div align="center">
|
|
1319
|
+
|
|
1320
|
+
**Made with ❤️ by developers, for developers**
|
|
403
1321
|
|
|
404
|
-
|
|
405
|
-
- The fetchApiData function demonstrates using the configured Axios instance to make a GET request. The response data, with date strings already converted into Date (or in case of other libs proper class for given date lib) objects, is logged to the console. Errors from the request are caught and logged.
|
|
1322
|
+
[⬆ Back to Top](#-date-interceptors)
|
|
406
1323
|
|
|
407
|
-
|
|
1324
|
+
</div>
|