@rivinia/http 0.5.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +28 -378
- package/fesm2022/rivinia-http.mjs +5 -5
- package/fesm2022/rivinia-http.mjs.map +1 -1
- package/package.json +13 -1
- package/types/rivinia-http.d.ts +1 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
- Documentation: README restructured into docs/http/ pages; stale pre-1.0 wording removed.
|
|
6
|
+
|
|
7
|
+
## 1.0.0
|
|
8
|
+
|
|
9
|
+
- First stable release. URL joining collapses repeated boundary slashes; request URLs otherwise remain consumer-controlled.
|
|
10
|
+
|
|
11
|
+
## Unreleased
|
|
12
|
+
|
|
13
|
+
- Fix `joinUrl` so repeated leading and trailing slashes are collapsed to one boundary slash while query strings are preserved.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rivinia contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,402 +3,52 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
4
4
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
requests and reactive `httpResource` requests. It leaves URLs, backend
|
|
8
|
-
contracts, and interceptors under app control. Response bodies pass through
|
|
9
|
-
unchanged unless a request explicitly asks to unwrap one.
|
|
6
|
+
A typed Angular HTTP client for Observable requests and reactive resources.
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
- Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
|
|
13
|
-
as a dependency.
|
|
14
|
-
|
|
15
|
-
## Install and configure
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @rivinia/http
|
|
19
|
-
ng add @rivinia/http
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
For a standalone app, `ng add` adds `provideHttpClient()` and
|
|
23
|
-
`provideRiviniaHttp()` to the selected project's `src/app/app.config.ts`.
|
|
24
|
-
It has no prompts and does not set a base URL or an envelope policy. It is
|
|
25
|
-
idempotent and only edits an existing app config containing a `providers: []`
|
|
26
|
-
array; if no config is found it warns and skips, so add the providers
|
|
27
|
-
manually. In a multi-project workspace it targets `defaultProject` or the
|
|
28
|
-
first project; use `--project <name>` to select a project.
|
|
29
|
-
|
|
30
|
-
The equivalent minimal standalone setup is:
|
|
8
|
+
Package version: **1.0.1**
|
|
31
9
|
|
|
32
10
|
```ts
|
|
33
|
-
// src/app/app.config.ts
|
|
34
|
-
import { provideHttpClient } from '@angular/common/http';
|
|
35
|
-
import { ApplicationConfig } from '@angular/core';
|
|
36
|
-
import { provideRiviniaHttp } from '@rivinia/http';
|
|
37
|
-
|
|
38
|
-
export const appConfig: ApplicationConfig = {
|
|
39
|
-
providers: [provideHttpClient(), provideRiviniaHttp()],
|
|
40
|
-
};
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Use the same `provideHttpClient` setup to add app-owned interceptors:
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
import { ApplicationConfig } from '@angular/core';
|
|
47
|
-
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
48
|
-
import { provideRiviniaHttp } from '@rivinia/http';
|
|
49
|
-
import { authInterceptor } from './auth.interceptor';
|
|
50
|
-
|
|
51
|
-
export const appConfig: ApplicationConfig = {
|
|
52
|
-
providers: [provideHttpClient(withInterceptors([authInterceptor])), provideRiviniaHttp()],
|
|
53
|
-
};
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Alternatively, pass features through `provideRiviniaHttp`:
|
|
57
|
-
|
|
58
|
-
```ts
|
|
59
|
-
import { ApplicationConfig } from '@angular/core';
|
|
60
|
-
import { withInterceptors } from '@angular/common/http';
|
|
61
|
-
import { provideRiviniaHttp } from '@rivinia/http';
|
|
62
|
-
import { authInterceptor } from './auth.interceptor';
|
|
63
|
-
|
|
64
|
-
export const appConfig: ApplicationConfig = {
|
|
65
|
-
providers: [provideRiviniaHttp({ httpFeatures: [withInterceptors([authInterceptor])] })],
|
|
66
|
-
};
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Choose one backend setup: when using `httpFeatures`, do not also call
|
|
70
|
-
`provideHttpClient()`; otherwise the HTTP backend and its default XSRF
|
|
71
|
-
interceptor can be registered twice. If the app already configures
|
|
72
|
-
`provideHttpClient(...)`, keep that and call `provideRiviniaHttp()` without
|
|
73
|
-
`httpFeatures`.
|
|
74
|
-
|
|
75
|
-
## URLs and typed service calls
|
|
76
|
-
|
|
77
|
-
The client sends the endpoint string exactly as given. It never prepends a
|
|
78
|
-
base URL or rewrites a path. Compose URLs yourself, for example with
|
|
79
|
-
`joinUrl` and the app's environment:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
// src/environments/environment.ts
|
|
83
|
-
export const environment = {
|
|
84
|
-
apiUrl: 'https://api.example.com',
|
|
85
|
-
} as const;
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
// src/app/trip.service.ts
|
|
90
11
|
import { Service } from '@angular/core';
|
|
91
|
-
import type { HttpResponse } from '@angular/common/http';
|
|
92
|
-
import { Observable } from 'rxjs';
|
|
93
12
|
import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
|
|
94
|
-
import { environment } from '../environments/environment';
|
|
95
|
-
|
|
96
|
-
export interface Trip {
|
|
97
|
-
id: string;
|
|
98
|
-
title: string;
|
|
99
|
-
departureDate: string;
|
|
100
|
-
status: 'planned' | 'booked' | 'cancelled';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface TripDraft {
|
|
104
|
-
title: string;
|
|
105
|
-
departureDate: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface ApiEnvelope<T> {
|
|
109
|
-
data: T;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
13
|
@Service()
|
|
113
14
|
export class TripApi {
|
|
114
15
|
private readonly http = injectRiviniaHttp();
|
|
115
|
-
private readonly
|
|
116
|
-
|
|
117
|
-
list(): Observable<Trip[]> {
|
|
118
|
-
return this.http.get<Trip[]>(this.tripsUrl, {
|
|
119
|
-
params: { page: 1, search: 'coast' },
|
|
120
|
-
unwrap: true,
|
|
121
|
-
unwrapKey: 'data',
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
get(id: string): Observable<Trip> {
|
|
126
|
-
return this.http.get<Trip>(joinUrl(this.tripsUrl, id), {
|
|
127
|
-
unwrap: true,
|
|
128
|
-
unwrapKey: 'data',
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
create(input: TripDraft): Observable<Trip> {
|
|
133
|
-
return this.http.post<Trip>(this.tripsUrl, input, {
|
|
134
|
-
unwrap: true,
|
|
135
|
-
unwrapKey: 'data',
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
replace(id: string, input: TripDraft): Observable<Trip> {
|
|
140
|
-
return this.http.put<Trip>(joinUrl(this.tripsUrl, id), input, {
|
|
141
|
-
unwrap: true,
|
|
142
|
-
unwrapKey: 'data',
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
updateStatus(id: string, status: Trip['status']): Observable<Trip> {
|
|
147
|
-
return this.http.patch<Trip>(
|
|
148
|
-
joinUrl(this.tripsUrl, id),
|
|
149
|
-
{ status },
|
|
150
|
-
{ unwrap: true, unwrapKey: 'data' },
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
cancel(id: string, reason: string): Observable<void> {
|
|
155
|
-
return this.http.delete<void>(joinUrl(this.tripsUrl, id), {
|
|
156
|
-
body: { reason },
|
|
157
|
-
unwrap: true,
|
|
158
|
-
unwrapKey: 'data',
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
downloadItinerary(id: string): Observable<Blob> {
|
|
163
|
-
return this.http.getBlob(joinUrl(this.tripsUrl, `${id}/itinerary`));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
getOperatorNotes(id: string): Observable<string> {
|
|
167
|
-
return this.http.getText(joinUrl(this.tripsUrl, `${id}/operator-notes`));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
listWithHeaders(): Observable<HttpResponse<ApiEnvelope<Trip[]>>> {
|
|
171
|
-
return this.http.getResponse<ApiEnvelope<Trip[]>>(this.tripsUrl, {
|
|
172
|
-
params: { page: 1 },
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
createAndReadLocation(input: TripDraft): Observable<HttpResponse<ApiEnvelope<Trip>>> {
|
|
177
|
-
return this.http.postResponse<ApiEnvelope<Trip>>(this.tripsUrl, input);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The JSON endpoints in this example return an object with a `data` field; the first five
|
|
183
|
-
methods opt in to extracting that field. `getBlob()` downloads a binary file,
|
|
184
|
-
and `getText()` reads plain text; neither method unwraps or checks JSON
|
|
185
|
-
envelopes. `getResponse()` and `postResponse()` return the complete
|
|
186
|
-
`HttpResponse` so the caller can inspect the status and headers; their body
|
|
187
|
-
stays raw and is not unwrapped.
|
|
188
|
-
|
|
189
|
-
| `RiviniaHttp` method | Used above for |
|
|
190
|
-
| -------------------- | ------------------------------------------------------------------ |
|
|
191
|
-
| `get` | `list()` and `get(id)` |
|
|
192
|
-
| `post` | `create(input)` |
|
|
193
|
-
| `put` | `replace(id, input)` |
|
|
194
|
-
| `patch` | `updateStatus(id, status)` |
|
|
195
|
-
| `delete` | `cancel(id, reason)`; its optional request body is passed through. |
|
|
196
|
-
| `getBlob` | `downloadItinerary(id)` |
|
|
197
|
-
| `getText` | `getOperatorNotes(id)` |
|
|
198
|
-
| `getResponse` | `listWithHeaders()` |
|
|
199
|
-
| `postResponse` | `createAndReadLocation(input)` |
|
|
200
|
-
| `getResource` | Reactive paginated list in `TripListComponent`. |
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
// Example use where the API returns pagination metadata in a header.
|
|
204
|
-
this.api.listWithHeaders().subscribe((response) => {
|
|
205
|
-
const total = Number(response.headers.get('X-Total-Count') ?? 0);
|
|
206
|
-
console.log(response.status, total, response.body?.data);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// A create endpoint may return 201 plus a Location header.
|
|
210
|
-
this.api.createAndReadLocation(input).subscribe((response) => {
|
|
211
|
-
const location = response.headers.get('Location');
|
|
212
|
-
console.log(response.status, location, response.body?.data);
|
|
213
|
-
});
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
`Observable<T>` is the compile-time type of the result; it does not validate
|
|
217
|
-
the server payload. `joinUrl` removes one boundary slash and passes absolute
|
|
218
|
-
`http://` or `https://` endpoints through unchanged. Use `params` for query
|
|
219
|
-
values: `null` and `undefined` entries are omitted, while strings, numbers,
|
|
220
|
-
and booleans are stringified. The standard request options also pass
|
|
221
|
-
`headers`, `HttpContext`, and `withCredentials` through to Angular's
|
|
222
|
-
`HttpClient`. Use `HttpContext` with your own interceptors for app-specific
|
|
223
|
-
controls such as opting out of auth.
|
|
224
|
-
|
|
225
|
-
## Reactive resources
|
|
226
|
-
|
|
227
|
-
`getResource` uses Angular's `httpResource`. The endpoint can be a string, a
|
|
228
|
-
signal, or a reactive function; `undefined` disables the request. URL,
|
|
229
|
-
`params`, `headers`, and `context` can be reactive inputs and trigger a new
|
|
230
|
-
request when their signal values change. Other options are read once when
|
|
231
|
-
the resource is created.
|
|
232
|
-
|
|
233
|
-
```ts
|
|
234
|
-
import { Component, signal } from '@angular/core';
|
|
235
|
-
import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
|
|
236
|
-
import { environment } from '../environments/environment';
|
|
237
|
-
|
|
238
|
-
import type { Trip } from './trip.service';
|
|
239
|
-
|
|
240
|
-
@Component({
|
|
241
|
-
selector: 'app-trip-list',
|
|
242
|
-
templateUrl: './trip-list.component.html',
|
|
243
|
-
})
|
|
244
|
-
export class TripListComponent {
|
|
245
|
-
private readonly http = injectRiviniaHttp();
|
|
246
|
-
readonly page = signal(1);
|
|
247
|
-
readonly trips = this.http.getResource<Trip[]>(joinUrl(environment.apiUrl, 'trips'), {
|
|
248
|
-
params: () => ({ page: this.page() }),
|
|
249
|
-
unwrap: true,
|
|
250
|
-
unwrapKey: 'data',
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
nextPage(): void {
|
|
254
|
-
this.page.update((value) => value + 1);
|
|
255
|
-
}
|
|
16
|
+
private readonly url = joinUrl('/api', 'trips');
|
|
17
|
+
list() { return this.http.get<Trip[]>(this.url); }
|
|
256
18
|
}
|
|
257
19
|
```
|
|
258
20
|
|
|
259
|
-
|
|
260
|
-
<!-- src/app/trip-list.component.html -->
|
|
261
|
-
@if (trips.isLoading()) {
|
|
262
|
-
<p>Loading trips…</p>
|
|
263
|
-
} @else if (trips.hasValue()) {
|
|
264
|
-
<ul>
|
|
265
|
-
@for (trip of trips.value(); track trip.id) {
|
|
266
|
-
<li>{{ trip.title }} — {{ trip.departureDate }} ({{ trip.status }})</li>
|
|
267
|
-
}
|
|
268
|
-
</ul>
|
|
269
|
-
} @else if (trips.error()) {
|
|
270
|
-
<p role="alert">Could not load trips. Try again.</p>
|
|
271
|
-
}
|
|
272
|
-
<button type="button" (click)="nextPage()">Next page</button>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
In a real app, share the Trip model through a model module. `parse` is optional;
|
|
276
|
-
when provided it runs after envelope validation and unwrapping, and can perform
|
|
277
|
-
runtime validation (for example with Zod or Valibot). A parse error, HTTP
|
|
278
|
-
error, or library envelope error is exposed through `trips.error()`.
|
|
279
|
-
`defaultValue` supplies a value while idle/loading. `reportProgress` is
|
|
280
|
-
forwarded to the resource request; for progress events on an `Observable`,
|
|
281
|
-
use Angular `HttpClient` directly.
|
|
21
|
+
## Why @rivinia/http
|
|
282
22
|
|
|
283
|
-
|
|
23
|
+
- Typed Observable requests and reactive `httpResource` requests share one client.
|
|
24
|
+
- URLs, interceptors, and response contracts remain application-owned.
|
|
25
|
+
- Response bodies pass through unless a request explicitly unwraps an envelope.
|
|
284
26
|
|
|
285
|
-
|
|
286
|
-
a field named `data`. To extract a payload, opt in on that call and provide a
|
|
287
|
-
key:
|
|
27
|
+
## Install
|
|
288
28
|
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
The type contract requires `unwrapKey` when `unwrap: true`, and rejects a key
|
|
295
|
-
when unwrapping is off. Extraction is strict and single-level: the response
|
|
296
|
-
must be a non-null, non-array object with that own property. A missing key,
|
|
297
|
-
primitive/array/null body, or empty key throws `RiviniaHttpUnwrapError`;
|
|
298
|
-
it does not silently return the original body. Present values such as `null`,
|
|
299
|
-
`false`, `0`, and `''` are returned as-is. Nested data and sibling fields are
|
|
300
|
-
not modified. `unwrapAs<T>` performs the same one-level extraction outside a
|
|
301
|
-
request; its generic type is an unchecked cast, not runtime validation.
|
|
302
|
-
|
|
303
|
-
An optional envelope policy checks backend-declared failures before
|
|
304
|
-
unwrapping. The default keys are `isSuccess` and `error`; only an object whose
|
|
305
|
-
`isSuccess` value is exactly `false` is considered failed:
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
// Apply to JSON request paths made by this configured client.
|
|
309
|
-
provideRiviniaHttp({ envelope: {} });
|
|
310
|
-
|
|
311
|
-
// Or specify a per-request policy; it replaces the global policy.
|
|
312
|
-
this.http.get<Trip[]>(url, {
|
|
313
|
-
envelope: { successKey: 'ok', errorKey: 'message' },
|
|
314
|
-
});
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
When a failure is detected, `RiviniaHttpEnvelopeError` is sent through the
|
|
318
|
-
Observable error channel or appears as the resource error. It carries the raw
|
|
319
|
-
`body` and failure `code`. Set `throwOnFailure: false` for a passive policy.
|
|
320
|
-
Policy validation runs before unwrapping, including on `getResponse` and
|
|
321
|
-
`postResponse`. Those full-response methods return raw `HttpResponse<T>`
|
|
322
|
-
(status, headers, and unwrapped body is not applied). `getBlob` and `getText`
|
|
323
|
-
also return raw bodies and do not run envelope validation.
|
|
324
|
-
|
|
325
|
-
The generic type does not validate the server response. For untrusted or
|
|
326
|
-
loosely typed APIs, validate the extracted value with `getResource`'s `parse`
|
|
327
|
-
hook, or validate in the Observable pipeline in app code. Do not log
|
|
328
|
-
`RiviniaHttpUnwrapError.body` or `RiviniaHttpEnvelopeError.body` without
|
|
329
|
-
considering that they may contain sensitive data.
|
|
330
|
-
|
|
331
|
-
## Public API
|
|
332
|
-
|
|
333
|
-
| Export | Kind | Purpose |
|
|
334
|
-
| ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
335
|
-
| `provideRiviniaHttp(config?)` | provider | Sets `envelope` and optional `httpFeatures`; no global unwrap option. |
|
|
336
|
-
| `RiviniaHttp` | class | `get`, `post`, `put`, `patch`, `delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, and `getResource`. |
|
|
337
|
-
| `injectRiviniaHttp()` | function | Injection-context shorthand for `inject(RiviniaHttp)`. |
|
|
338
|
-
| `joinUrl(base, endpoint)` | utility | Removes one boundary slash when joining; absolute HTTP(S) endpoints pass through. |
|
|
339
|
-
| `unwrapAs(body, key)` | utility | Strict, single-level typed extraction. |
|
|
340
|
-
| `extractKey(body, key)` | utility | Strict extraction without a generic cast. |
|
|
341
|
-
| `RiviniaHttpUnwrapError` | class | Error for invalid or missing unwrap properties. |
|
|
342
|
-
| `RiviniaHttpEnvelopeError` | class | Error for a failed configured response envelope. |
|
|
343
|
-
| `RiviniaHttpConfig` | type | `envelope` and `httpFeatures` provider configuration. |
|
|
344
|
-
| `RiviniaEnvelopePolicy` | type | Success key, error key, and failure policy settings. |
|
|
345
|
-
| `RiviniaRequestOptions` | type | Options for requests that may unwrap. |
|
|
346
|
-
| `RiviniaRawOptions` | type | Options for methods that never unwrap. |
|
|
347
|
-
| `RiviniaDeleteOptions` | type | Delete options including an optional request body. |
|
|
348
|
-
| `RiviniaDeleteRequestOptions` | type | Delete options including the unwrap contract. |
|
|
349
|
-
| `RiviniaRequestBase` | type | Shared transport and envelope options. |
|
|
350
|
-
| `RiviniaResourceOptions` | type | Reactive resource options and unwrap contract. |
|
|
351
|
-
| `RiviniaResourceBase` | type | Shared resource-specific options. |
|
|
352
|
-
| `ReactiveInput` | type | Static value, signal, or reactive function input. |
|
|
353
|
-
| `QueryParamsInput` | type | `HttpParams` or a primitive-valued query object. |
|
|
354
|
-
| `RiviniaUnwrapOn` | type | Required key form for `{ unwrap: true }`. |
|
|
355
|
-
| `RiviniaUnwrapOff` | type | Form where an unwrap key is disallowed. |
|
|
356
|
-
|
|
357
|
-
## Testing
|
|
358
|
-
|
|
359
|
-
Use Angular's HTTP testing providers. The testing provider follows
|
|
360
|
-
`provideHttpClient()` so the test backend replaces the real backend:
|
|
361
|
-
|
|
362
|
-
```ts
|
|
363
|
-
import { provideHttpClient } from '@angular/common/http';
|
|
364
|
-
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
365
|
-
import { TestBed } from '@angular/core/testing';
|
|
366
|
-
import { provideRiviniaHttp, RiviniaHttp } from '@rivinia/http';
|
|
367
|
-
|
|
368
|
-
TestBed.configureTestingModule({
|
|
369
|
-
providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const api = TestBed.inject(RiviniaHttp);
|
|
373
|
-
const requests = TestBed.inject(HttpTestingController);
|
|
374
|
-
api
|
|
375
|
-
.get<unknown[]>('https://api.test/trips', {
|
|
376
|
-
unwrap: true,
|
|
377
|
-
unwrapKey: 'data',
|
|
378
|
-
})
|
|
379
|
-
.subscribe();
|
|
380
|
-
requests.expectOne('https://api.test/trips').flush({ data: [] });
|
|
381
|
-
requests.verify();
|
|
29
|
+
```bash
|
|
30
|
+
npm install @rivinia/http
|
|
31
|
+
ng add @rivinia/http
|
|
382
32
|
```
|
|
383
33
|
|
|
384
|
-
##
|
|
34
|
+
## Quick start
|
|
385
35
|
|
|
386
|
-
|
|
387
|
-
|
|
36
|
+
1. Configure `provideHttpClient()` and `provideRiviniaHttp()`.
|
|
37
|
+
2. Inject `RiviniaHttp` in a service and compose an endpoint URL.
|
|
38
|
+
3. Render the typed response in a feature.
|
|
388
39
|
|
|
389
|
-
|
|
390
|
-
// Before
|
|
391
|
-
provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
|
|
392
|
-
this.http.get<Trip[]>(url);
|
|
393
|
-
|
|
394
|
-
// After
|
|
395
|
-
provideRiviniaHttp();
|
|
396
|
-
this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
397
|
-
```
|
|
40
|
+
## Compatibility
|
|
398
41
|
|
|
399
|
-
|
|
400
|
-
|
|
42
|
+
| Angular | RxJS | Node |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `^22.0.0` | `^6.5.3 or ^7.4.0` | `^22.22.3 or ^24.15.0 or >=26.0.0` |
|
|
401
45
|
|
|
402
|
-
##
|
|
46
|
+
## Go further
|
|
403
47
|
|
|
404
|
-
|
|
48
|
+
| Topic | Read |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| URLs and services | [URLs and services](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.1/docs/http/urls-and-services.md) |
|
|
51
|
+
| Reactive resources | [Reactive resources](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.1/docs/http/resources.md) |
|
|
52
|
+
| Response envelopes and errors | [Response envelopes and errors](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.1/docs/http/envelopes-and-errors.md) |
|
|
53
|
+
| API reference | [API reference](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.1/docs/http/api.md) |
|
|
54
|
+
| Testing | [Testing](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.1/docs/http/testing.md) |
|
|
@@ -4,8 +4,8 @@ import { HttpClient, HttpContext, HttpParams, httpResource, provideHttpClient }
|
|
|
4
4
|
import { map } from "rxjs";
|
|
5
5
|
function joinUrl(baseUrl, endpoint) {
|
|
6
6
|
if (/^https?:\/\//i.test(endpoint)) return endpoint;
|
|
7
|
-
const base = baseUrl.
|
|
8
|
-
const path = endpoint.
|
|
7
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
8
|
+
const path = endpoint.replace(/^\/+/, "");
|
|
9
9
|
return path ? `${base}/${path}` : base;
|
|
10
10
|
}
|
|
11
11
|
var RiviniaHttpUnwrapError = class extends Error {
|
|
@@ -165,7 +165,7 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
165
165
|
}
|
|
166
166
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
167
167
|
minVersion: "12.0.0",
|
|
168
|
-
version: "22.2.
|
|
168
|
+
version: "22.2.3",
|
|
169
169
|
ngImport: i0,
|
|
170
170
|
type: RiviniaHttp,
|
|
171
171
|
deps: [],
|
|
@@ -173,14 +173,14 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
173
173
|
});
|
|
174
174
|
static ɵprov = i0.ɵɵngDeclareService({
|
|
175
175
|
minVersion: "22.0.0",
|
|
176
|
-
version: "22.2.
|
|
176
|
+
version: "22.2.3",
|
|
177
177
|
ngImport: i0,
|
|
178
178
|
type: RiviniaHttp
|
|
179
179
|
});
|
|
180
180
|
};
|
|
181
181
|
i0.ɵɵngDeclareClassMetadata({
|
|
182
182
|
minVersion: "12.0.0",
|
|
183
|
-
version: "22.2.
|
|
183
|
+
version: "22.2.3",
|
|
184
184
|
ngImport: i0,
|
|
185
185
|
type: RiviniaHttp,
|
|
186
186
|
decorators: [{ type: Service }]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/http-utils.ts","../../../projects/http/src/lib/http-envelope.ts","../../../projects/http/src/lib/http-config.ts","../../../projects/http/src/lib/rivinia-http.ts","../../../projects/http/src/lib/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from './http-options';\nimport { extractKey } from './http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport { RiviniaHttp } from './rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/utils/http-utils.ts","../../../projects/http/src/lib/models/http-envelope.ts","../../../projects/http/src/lib/models/http-config.ts","../../../projects/http/src/lib/services/rivinia-http.ts","../../../projects/http/src/lib/providers/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.replace(/\\/+$/, '');\n const path = endpoint.replace(/^\\/+/, '');\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from '../models/http-options';\nimport { extractKey } from '../utils/http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from '../models/http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport { RiviniaHttp } from '../services/rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Headless typed HTTP client for Angular: envelope unwrapping, plain Observables and httpResource.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
},
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"schematics": "./schematics/collection.json",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/abdelfattahqandil21-oss/Revinia.git",
|
|
28
|
+
"directory": "projects/http"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/abdelfattahqandil21-oss/Revinia/tree/http/v1.0.0/projects/http#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/abdelfattahqandil21-oss/Revinia/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
36
|
+
},
|
|
25
37
|
"module": "fesm2022/rivinia-http.mjs",
|
|
26
38
|
"typings": "types/rivinia-http.d.ts",
|
|
27
39
|
"exports": {
|
package/types/rivinia-http.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef, HttpResponse } from "@angular/common/http";
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
|
-
import { EnvironmentProviders,
|
|
3
|
+
import { EnvironmentProviders, Signal } from "@angular/core";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
5
5
|
/**
|
|
6
6
|
* Typed envelope failure. Thrown when a backend signals failure inside a
|
|
@@ -29,8 +29,6 @@ interface RiviniaEnvelopePolicy {
|
|
|
29
29
|
/** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */
|
|
30
30
|
readonly throwOnFailure?: boolean;
|
|
31
31
|
}
|
|
32
|
-
/** Throws when the envelope signals failure and the policy opts in. */
|
|
33
|
-
declare function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void;
|
|
34
32
|
/**
|
|
35
33
|
* Global configuration for `@rivinia/http`.
|
|
36
34
|
*
|
|
@@ -61,11 +59,6 @@ interface RiviniaHttpConfig {
|
|
|
61
59
|
*/
|
|
62
60
|
envelope?: RiviniaEnvelopePolicy;
|
|
63
61
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Internal configuration token. Consumers must use `provideRiviniaHttp()`.
|
|
66
|
-
* @internal
|
|
67
|
-
*/
|
|
68
|
-
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
69
62
|
/**
|
|
70
63
|
* A value, a signal of it, or a reactive function returning it — evaluated
|
|
71
64
|
* inside the `httpResource` request function, so signal reads stay reactive
|