@rivinia/http 1.0.0 → 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.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +28 -386
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Documentation: README restructured into docs/http/ pages; stale pre-1.0 wording removed.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  - First stable release. URL joining collapses repeated boundary slashes; request URLs otherwise remain consumer-controlled.
package/README.md CHANGED
@@ -3,410 +3,52 @@
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
- <p align="center">
7
- <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/http/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
8
- </p>
6
+ A typed Angular HTTP client for Observable requests and reactive resources.
9
7
 
10
- `@rivinia/http` is a thin, typed Angular HTTP client for plain `Observable`
11
- requests and reactive `httpResource` requests. It leaves URLs, backend
12
- contracts, and interceptors under app control. Response bodies pass through
13
- unchanged unless a request explicitly asks to unwrap one.
14
-
15
- - Package version: **1.0.0**
16
- - Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
17
- as a dependency.
18
-
19
- ## Install and configure
20
-
21
- ```bash
22
- npm install @rivinia/http
23
- ng add @rivinia/http
24
- ```
25
-
26
- For a standalone app, `ng add` adds `provideHttpClient()` and
27
- `provideRiviniaHttp()` to the selected project's `src/app/app.config.ts`.
28
- It has no prompts and does not set a base URL or an envelope policy. It is
29
- idempotent and only edits an existing app config containing a `providers: []`
30
- array; if no config is found it warns and skips, so add the providers
31
- manually. In a multi-project workspace it targets `defaultProject` or the
32
- first project; use `--project <name>` to select a project.
33
-
34
- The equivalent minimal standalone setup is:
35
-
36
- ```ts
37
- // src/app/app.config.ts
38
- import { provideHttpClient } from '@angular/common/http';
39
- import { ApplicationConfig } from '@angular/core';
40
- import { provideRiviniaHttp } from '@rivinia/http';
41
-
42
- export const appConfig: ApplicationConfig = {
43
- providers: [provideHttpClient(), provideRiviniaHttp()],
44
- };
45
- ```
46
-
47
- Use the same `provideHttpClient` setup to add app-owned interceptors:
48
-
49
- ```ts
50
- import { ApplicationConfig } from '@angular/core';
51
- import { provideHttpClient, withInterceptors } from '@angular/common/http';
52
- import { provideRiviniaHttp } from '@rivinia/http';
53
- import { authInterceptor } from './auth.interceptor';
54
-
55
- export const appConfig: ApplicationConfig = {
56
- providers: [provideHttpClient(withInterceptors([authInterceptor])), provideRiviniaHttp()],
57
- };
58
- ```
59
-
60
- Alternatively, pass features through `provideRiviniaHttp`:
8
+ Package version: **1.0.1**
61
9
 
