@rivinia/http 0.5.0 → 0.5.2

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.
Files changed (2) hide show
  1. package/README.md +337 -132
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -3,198 +3,403 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/http.svg)](https://www.npmjs.com/package/@rivinia/http)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/http.svg)](https://www.npmjs.com/package/@rivinia/http)
5
5
 
6
- Headless typed HTTP client for Angular 22+. Configurable envelope
7
- unwrapping, plain Observables **and** `httpResource` — URLs always used as
8
- given, zero styling, zero backend opinions, interceptors left to your app.
6
+ `@rivinia/http` is a thin, typed Angular HTTP client for plain `Observable`
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.
9
10
 
10
- - Version: **0.5.0**
11
- - Requires: **Angular `^22.0.0`**, `rxjs` (same range Angular uses), `tslib`
12
- - Since Angular 21 `HttpClient` (fetch backend) is injectable with zero setup.
13
- Call `provideHttpClient()` / pass `httpFeatures` only when you configure
14
- interceptors or other backend features.
11
+ - Package version: **0.5.2**
12
+ - Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
13
+ as a dependency.
15
14
 
16
- ## Install
15
+ ## Install and configure
17
16
 
18
17
  ```bash
19
18
  npm install @rivinia/http
20
- ng add @rivinia/http # wires app.config.ts (no questions: unwrapping is per call)
19
+ ng add @rivinia/http
21
20
  ```
22
21
 
23
- ## Quick start — pick ONE backend style
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.
24
29
 
25
- ```ts
26
- // app.config.ts — style A: nothing to configure (Angular ≥21 default)
27
- providers: [provideRiviniaHttp()];
28
-
29
- // style B: single call with interceptors (do NOT also call provideHttpClient)
30
- providers: [provideRiviniaHttp({ httpFeatures: [withInterceptors([authInterceptor])] })];
30
+ The equivalent minimal standalone setup is:
31
31
 
32
- // style C: explicit backend config, like the official setup guide
33
- providers: [provideHttpClient(withInterceptors([authInterceptor])), provideRiviniaHttp()];
32
+ ```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
+ };
34
41
  ```
35
42
 
36
- ## URLs: yours, always
37
-
38
- The library never joins, prefixes, or rewrites URLs. Compose them at the
39
- call site from wherever you want — typically your environment files
40
- (the BRT pattern):
43
+ Use the same `provideHttpClient` setup to add app-owned interceptors:
41
44
 
42
45
  ```ts
43
- // src/environments/environment.ts / environment.prod.ts
44
- export const environment = { apiUrl: 'https://api.example.com' };
45
-
46
- // feature service
47
- import { joinUrl } from '@rivinia/http';
48
-
49
- export class TripService {
50
- private api = injectRiviniaHttp();
51
- private url = (path: string) => joinUrl(environment.apiUrl, path);
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
+ ```
52
55
 
53
- list = () => this.api.get<Trip[]>(this.url('trips'));
54
- create = (dto: CreateTrip) => this.api.post<Trip>(this.url('trips'), dto);
55
- }
56
+ Alternatively, pass features through `provideRiviniaHttp`:
56
57
 
57
- // Reactive resource (auto refetch, loading/error signals)
58
- export class TripPage {
59
- page = signal(1);
60
- trips = this.api.getResource<Trip[]>(() => this.url(`trips?page=${this.page()}`));
61
- }
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
+ };
62
67
  ```
63
68
 
64
- ```html
65
- @if (trips.hasValue()) { @for (t of trips.value(); track t.id) {
66
- <p>{{ t.title }}</p>
67
- } } @else if (trips.isLoading()) {
68
- <p>Loading…</p>
69
- }
70
- ```
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`.
71
74
 
72
- ## Envelope unwrapping (explicit, per call)
75
+ ## URLs and typed service calls
73
76
 
74
- The library assumes nothing about your backend: responses pass through raw
75
- unless the call opts in. There is no global unwrap configuration and no
76
- default key — every unwrapping call states both facts:
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:
77
80
 
78
81
  ```ts
79
- this.api.get<Raw>(url); // raw body (default)
80
- this.api.get<Legacy>(url, { unwrap: true, unwrapKey: 'result' }); // explicit
82
+ // src/environments/environment.ts
83
+ export const environment = {
84
+ apiUrl: 'https://api.example.com',
85
+ } as const;
81
86
  ```
82
87
 
83
- The type system enforces the contract: `{ unwrap: true }` without a key,
84
- a key alone, and `{ unwrap: false, unwrapKey }` all fail compilation.
85
- Different shapes per API in the same app:
86
-
87
88
  ```ts
88
- export class MixedService {
89
- private api = injectRiviniaHttp();
89
+ // src/app/trip.service.ts
90
+ import { Service } from '@angular/core';
91
+ import type { HttpResponse } from '@angular/common/http';
92
+ import { Observable } from 'rxjs';
93
+ 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
+ }
90
102
 
91
- // main backend: { data: … } (explicit key per call)
92
- trips = () => this.api.get<Trip[]>(fullUrl('trips'), { unwrap: true, unwrapKey: 'data' });
103
+ export interface TripDraft {
104
+ title: string;
105
+ departureDate: string;
106
+ }
93
107
 
94
- // legacy backend: { result: … }
95
- legacy = () => this.api.get<Old[]>(fullUrl('old'), { unwrap: true, unwrapKey: 'result' });
108
+ interface ApiEnvelope<T> {
109
+ data: T;
110
+ }
96
111
 
97
- // webhook endpoint: raw body (default — nothing to say)
98
- ping = () => this.api.get<string>(fullUrl('ping'));
112
+ @Service()
113
+ export class TripApi {
114
+ private readonly http = injectRiviniaHttp();
115
+ private readonly tripsUrl = joinUrl(environment.apiUrl, 'trips');
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
+ }
99
179
  }
