@rivinia/http 0.5.0 → 0.5.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/README.md +249 -132
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,197 +3,314 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
4
4
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
-
|
|
11
|
-
- Requires
|
|
12
|
-
|
|
13
|
-
Call `provideHttpClient()` / pass `httpFeatures` only when you configure
|
|
14
|
-
interceptors or other backend features.
|
|
11
|
+
- Prepared patch: **0.5.1** (latest published: **0.5.0**)
|
|
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
|
|
19
|
+
ng add @rivinia/http
|
|
21
20
|
```
|
|
22
21
|
|
|
23
|
-
|
|
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:
|
|
24
31
|
|
|
25
32
|
```ts
|
|
26
|
-
// app.config.ts
|
|
27
|
-
|
|
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
|
+
```
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
providers: [provideRiviniaHttp({ httpFeatures: [withInterceptors([authInterceptor])] })];
|
|
43
|
+
Use the same `provideHttpClient` setup to add app-owned interceptors:
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
};
|
|
34
54
|
```
|
|
35
55
|
|
|
36
|
-
|
|
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):
|
|
56
|
+
Alternatively, pass features through `provideRiviniaHttp`:
|
|
41
57
|
|
|
42
58
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
```
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
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`.
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
private api = injectRiviniaHttp();
|
|
51
|
-
private url = (path: string) => joinUrl(environment.apiUrl, path);
|
|
75
|
+
## URLs and typed service calls
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
import { Injectable } from '@angular/core';
|
|
91
|
+
import { Observable } from 'rxjs';
|
|
92
|
+
import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
|
|
93
|
+
import { environment } from '../environments/environment';
|
|
94
|
+
|
|
95
|
+
interface Trip {
|
|
96
|
+
id: number;
|
|
97
|
+
title: string;
|
|
55
98
|
}
|
|
56
99
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
page = signal(1);
|
|
60
|
-
trips = this.api.getResource<Trip[]>(() => this.url(`trips?page=${this.page()}`));
|
|
100
|
+
interface CreateTrip {
|
|
101
|
+
title: string;
|
|
61
102
|
}
|
|
62
|
-
```
|
|
63
103
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
104
|
+
@Injectable({ providedIn: 'root' })
|
|
105
|
+
export class TripService {
|
|
106
|
+
private readonly http = injectRiviniaHttp();
|
|
107
|
+
private readonly tripsUrl = joinUrl(environment.apiUrl, 'trips');
|
|
108
|
+
|
|
109
|
+
list(): Observable<Trip[]> {
|
|
110
|
+
return this.http.get<Trip[]>(this.tripsUrl, {
|
|
111
|
+
params: { page: 1, search: 'coast' },
|
|
112
|
+
unwrap: true,
|
|
113
|
+
unwrapKey: 'data',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
create(input: CreateTrip): Observable<Trip> {
|
|
118
|
+
return this.http.post<Trip>(this.tripsUrl, input, { unwrap: true, unwrapKey: 'data' });
|
|
119
|
+
}
|
|
69
120
|
}
|
|
70
121
|
```
|
|
71
122
|
|
|
72
|
-
|
|
123
|
+
Here the API is expected to respond with `{ "data": [...] }` for the list and
|
|
124
|
+
`{ "data": { "id": 1, "title": "..." } }` for the create. `Observable<T>` is
|
|
125
|
+
the compile-time type of the result; it does not validate the server payload.
|
|
126
|
+
`joinUrl` removes one boundary slash and passes absolute `http://` or
|
|
127
|
+
`https://` endpoints through unchanged. Use `params` for query values:
|
|
128
|
+
`null` and `undefined` entries are omitted, while strings, numbers, and
|
|
129
|
+
booleans are stringified.
|
|
73
130
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
`get`, `post`, `put`, `patch`, and `delete` return typed `Observable`s.
|
|
132
|
+
Mutation bodies are passed to Angular unchanged; `delete` accepts an optional
|
|
133
|
+
`body` in its options:
|
|
77
134
|
|
|
78
135
|
```ts
|
|
79
|
-
this.
|
|
80
|
-
|
|
136
|
+
this.http.delete<void>(joinUrl(environment.apiUrl, 'trips/1'), {
|
|
137
|
+
body: { reason: 'duplicate' },
|
|
138
|
+
});
|
|
81
139
|
```
|
|
82
140
|
|
|
83
|
-
The
|
|
84
|
-
|
|
85
|
-
|
|
141
|
+
The standard request options also pass `headers`, `HttpContext`, and
|
|
142
|
+
`withCredentials` through to Angular's `HttpClient`. Use `HttpContext` with
|
|
143
|
+
your own interceptors for app-specific controls such as opting out of auth.
|
|
86
144
|
|
|
87
|
-
|
|
88
|
-
export class MixedService {
|
|
89
|
-
private api = injectRiviniaHttp();
|
|
145
|
+
## Reactive resources
|
|
90
146
|
|
|
91
|
-
|
|
92
|
-
|
|
147
|
+
`getResource` uses Angular's `httpResource`. The endpoint can be a string, a
|
|
148
|
+
signal, or a reactive function; `undefined` disables the request. URL,
|
|
149
|
+
`params`, `headers`, and `context` can be reactive inputs and trigger a new
|
|
150
|
+
request when their signal values change. Other options are read once when
|
|
151
|
+
the resource is created.
|
|
93
152
|
|
|
94
|
-
|
|
95
|
-
|
|
153
|
+
```ts
|
|
154
|
+
import { Component, signal } from '@angular/core';
|
|
155
|
+
import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
|
|
156
|
+
import { environment } from '../environments/environment';
|
|
96
157
|
|
|
97
|
-
|
|
98
|
-
|
|
158
|
+
interface Trip {
|
|
159
|
+
id: number;
|
|
160
|
+
title: string;
|
|
99
161
|
}
|
|
100
|
-
```
|
|
101
|
-
|
|
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()`:
|
|
106
162
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
163
|
+
@Component({
|
|
164
|
+
selector: 'app-trip-list',
|
|
165
|
+
template: `
|
|
166
|
+
@if (trips.isLoading()) {
|
|
167
|
+
<p>Loading…</p>
|
|
168
|
+
} @else if (trips.hasValue()) {
|
|
169
|
+
@for (trip of trips.value(); track trip.id) {
|
|
170
|
+
<p>{{ trip.title }}</p>
|
|
171
|
+
}
|
|
172
|
+
} @else if (trips.error()) {
|
|
173
|
+
<p>Could not load trips.</p>
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
})
|
|
177
|
+
export class TripListComponent {
|
|
178
|
+
private readonly http = injectRiviniaHttp();
|
|
179
|
+
readonly page = signal(1);
|
|
180
|
+
readonly trips = this.http.getResource<Trip[]>(joinUrl(environment.apiUrl, 'trips'), {
|
|
181
|
+
params: () => ({ page: this.page() }),
|
|
182
|
+
unwrap: true,
|
|
183
|
+
unwrapKey: 'data',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
112
186
|
```
|
|
113
187
|
|
|
114
|
-
|
|
115
|
-
after
|
|
188
|
+
In a real app, share the Trip model through a model module. `parse` is optional; when
|
|
189
|
+
provided it runs after envelope validation and unwrapping, and can perform
|
|
190
|
+
runtime validation (for example with Zod or Valibot). A parse error, HTTP
|
|
191
|
+
error, or library envelope error is exposed through `trips.error()`.
|
|
192
|
+
`defaultValue` supplies a value while idle/loading. `reportProgress` is
|
|
193
|
+
forwarded to the resource request; for progress events on an `Observable`,
|
|
194
|
+
use Angular `HttpClient` directly.
|
|
116
195
|
|
|
117
|
-
##
|
|
196
|
+
## Response envelopes and errors
|
|
118
197
|
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
By default, all JSON response bodies are returned raw, even when they contain
|
|
199
|
+
a field named `data`. To extract a payload, opt in on that call and provide a
|
|
200
|
+
key:
|
|
121
201
|
|
|
122
202
|
```ts
|
|
123
|
-
|
|
124
|
-
this.
|
|
203
|
+
this.http.get<Trip[]>(url); // raw response body
|
|
204
|
+
this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
125
205
|
```
|
|
126
206
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
207
|
+
The type contract requires `unwrapKey` when `unwrap: true`, and rejects a key
|
|
208
|
+
when unwrapping is off. Extraction is strict and single-level: the response
|
|
209
|
+
must be a non-null, non-array object with that own property. A missing key,
|
|
210
|
+
primitive/array/null body, or empty key throws `RiviniaHttpUnwrapError`;
|
|
211
|
+
it does not silently return the original body. Present values such as `null`,
|
|
212
|
+
`false`, `0`, and `''` are returned as-is. Nested data and sibling fields are
|
|
213
|
+
not modified. `unwrapAs<T>` performs the same one-level extraction outside a
|
|
214
|
+
request; its generic type is an unchecked cast, not runtime validation.
|
|
131
215
|
|
|
132
|
-
|
|
216
|
+
An optional envelope policy checks backend-declared failures before
|
|
217
|
+
unwrapping. The default keys are `isSuccess` and `error`; only an object whose
|
|
218
|
+
`isSuccess` value is exactly `false` is considered failed:
|
|
133
219
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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` |
|
|
220
|
+
```ts
|
|
221
|
+
// Apply to JSON request paths made by this configured client.
|
|
222
|
+
provideRiviniaHttp({ envelope: {} });
|
|
143
223
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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).
|
|
224
|
+
// Or specify a per-request policy; it replaces the global policy.
|
|
225
|
+
this.http.get<Trip[]>(url, {
|
|
226
|
+
envelope: { successKey: 'ok', errorKey: 'message' },
|
|
227
|
+
});
|
|
228
|
+
```
|
|
152
229
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
230
|
+
When a failure is detected, `RiviniaHttpEnvelopeError` is sent through the
|
|
231
|
+
Observable error channel or appears as the resource error. It carries the raw
|
|
232
|
+
`body` and failure `code`. Set `throwOnFailure: false` for a passive policy.
|
|
233
|
+
Policy validation runs before unwrapping, including on `getResponse` and
|
|
234
|
+
`postResponse`. Those full-response methods return raw `HttpResponse<T>`
|
|
235
|
+
(status, headers, and unwrapped body is not applied). `getBlob` and `getText`
|
|
236
|
+
also return raw bodies and do not run envelope validation.
|
|
237
|
+
|
|
238
|
+
The generic type does not validate the server response. For untrusted or
|
|
239
|
+
loosely typed APIs, validate the extracted value with `getResource`'s `parse`
|
|
240
|
+
hook, or validate in the Observable pipeline in app code. Do not log
|
|
241
|
+
`RiviniaHttpUnwrapError.body` or `RiviniaHttpEnvelopeError.body` without
|
|
242
|
+
considering that they may contain sensitive data.
|
|
243
|
+
|
|
244
|
+
## Public API
|
|
245
|
+
|
|
246
|
+
| Export | Kind | Purpose |
|
|
247
|
+
| ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
248
|
+
| `provideRiviniaHttp(config?)` | provider | Sets `envelope` and optional `httpFeatures`; no global unwrap option. |
|
|
249
|
+
| `RiviniaHttp` | class | `get`, `post`, `put`, `patch`, `delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, and `getResource`. |
|
|
250
|
+
| `injectRiviniaHttp()` | function | Injection-context shorthand for `inject(RiviniaHttp)`. |
|
|
251
|
+
| `joinUrl(base, endpoint)` | utility | Removes one boundary slash when joining; absolute HTTP(S) endpoints pass through. |
|
|
252
|
+
| `unwrapAs(body, key)` | utility | Strict, single-level typed extraction. |
|
|
253
|
+
| `extractKey(body, key)` | utility | Strict extraction without a generic cast. |
|
|
254
|
+
| `RiviniaHttpUnwrapError` | class | Error for invalid or missing unwrap properties. |
|
|
255
|
+
| `RiviniaHttpEnvelopeError` | class | Error for a failed configured response envelope. |
|
|
256
|
+
| `RiviniaHttpConfig` | type | `envelope` and `httpFeatures` provider configuration. |
|
|
257
|
+
| `RiviniaEnvelopePolicy` | type | Success key, error key, and failure policy settings. |
|
|
258
|
+
| `RiviniaRequestOptions` | type | Options for requests that may unwrap. |
|
|
259
|
+
| `RiviniaRawOptions` | type | Options for methods that never unwrap. |
|
|
260
|
+
| `RiviniaDeleteOptions` | type | Delete options including an optional request body. |
|
|
261
|
+
| `RiviniaDeleteRequestOptions` | type | Delete options including the unwrap contract. |
|
|
262
|
+
| `RiviniaRequestBase` | type | Shared transport and envelope options. |
|
|
263
|
+
| `RiviniaResourceOptions` | type | Reactive resource options and unwrap contract. |
|
|
264
|
+
| `RiviniaResourceBase` | type | Shared resource-specific options. |
|
|
265
|
+
| `ReactiveInput` | type | Static value, signal, or reactive function input. |
|
|
266
|
+
| `QueryParamsInput` | type | `HttpParams` or a primitive-valued query object. |
|
|
267
|
+
| `RiviniaUnwrapOn` | type | Required key form for `{ unwrap: true }`. |
|
|
268
|
+
| `RiviniaUnwrapOff` | type | Form where an unwrap key is disallowed. |
|
|
269
|
+
|
|
270
|
+
## Testing
|
|
271
|
+
|
|
272
|
+
Use Angular's HTTP testing providers. The testing provider follows
|
|
273
|
+
`provideHttpClient()` so the test backend replaces the real backend:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
277
|
+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
278
|
+
import { TestBed } from '@angular/core/testing';
|
|
279
|
+
import { provideRiviniaHttp, RiviniaHttp } from '@rivinia/http';
|
|
156
280
|
|
|
157
|
-
|
|
281
|
+
TestBed.configureTestingModule({
|
|
282
|
+
providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
|
|
283
|
+
});
|
|
158
284
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
285
|
+
const api = TestBed.inject(RiviniaHttp);
|
|
286
|
+
const requests = TestBed.inject(HttpTestingController);
|
|
287
|
+
api
|
|
288
|
+
.get<unknown[]>('https://api.test/trips', {
|
|
289
|
+
unwrap: true,
|
|
290
|
+
unwrapKey: 'data',
|
|
291
|
+
})
|
|
292
|
+
.subscribe();
|
|
293
|
+
requests.expectOne('https://api.test/trips').flush({ data: [] });
|
|
294
|
+
requests.verify();
|
|
295
|
+
```
|
|
164
296
|
|
|
165
|
-
## Migration from
|
|
297
|
+
## Migration from 0.4.x
|
|
166
298
|
|
|
167
|
-
Global `unwrap
|
|
299
|
+
Global `unwrap` and `unwrapKey` settings were removed in 0.5.0. Move the
|
|
300
|
+
unwrap decision and key to each applicable request:
|
|
168
301
|
|
|
169
302
|
```ts
|
|
303
|
+
// Before
|
|
170
304
|
provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
|
|
171
|
-
this.
|
|
172
|
-
```
|
|
305
|
+
this.http.get<Trip[]>(url);
|
|
173
306
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```ts
|
|
307
|
+
// After
|
|
177
308
|
provideRiviniaHttp();
|
|
178
|
-
this.
|
|
309
|
+
this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
179
310
|
```
|
|
180
311
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
`ng add` no longer asks about unwrapping and emits bare
|
|
184
|
-
`provideRiviniaHttp()` — there is no automatic migration of call sites.
|
|
185
|
-
|
|
186
|
-
## Testing your app code
|
|
187
|
-
|
|
188
|
-
Standard Angular tooling — nothing custom to learn:
|
|
189
|
-
|
|
190
|
-
```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: [] });
|
|
196
|
-
```
|
|
312
|
+
The `ng add` schematic now emits bare providers and does not migrate request
|
|
313
|
+
call sites. Without the per-request options, the response remains raw.
|
|
197
314
|
|
|
198
315
|
## License
|
|
199
316
|
|