@rivinia/http 0.5.1 → 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 +130 -42
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ requests and reactive `httpResource` requests. It leaves URLs, backend
8
8
  contracts, and interceptors under app control. Response bodies pass through
9
9
  unchanged unless a request explicitly asks to unwrap one.
10
10
 
11
- - Prepared patch: **0.5.1** (latest published: **0.5.0**)
11
+ - Package version: **0.5.2**
12
12
  - Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
13
13
  as a dependency.
14
14
 
@@ -87,22 +87,30 @@ export const environment = {
87
87
 
88
88
  ```ts
89
89
  // src/app/trip.service.ts
90
- import { Injectable } from '@angular/core';
90
+ import { Service } from '@angular/core';
91
+ import type { HttpResponse } from '@angular/common/http';
91
92
  import { Observable } from 'rxjs';
92
93
  import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
93
94
  import { environment } from '../environments/environment';
94
95
 
95
- interface Trip {
96
- id: number;
96
+ export interface Trip {
97
+ id: string;
97
98
  title: string;
99
+ departureDate: string;
100
+ status: 'planned' | 'booked' | 'cancelled';
98
101
  }
99
102
 
100
- interface CreateTrip {
103
+ export interface TripDraft {
101
104
  title: string;
105
+ departureDate: string;
102
106
  }
103
107
 
104
- @Injectable({ providedIn: 'root' })
105
- export class TripService {
108
+ interface ApiEnvelope<T> {
109
+ data: T;
110
+ }
111
+
112
+ @Service()
113
+ export class TripApi {
106
114
  private readonly http = injectRiviniaHttp();
107
115
  private readonly tripsUrl = joinUrl(environment.apiUrl, 'trips');
108
116
 
@@ -114,33 +122,105 @@ export class TripService {
114
122
  });
115
123
  }
116
124
 
117
- create(input: CreateTrip): Observable<Trip> {
118
- return this.http.post<Trip>(this.tripsUrl, input, { unwrap: true, unwrapKey: 'data' });
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);
119
178
  }
120
179
  }
121
180
  ```
122
181
 
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.
130
-
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:
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`. |
134
201
 
135
202
  ```ts
136
- this.http.delete<void>(joinUrl(environment.apiUrl, 'trips/1'), {
137
- body: { reason: 'duplicate' },
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);
138
213
  });
139
214
  ```
140
215
 
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.
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.
144
224
 
145
225
  ## Reactive resources
146
226
 
@@ -155,24 +235,12 @@ import { Component, signal } from '@angular/core';
155
235
  import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
156
236
  import { environment } from '../environments/environment';
157
237
 
158
- interface Trip {
159
- id: number;
160
- title: string;
161
- }
238
+ import type { Trip } from './trip.service';
162
239
 
163
240
  @Component({
164
241
  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
- `,
242
+ standalone: true,
243
+ templateUrl: './trip-list.component.html',
176
244
  })
177
245
  export class TripListComponent {
178
246
  private readonly http = injectRiviniaHttp();
@@ -182,11 +250,31 @@ export class TripListComponent {
182
250
  unwrap: true,
183
251
  unwrapKey: 'data',
184
252
  });
253
+
254
+ nextPage(): void {
255
+ this.page.update((value) => value + 1);
256
+ }
257
+ }
258
+ ```
259
+
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>
185
272
  }
273
+ <button type="button" (click)="nextPage()">Next page</button>
186
274
  ```
187
275
 
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
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
190
278
  runtime validation (for example with Zod or Valibot). A parse error, HTTP
191
279
  error, or library envelope error is exposed through `trips.error()`.
192
280
  `defaultValue` supplies a value while idle/loading. `reportProgress` is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/http",
3
- "version": "0.5.1",
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",