100
180
  ```
101
181
 
102
- A missing key never passes the body through: it throws
103
- `RiviniaHttpUnwrapError` (with `key` + `endpoint`; the raw `body` is attached
104
- for diagnosis but never interpolated into the message). Catch it like any
105
- HTTP error — in `getResource` it surfaces as `resource.error()`:
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`. |
106
201
 
107
202
  ```ts
108
- this.api.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' }).subscribe({
109
- next: (trips) => (this.list = trips),
110
- error: (e) => this.fail(e), // RiviniaHttpUnwrapError | HttpErrorResponse
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);
111
213
  });
112
214
  ```
113
215
 
114
- `getResource` applies the same rules through `parse` (your own `parse` runs
115
- after unwrapping — ideal for Zod/Valibot validation).
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.
116
224
 
117
- ## Envelope failure validation (opt-in)
225
+ ## Reactive resources
118
226
 
119
- Backends that report failure inside HTTP 200 (`{ data: null, isSuccess: false }`)
120
- must never flow into `next()` as success — critical in payment flows:
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.
121
232
 
122
233
  ```ts
123
- provideRiviniaHttp({ envelope: {} }); // whole app (isSuccess/error keys)
124
- this.api.get(url, { envelope: { successKey: 'ok', errorKey: 'message' } });
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
+ standalone: true,
243
+ templateUrl: './trip-list.component.html',
244
+ })
245
+ export class TripListComponent {
246
+ private readonly http = injectRiviniaHttp();
247
+ readonly page = signal(1);
248
+ readonly trips = this.http.getResource<Trip[]>(joinUrl(environment.apiUrl, 'trips'), {
249
+ params: () => ({ page: this.page() }),
250
+ unwrap: true,
251
+ unwrapKey: 'data',
252
+ });
253
+
254
+ nextPage(): void {
255
+ this.page.update((value) => value + 1);
256
+ }
257
+ }
125
258
  ```
126
259
 
