@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.
- package/README.md +130 -42
- 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
|
-
-
|
|
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 {
|
|
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:
|
|
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
|
|
103
|
+
export interface TripDraft {
|
|
101
104
|
title: string;
|
|
105
|
+
departureDate: string;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
118
|
-
return this.http.
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
`
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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;
|
|
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
|