62
10
  ```ts
63
- import { ApplicationConfig } from '@angular/core';
64
- import { withInterceptors } from '@angular/common/http';
65
- import { provideRiviniaHttp } from '@rivinia/http';
66
- import { authInterceptor } from './auth.interceptor';
67
-
68
- export const appConfig: ApplicationConfig = {
69
- providers: [provideRiviniaHttp({ httpFeatures: [withInterceptors([authInterceptor])] })],
70
- };
71
- ```
72
-
73
- Choose one backend setup: when using `httpFeatures`, do not also call
74
- `provideHttpClient()`; otherwise the HTTP backend and its default XSRF
75
- interceptor can be registered twice. If the app already configures
76
- `provideHttpClient(...)`, keep that and call `provideRiviniaHttp()` without
77
- `httpFeatures`.
78
-
79
- ## URLs and typed service calls
80
-
81
- The client sends the endpoint string exactly as given. It never prepends a
82
- base URL or rewrites a path. Compose URLs yourself, for example with
83
- `joinUrl` and the app's environment:
84
-
85
- ```ts
86
- // src/environments/environment.ts
87
- export const environment = {
88
- apiUrl: 'https://api.example.com',
89
- } as const;
90
- ```
91
-
92
- ```ts
93
- // src/app/trip.service.ts
94
11
  import { Service } from '@angular/core';
95
- import type { HttpResponse } from '@angular/common/http';
96
- import { Observable } from 'rxjs';
97
12
  import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
98
- import { environment } from '../environments/environment';
99
-
100
- export interface Trip {
101
- id: string;
102
- title: string;
103
- departureDate: string;
104
- status: 'planned' | 'booked' | 'cancelled';
105
- }
106
-
107
- export interface TripDraft {
108
- title: string;
109
- departureDate: string;
110
- }
111
-
112
- interface ApiEnvelope<T> {
113
- data: T;
114
- }
115
-
116
13
  @Service()
117
14
  export class TripApi {
118
15
  private readonly http = injectRiviniaHttp();
119
- private readonly tripsUrl = joinUrl(environment.apiUrl, 'trips');
120
-
121
- list(): Observable<Trip[]> {
122
- return this.http.get<Trip[]>(this.tripsUrl, {
123
- params: { page: 1, search: 'coast' },
124
- unwrap: true,
125
- unwrapKey: 'data',
126
- });
127
- }
128
-
129
- get(id: string): Observable<Trip> {
130
- return this.http.get<Trip>(joinUrl(this.tripsUrl, id), {
131
- unwrap: true,
132
- unwrapKey: 'data',
133
- });
134
- }
135
-
136
- create(input: TripDraft): Observable<Trip> {
137
- return this.http.post<Trip>(this.tripsUrl, input, {
138
- unwrap: true,
139
- unwrapKey: 'data',
140
- });
141
- }
142
-
143
- replace(id: string, input: TripDraft): Observable<Trip> {
144
- return this.http.put<Trip>(joinUrl(this.tripsUrl, id), input, {
145
- unwrap: true,
146
- unwrapKey: 'data',
147
- });
148
- }
149
-
150
- updateStatus(id: string, status: Trip['status']): Observable<Trip> {
151
- return this.http.patch<Trip>(
152
- joinUrl(this.tripsUrl, id),
153
- { status },
154
- { unwrap: true, unwrapKey: 'data' },
155
- );
156
- }
157
-
158
- cancel(id: string, reason: string): Observable<void> {
159
- return this.http.delete<void>(joinUrl(this.tripsUrl, id), {
160
- body: { reason },
161
- unwrap: true,
162
- unwrapKey: 'data',
163
- });
164
- }
165
-
166
- downloadItinerary(id: string): Observable<Blob> {
167
- return this.http.getBlob(joinUrl(this.tripsUrl, `${id}/itinerary`));
168
- }
169
-
170
- getOperatorNotes(id: string): Observable<string> {
171
- return this.http.getText(joinUrl(this.tripsUrl, `${id}/operator-notes`));
172
- }
173
-
174
- listWithHeaders(): Observable<HttpResponse<ApiEnvelope<Trip[]>>> {
175
- return this.http.getResponse<ApiEnvelope<Trip[]>>(this.tripsUrl, {
176
- params: { page: 1 },
177
- });
178
- }
179
-
180
- createAndReadLocation(input: TripDraft): Observable<HttpResponse<ApiEnvelope<Trip>>> {
181
- return this.http.postResponse<ApiEnvelope<Trip>>(this.tripsUrl, input);
182
- }
16
+ private readonly url = joinUrl('/api', 'trips');
17
+ list() { return this.http.get<Trip[]>(this.url); }
183
18
  }
184
19
  ```
185
20
 
186
- The JSON endpoints in this example return an object with a `data` field; the first five
187
- methods opt in to extracting that field. `getBlob()` downloads a binary file,
188
- and `getText()` reads plain text; neither method unwraps or checks JSON
189
- envelopes. `getResponse()` and `postResponse()` return the complete
190
- `HttpResponse` so the caller can inspect the status and headers; their body
191
- stays raw and is not unwrapped.
21
+ ## Why @rivinia/http
192
22
 
193
- | `RiviniaHttp` method | Used above for |
194
- | -------------------- | ------------------------------------------------------------------ |
195
- | `get` | `list()` and `get(id)` |
196
- | `post` | `create(input)` |
197
- | `put` | `replace(id, input)` |
198
- | `patch` | `updateStatus(id, status)` |
199
- | `delete` | `cancel(id, reason)`; its optional request body is passed through. |
200
- | `getBlob` | `downloadItinerary(id)` |
201
- | `getText` | `getOperatorNotes(id)` |
202
- | `getResponse` | `listWithHeaders()` |
203
- | `postResponse` | `createAndReadLocation(input)` |
204
- | `getResource` | Reactive paginated list in `TripListComponent`. |
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.
205
26
 