127
- A failed envelope throws `RiviniaHttpEnvelopeError` (`body` + `code` attached;
128
- in `getResource` it surfaces as `resource.error()`). Validation runs on the raw
129
- body before unwrapping, independent of `unwrap`. Per-call policy replaces any
130
- global one wholesale.
131
-
132
- ## API
133
-
134
- | Export | Kind | Purpose |
135
- | ----------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
- | `provideRiviniaHttp(config?)` | provider | `{ envelope?, httpFeatures? }` (no unwrap config — per call only) |
137
- | `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, `getResource` |
138
- | `injectRiviniaHttp()` | function | shorthand for `inject(RiviniaHttp)` |
139
- | `joinUrl(base, endpoint)` | util | slash-safe join; absolute URLs pass through |
140
- | `unwrapAs(body, key)` | util | strict single-level extraction — throws `RiviniaHttpUnwrapError` on missing key (nested keys preserved) |
141
- | `extractKey(body, key)` | util | `unwrapAs` without the generic cast (same strictness) |
142
- | types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaRawOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `RiviniaEnvelopePolicy`, `ReactiveInput`, `QueryParamsInput` |
260
+ ```html
261
+ <!-- src/app/trip-list.component.html -->
262
+ @if (trips.isLoading()) {
263
+ <p>Loading trips…</p>
264
+ } @else if (trips.hasValue()) {
265
+ <ul>
266
+ @for (trip of trips.value(); track trip.id) {
267
+ <li>{{ trip.title }} — {{ trip.departureDate }} ({{ trip.status }})</li>
268
+ }
269
+ </ul>
270
+ } @else if (trips.error()) {
271
+ <p role="alert">Could not load trips. Try again.</p>
272
+ }
273
+ <button type="button" (click)="nextPage()">Next page</button>
274
+ ```
143
275
 
144
- `getResponse`/`postResponse` return the raw `HttpResponse` (status + headers,
145
- body never unwrapped — and their options REJECT unwrap fields at compile
146
- time via `RiviniaRawOptions`; compose `unwrapAs` yourself if needed). A configured
147
- `envelope` policy still validates the body on these paths: a globally declared
148
- failure rule holds on every JSON path, never just the unwrapped ones. `getText`
149
- returns raw text; binary/text bodies cannot carry envelopes, so the policy does
150
- not apply there. In `getResource`, the URL, `params`, `headers` and `context`
151
- accept values, signals or reactive functions (everything else is static).
276
+ In a real app, share the Trip model through a model module. `parse` is optional;
277
+ when provided it runs after envelope validation and unwrapping, and can perform
278
+ runtime validation (for example with Zod or Valibot). A parse error, HTTP
279
+ error, or library envelope error is exposed through `trips.error()`.
280
+ `defaultValue` supplies a value while idle/loading. `reportProgress` is
281
+ forwarded to the resource request; for progress events on an `Observable`,
282
+ use Angular `HttpClient` directly.
152
283
 
153
- `getBlob` is never unwrapped. `HttpContext` passes through untouched, so
154
- app interceptors (auth opt-out, error toasts) keep working. For caching,
155
- polling, retry and TanStack Query interop see [RECIPES.md](./RECIPES.md).
284
+ ## Response envelopes and errors
156
285
 
157
- ## Type holes (honest, by design)
286
+ By default, all JSON response bodies are returned raw, even when they contain
287
+ a field named `data`. To extract a payload, opt in on that call and provide a
288
+ key:
158
289
 
159
- `unwrapAs<T>` is an unchecked cast — a wrong `T` compiles and mistypes the
160
- payload at runtime. What IS checked is generic flow plus the unwrap contract
161
- itself (see `http-type-safety.spec.ts` `@ts-expect-error` tests). For untyped
162
- backends, pair unwrapping with `envelope` validation or a `parse` hook
163
- (Zod/Valibot — see [RECIPES.md](./RECIPES.md)).
290
+ ```ts
291
+ this.http.get<Trip[]>(url); // raw response body
292
+ this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
293
+ ```
164
294
 
165
- ## Migration from global unwrapping (0.4.0 → 0.5.0, breaking)
295
+ The type contract requires `unwrapKey` when `unwrap: true`, and rejects a key
296
+ when unwrapping is off. Extraction is strict and single-level: the response
297
+ must be a non-null, non-array object with that own property. A missing key,
298
+ primitive/array/null body, or empty key throws `RiviniaHttpUnwrapError`;
299
+ it does not silently return the original body. Present values such as `null`,
300
+ `false`, `0`, and `''` are returned as-is. Nested data and sibling fields are
301
+ not modified. `unwrapAs<T>` performs the same one-level extraction outside a
302
+ request; its generic type is an unchecked cast, not runtime validation.
166
303
 
167
- Global `unwrap`/`unwrapKey` configuration is removed. Previously:
304
+ An optional envelope policy checks backend-declared failures before
305
+ unwrapping. The default keys are `isSuccess` and `error`; only an object whose
306
+ `isSuccess` value is exactly `false` is considered failed:
168
307
 
169
308
  ```ts
170
- provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
171
- this.api.get<Trip[]>(url); // arrived unwrapped
309
+ // Apply to JSON request paths made by this configured client.
310
+ provideRiviniaHttp({ envelope: {} });
311
+
312
+ // Or specify a per-request policy; it replaces the global policy.
313
+ this.http.get<Trip[]>(url, {
314
+ envelope: { successKey: 'ok', errorKey: 'message' },
315
+ });
172
316
  ```
173
317
 
174
- Now every unwrapping call states its key explicitly:
318
+ When a failure is detected, `RiviniaHttpEnvelopeError` is sent through the
319
+ Observable error channel or appears as the resource error. It carries the raw
320
+ `body` and failure `code`. Set `throwOnFailure: false` for a passive policy.
321
+ Policy validation runs before unwrapping, including on `getResponse` and
322
+ `postResponse`. Those full-response methods return raw `HttpResponse<T>`
323
+ (status, headers, and unwrapped body is not applied). `getBlob` and `getText`
324
+ also return raw bodies and do not run envelope validation.
325
+
326
+ The generic type does not validate the server response. For untrusted or
327
+ loosely typed APIs, validate the extracted value with `getResource`'s `parse`
328
+ hook, or validate in the Observable pipeline in app code. Do not log
329
+ `RiviniaHttpUnwrapError.body` or `RiviniaHttpEnvelopeError.body` without
330
+ considering that they may contain sensitive data.
331
+
332
+ ## Public API
333
+
334
+ | Export | Kind | Purpose |
335
+ | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
336
+ | `provideRiviniaHttp(config?)` | provider | Sets `envelope` and optional `httpFeatures`; no global unwrap option. |
337
+ | `RiviniaHttp` | class | `get`, `post`, `put`, `patch`, `delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, and `getResource`. |
338
+ | `injectRiviniaHttp()` | function | Injection-context shorthand for `inject(RiviniaHttp)`. |
339
+ | `joinUrl(base, endpoint)` | utility | Removes one boundary slash when joining; absolute HTTP(S) endpoints pass through. |
340
+ | `unwrapAs(body, key)` | utility | Strict, single-level typed extraction. |
341
+ | `extractKey(body, key)` | utility | Strict extraction without a generic cast. |
342
+ | `RiviniaHttpUnwrapError` | class | Error for invalid or missing unwrap properties. |
343
+ | `RiviniaHttpEnvelopeError` | class | Error for a failed configured response envelope. |
344
+ | `RiviniaHttpConfig` | type | `envelope` and `httpFeatures` provider configuration. |
345
+ | `RiviniaEnvelopePolicy` | type | Success key, error key, and failure policy settings. |
346
+ | `RiviniaRequestOptions` | type | Options for requests that may unwrap. |
347
+ | `RiviniaRawOptions` | type | Options for methods that never unwrap. |
348
+ | `RiviniaDeleteOptions` | type | Delete options including an optional request body. |
349
+ | `RiviniaDeleteRequestOptions` | type | Delete options including the unwrap contract. |
350
+ | `RiviniaRequestBase` | type | Shared transport and envelope options. |
351
+ | `RiviniaResourceOptions` | type | Reactive resource options and unwrap contract. |
352
+ | `RiviniaResourceBase` | type | Shared resource-specific options. |
353
+ | `ReactiveInput` | type | Static value, signal, or reactive function input. |
354
+ | `QueryParamsInput` | type | `HttpParams` or a primitive-valued query object. |
355
+ | `RiviniaUnwrapOn` | type | Required key form for `{ unwrap: true }`. |
356
+ | `RiviniaUnwrapOff` | type | Form where an unwrap key is disallowed. |
357
+
358
+ ## Testing
359
+
360
+ Use Angular's HTTP testing providers. The testing provider follows
361
+ `provideHttpClient()` so the test backend replaces the real backend:
175
362
 
176
363
  ```ts
177
- provideRiviniaHttp();
178
- this.api.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
179
- ```
364
+ import { provideHttpClient } from '@angular/common/http';
365
+ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
366
+ import { TestBed } from '@angular/core/testing';
367
+ import { provideRiviniaHttp, RiviniaHttp } from '@rivinia/http';
368
+
369
+ TestBed.configureTestingModule({
370
+ providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
371
+ });
180
372
 
181
- Migrate each applicable call, or the body arrives wrapped (raw). Missing
182
- keys now throw `RiviniaHttpUnwrapError` instead of passing the body through.
183
- `ng add` no longer asks about unwrapping and emits bare
184
- `provideRiviniaHttp()` — there is no automatic migration of call sites.
373
+ const api = TestBed.inject(RiviniaHttp);
374
+ const requests = TestBed.inject(HttpTestingController);
375
+ api
376
+ .get<unknown[]>('https://api.test/trips', {
377
+ unwrap: true,
378
+ unwrapKey: 'data',
379
+ })
380
+ .subscribe();
381
+ requests.expectOne('https://api.test/trips').flush({ data: [] });
382
+ requests.verify();
383
+ ```
185
384
 
186
- ## Testing your app code
385
+ ## Migration from 0.4.x
187
386
 
188
- Standard Angular tooling — nothing custom to learn:
387
+ Global `unwrap` and `unwrapKey` settings were removed in 0.5.0. Move the
388
+ unwrap decision and key to each applicable request:
189
389
 
190
390
  ```ts
191
- TestBed.configureTestingModule({
192
- providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
193
- });
194
- api.get('https://api.test/users').subscribe();
195
- TestBed.inject(HttpTestingController).expectOne('https://api.test/users').flush({ data: [] });
391
+ // Before
392
+ provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
393
+ this.http.get<Trip[]>(url);
394
+
395
+ // After
396
+ provideRiviniaHttp();
397
+ this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
196
398
  ```
197
399
 
400
+ The `ng add` schematic now emits bare providers and does not migrate request
401
+ call sites. Without the per-request options, the response remains raw.
402
+
198
403
  ## License
199
404
 
200
405
  MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/http",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Headless typed HTTP client for Angular: envelope unwrapping, plain Observables and httpResource.",
5
5
  "keywords": [
6
6
  "angular",