206
- ```ts
207
- // Example use where the API returns pagination metadata in a header.
208
- this.api.listWithHeaders().subscribe((response) => {
209
- const total = Number(response.headers.get('X-Total-Count') ?? 0);
210
- console.log(response.status, total, response.body?.data);
211
- });
27
+ ## Install
212
28
 
213
- // A create endpoint may return 201 plus a Location header.
214
- this.api.createAndReadLocation(input).subscribe((response) => {
215
- const location = response.headers.get('Location');
216
- console.log(response.status, location, response.body?.data);
217
- });
218
- ```
219
-
220
- `Observable<T>` is the compile-time type of the result; it does not validate
221
- the server payload. `joinUrl` removes one boundary slash and passes absolute
222
- `http://` or `https://` endpoints through unchanged. Use `params` for query
223
- values: `null` and `undefined` entries are omitted, while strings, numbers,
224
- and booleans are stringified. The standard request options also pass
225
- `headers`, `HttpContext`, and `withCredentials` through to Angular's
226
- `HttpClient`. Use `HttpContext` with your own interceptors for app-specific
227
- controls such as opting out of auth.
228
-
229
- ## Reactive resources
230
-
231
- `getResource` uses Angular's `httpResource`. The endpoint can be a string, a
232
- signal, or a reactive function; `undefined` disables the request. URL,
233
- `params`, `headers`, and `context` can be reactive inputs and trigger a new
234
- request when their signal values change. Other options are read once when
235
- the resource is created.
236
-
237
- ```ts
238
- import { Component, signal } from '@angular/core';
239
- import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
240
- import { environment } from '../environments/environment';
241
-
242
- import type { Trip } from './trip.service';
243
-
244
- @Component({
245
- selector: 'app-trip-list',
246
- templateUrl: './trip-list.component.html',
247
- })
248
- export class TripListComponent {
249
- private readonly http = injectRiviniaHttp();
250
- readonly page = signal(1);
251
- readonly trips = this.http.getResource<Trip[]>(joinUrl(environment.apiUrl, 'trips'), {
252
- params: () => ({ page: this.page() }),
253
- unwrap: true,
254
- unwrapKey: 'data',
255
- });
256
-
257
- nextPage(): void {
258
- this.page.update((value) => value + 1);
259
- }
260
- }
261
- ```
262
-
263
- ```html
264
- <!-- src/app/trip-list.component.html -->
265
- @if (trips.isLoading()) {
266
- <p>Loading trips…</p>
267
- } @else if (trips.hasValue()) {
268
- <ul>
269
- @for (trip of trips.value(); track trip.id) {
270
- <li>{{ trip.title }} — {{ trip.departureDate }} ({{ trip.status }})</li>
271
- }
272
- </ul>
273
- } @else if (trips.error()) {
274
- <p role="alert">Could not load trips. Try again.</p>
275
- }
276
- <button type="button" (click)="nextPage()">Next page</button>
277
- ```
278
-
279
- In a real app, share the Trip model through a model module. `parse` is optional;
280
- when provided it runs after envelope validation and unwrapping, and can perform
281
- runtime validation (for example with Zod or Valibot). A parse error, HTTP
282
- error, or library envelope error is exposed through `trips.error()`.
283
- `defaultValue` supplies a value while idle/loading. `reportProgress` is
284
- forwarded to the resource request; for progress events on an `Observable`,
285
- use Angular `HttpClient` directly.
286
-
287
- ## Response envelopes and errors
288
-
289
- By default, all JSON response bodies are returned raw, even when they contain
290
- a field named `data`. To extract a payload, opt in on that call and provide a
291
- key:
292
-
293
- ```ts
294
- this.http.get<Trip[]>(url); // raw response body
295
- this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
296
- ```
297
-
298
- The type contract requires `unwrapKey` when `unwrap: true`, and rejects a key
299
- when unwrapping is off. Extraction is strict and single-level: the response
300
- must be a non-null, non-array object with that own property. A missing key,
301
- primitive/array/null body, or empty key throws `RiviniaHttpUnwrapError`;
302
- it does not silently return the original body. Present values such as `null`,
303
- `false`, `0`, and `''` are returned as-is. Nested data and sibling fields are
304
- not modified. `unwrapAs<T>` performs the same one-level extraction outside a
305
- request; its generic type is an unchecked cast, not runtime validation.
306
-
307
- An optional envelope policy checks backend-declared failures before
308
- unwrapping. The default keys are `isSuccess` and `error`; only an object whose
309
- `isSuccess` value is exactly `false` is considered failed:
310
-
311
- ```ts
312
- // Apply to JSON request paths made by this configured client.
313
- provideRiviniaHttp({ envelope: {} });
314
-
315
- // Or specify a per-request policy; it replaces the global policy.
316
- this.http.get<Trip[]>(url, {
317
- envelope: { successKey: 'ok', errorKey: 'message' },
318
- });
319
- ```
320
-
321
- When a failure is detected, `RiviniaHttpEnvelopeError` is sent through the
322
- Observable error channel or appears as the resource error. It carries the raw
323
- `body` and failure `code`. Set `throwOnFailure: false` for a passive policy.
324
- Policy validation runs before unwrapping, including on `getResponse` and
325
- `postResponse`. Those full-response methods return raw `HttpResponse<T>`
326
- (status, headers, and unwrapped body is not applied). `getBlob` and `getText`
327
- also return raw bodies and do not run envelope validation.
328
-
329
- The generic type does not validate the server response. For untrusted or
330
- loosely typed APIs, validate the extracted value with `getResource`'s `parse`
331
- hook, or validate in the Observable pipeline in app code. Do not log
332
- `RiviniaHttpUnwrapError.body` or `RiviniaHttpEnvelopeError.body` without
333
- considering that they may contain sensitive data.
334
-
335
- ## Public API
336
-
337
- | Export | Kind | Purpose |
338
- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
339
- | `provideRiviniaHttp(config?)` | provider | Sets `envelope` and optional `httpFeatures`; no global unwrap option. |
340
- | `RiviniaHttp` | class | `get`, `post`, `put`, `patch`, `delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, and `getResource`. |
341
- | `injectRiviniaHttp()` | function | Injection-context shorthand for `inject(RiviniaHttp)`. |
342
- | `joinUrl(base, endpoint)` | utility | Removes one boundary slash when joining; absolute HTTP(S) endpoints pass through. |
343
- | `unwrapAs(body, key)` | utility | Strict, single-level typed extraction. |
344
- | `extractKey(body, key)` | utility | Strict extraction without a generic cast. |
345
- | `RiviniaHttpUnwrapError` | class | Error for invalid or missing unwrap properties. |
346
- | `RiviniaHttpEnvelopeError` | class | Error for a failed configured response envelope. |
347
- | `RiviniaHttpConfig` | type | `envelope` and `httpFeatures` provider configuration. |
348
- | `RiviniaEnvelopePolicy` | type | Success key, error key, and failure policy settings. |
349
- | `RiviniaRequestOptions` | type | Options for requests that may unwrap. |
350
- | `RiviniaRawOptions` | type | Options for methods that never unwrap. |
351
- | `RiviniaDeleteOptions` | type | Delete options including an optional request body. |
352
- | `RiviniaDeleteRequestOptions` | type | Delete options including the unwrap contract. |
353
- | `RiviniaRequestBase` | type | Shared transport and envelope options. |
354
- | `RiviniaResourceOptions` | type | Reactive resource options and unwrap contract. |
355
- | `RiviniaResourceBase` | type | Shared resource-specific options. |
356
- | `ReactiveInput` | type | Static value, signal, or reactive function input. |
357
- | `QueryParamsInput` | type | `HttpParams` or a primitive-valued query object. |
358
- | `RiviniaUnwrapOn` | type | Required key form for `{ unwrap: true }`. |
359
- | `RiviniaUnwrapOff` | type | Form where an unwrap key is disallowed. |
360
-
361
- ## Testing
362
-
363
- Use Angular's HTTP testing providers. The testing provider follows
364
- `provideHttpClient()` so the test backend replaces the real backend:
365
-
366
- ```ts
367
- import { provideHttpClient } from '@angular/common/http';
368
- import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
369
- import { TestBed } from '@angular/core/testing';
370
- import { provideRiviniaHttp, RiviniaHttp } from '@rivinia/http';
371
-
372
- TestBed.configureTestingModule({
373
- providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
374
- });
375
-
376
- const api = TestBed.inject(RiviniaHttp);
377
- const requests = TestBed.inject(HttpTestingController);
378
- api
379
- .get<unknown[]>('https://api.test/trips', {
380
- unwrap: true,
381
- unwrapKey: 'data',
382
- })
383
- .subscribe();
384
- requests.expectOne('https://api.test/trips').flush({ data: [] });
385
- requests.verify();
29
+ ```bash
30
+ npm install @rivinia/http
31
+ ng add @rivinia/http
386
32
  ```
387
33
 
388
- ## Migration from 0.4.x
389
-
390
- Global `unwrap` and `unwrapKey` settings were removed in 0.5.0. Move the
391
- unwrap decision and key to each applicable request:
392
-
393
- ```ts
394
- // Before
395
- provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
396
- this.http.get<Trip[]>(url);
397
-
398
- // After
399
- provideRiviniaHttp();
400
- this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
401
- ```
34
+ ## Quick start
402
35
 
403
- The `ng add` schematic now emits bare providers and does not migrate request
404
- call sites. Without the per-request options, the response remains raw.
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.
405
39
 
406
- ## License
40
+ ## Compatibility
407
41
 
408
- MIT.
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` |
409
45
 
410
- ## Further reading
46
+ ## Go further
411
47
 
412
- [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.0/docs/decisions/D026-consumer-driven-http-0-3-0-reactive-function-endpoints.md).
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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/http",
3
- "version": "1.0.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",