@rivinia/http 0.4.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 +255 -96
- package/fesm2022/rivinia-http.mjs +31 -26
- package/fesm2022/rivinia-http.mjs.map +1 -1
- package/package.json +1 -1
- package/schematics/ng-add/index.cjs +7 -22
- package/schematics/ng-add/schema.json +2 -18
- package/types/rivinia-http.d.ts +124 -44
package/README.md
CHANGED
|
@@ -3,156 +3,315 @@
|
|
|
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
|
-
|
|
51
|
-
|
|
75
|
+
## URLs and typed service calls
|
|
76
|
+
|
|
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
|
+
```
|
|
52
87
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.api.get<Raw>(url); // raw body (default)
|
|
136
|
+
this.http.delete<void>(joinUrl(environment.apiUrl, 'trips/1'), {
|
|
137
|
+
body: { reason: 'duplicate' },
|
|
138
|
+
});
|
|
83
139
|
```
|
|
84
140
|
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
## Reactive resources
|
|
146
|
+
|
|
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.
|
|
91
152
|
|
|
92
|
-
|
|
93
|
-
|
|
153
|
+
```ts
|
|
154
|
+
import { Component, signal } from '@angular/core';
|
|
155
|
+
import { injectRiviniaHttp, joinUrl } from '@rivinia/http';
|
|
156
|
+
import { environment } from '../environments/environment';
|
|
94
157
|
|
|
95
|
-
|
|
96
|
-
|
|
158
|
+
interface Trip {
|
|
159
|
+
id: number;
|
|
160
|
+
title: string;
|
|
161
|
+
}
|
|
97
162
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
});
|
|
100
185
|
}
|
|
101
186
|
```
|
|
102
187
|
|
|
103
|
-
|
|
104
|
-
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.
|
|
105
195
|
|
|
106
|
-
##
|
|
196
|
+
## Response envelopes and errors
|
|
107
197
|
|
|
108
|
-
|
|
109
|
-
|
|
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:
|
|
110
201
|
|
|
111
202
|
```ts
|
|
112
|
-
|
|
113
|
-
this.
|
|
203
|
+
this.http.get<Trip[]>(url); // raw response body
|
|
204
|
+
this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
114
205
|
```
|
|
115
206
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|---|---|---|
|
|
125
|
-
| `provideRiviniaHttp(config?)` | provider | `{ unwrap?=false, unwrapKey?='data', envelope?, httpFeatures? }` |
|
|
126
|
-
| `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, `getResource` |
|
|
127
|
-
| `injectRiviniaHttp()` | function | shorthand for `inject(RiviniaHttp)` |
|
|
128
|
-
| `joinUrl(base, endpoint)` | util | slash-safe join; absolute URLs pass through |
|
|
129
|
-
| `unwrapAs(body, key)` | util | single-level envelope extraction (nested keys preserved) |
|
|
130
|
-
| types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `RiviniaEnvelopePolicy`, `ReactiveInput`, `QueryParamsInput` |
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
failure rule holds on every JSON path, never just the unwrapped ones. `getText`
|
|
136
|
-
returns raw text; binary/text bodies cannot carry envelopes, so the policy does
|
|
137
|
-
not apply there. In `getResource`, the URL, `params`, `headers` and `context`
|
|
138
|
-
accept values, signals or reactive functions (everything else is static).
|
|
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:
|
|
139
219
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
220
|
+
```ts
|
|
221
|
+
// Apply to JSON request paths made by this configured client.
|
|
222
|
+
provideRiviniaHttp({ envelope: {} });
|
|
143
223
|
|
|
144
|
-
|
|
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
|
+
```
|
|
145
229
|
|
|
146
|
-
|
|
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:
|
|
147
274
|
|
|
148
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';
|
|
280
|
+
|
|
149
281
|
TestBed.configureTestingModule({
|
|
150
282
|
providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
|
|
151
283
|
});
|
|
152
|
-
|
|
153
|
-
TestBed.inject(
|
|
284
|
+
|
|
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
|
+
```
|
|
296
|
+
|
|
297
|
+
## Migration from 0.4.x
|
|
298
|
+
|
|
299
|
+
Global `unwrap` and `unwrapKey` settings were removed in 0.5.0. Move the
|
|
300
|
+
unwrap decision and key to each applicable request:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// Before
|
|
304
|
+
provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
|
|
305
|
+
this.http.get<Trip[]>(url);
|
|
306
|
+
|
|
307
|
+
// After
|
|
308
|
+
provideRiviniaHttp();
|
|
309
|
+
this.http.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
154
310
|
```
|
|
155
311
|
|
|
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.
|
|
314
|
+
|
|
156
315
|
## License
|
|
157
316
|
|
|
158
317
|
MIT.
|
|
@@ -8,15 +8,27 @@ function joinUrl(baseUrl, endpoint) {
|
|
|
8
8
|
const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
|
|
9
9
|
return path ? `${base}/${path}` : base;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
var RiviniaHttpUnwrapError = class extends Error {
|
|
12
|
+
key;
|
|
13
|
+
endpoint;
|
|
14
|
+
body;
|
|
15
|
+
constructor(key, body, endpoint, reason) {
|
|
16
|
+
super(`[rivinia/http] Cannot unwrap key "${key}"` + (endpoint ? ` from ${endpoint}` : "") + (reason ? `: ${reason}` : ": property missing"));
|
|
17
|
+
this.name = "RiviniaHttpUnwrapError";
|
|
18
|
+
this.key = key;
|
|
19
|
+
this.endpoint = endpoint;
|
|
20
|
+
this.body = body;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function extractKey(body, key, endpoint) {
|
|
24
|
+
if (typeof key !== "string" || key.length === 0) throw new RiviniaHttpUnwrapError(key, body, endpoint, "invalid key");
|
|
25
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new RiviniaHttpUnwrapError(key, body, endpoint, "response is not an object");
|
|
26
|
+
const record = body;
|
|
27
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) throw new RiviniaHttpUnwrapError(key, body, endpoint);
|
|
28
|
+
return record[key];
|
|
17
29
|
}
|
|
18
|
-
function
|
|
19
|
-
return
|
|
30
|
+
function unwrapAs(body, key) {
|
|
31
|
+
return extractKey(body, key);
|
|
20
32
|
}
|
|
21
33
|
var RiviniaHttpEnvelopeError = class extends Error {
|
|
22
34
|
body;
|
|
@@ -43,22 +55,22 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
43
55
|
http = inject(HttpClient);
|
|
44
56
|
config = inject(RIVINIA_HTTP_CONFIG, { optional: true });
|
|
45
57
|
get(endpoint, options) {
|
|
46
|
-
return this.http.get(this.url(endpoint), this.httpOptions(options)).pipe(map((body) => this.applyUnwrap(body, options)));
|
|
58
|
+
return this.http.get(this.url(endpoint), this.httpOptions(options)).pipe(map((body) => this.applyUnwrap(endpoint, body, options)));
|
|
47
59
|
}
|
|
48
60
|
post(endpoint, body, options) {
|
|
49
|
-
return this.http.post(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
61
|
+
return this.http.post(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(endpoint, res, options)));
|
|
50
62
|
}
|
|
51
63
|
put(endpoint, body, options) {
|
|
52
|
-
return this.http.put(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
64
|
+
return this.http.put(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(endpoint, res, options)));
|
|
53
65
|
}
|
|
54
66
|
patch(endpoint, body, options) {
|
|
55
|
-
return this.http.patch(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
67
|
+
return this.http.patch(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(endpoint, res, options)));
|
|
56
68
|
}
|
|
57
69
|
delete(endpoint, options) {
|
|
58
70
|
return this.http.delete(this.url(endpoint), {
|
|
59
71
|
...this.httpOptions(options),
|
|
60
72
|
body: options?.body
|
|
61
|
-
}).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
73
|
+
}).pipe(map((res) => this.applyUnwrap(endpoint, res, options)));
|
|
62
74
|
}
|
|
63
75
|
getBlob(endpoint, options) {
|
|
64
76
|
return this.http.get(this.url(endpoint), {
|
|
@@ -107,13 +119,12 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
107
119
|
...reportProgress !== void 0 ? { reportProgress } : {}
|
|
108
120
|
};
|
|
109
121
|
};
|
|
110
|
-
const unwrapKey = this.resolveKey(requestOptions);
|
|
111
|
-
const unwrapEnabled = this.resolveUnwrap(requestOptions);
|
|
112
122
|
const envelope = this.resolveEnvelope(requestOptions);
|
|
113
123
|
const base = { defaultValue };
|
|
114
124
|
const applyParse = (raw) => {
|
|
115
125
|
if (envelope) assertEnvelope(raw, envelope);
|
|
116
|
-
|
|
126
|
+
if (requestOptions.unwrap !== true) return parse ? parse(raw) : raw;
|
|
127
|
+
const unwrapped = extractKey(raw, requestOptions.unwrapKey);
|
|
117
128
|
return parse ? parse(unwrapped) : unwrapped;
|
|
118
129
|
};
|
|
119
130
|
return defaultValue === void 0 ? httpResource(request, {
|
|
@@ -128,20 +139,14 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
128
139
|
url(endpoint) {
|
|
129
140
|
return endpoint;
|
|
130
141
|
}
|
|
131
|
-
resolveUnwrap(options) {
|
|
132
|
-
return options?.unwrap ?? this.config?.unwrap ?? false;
|
|
133
|
-
}
|
|
134
|
-
resolveKey(options) {
|
|
135
|
-
return options?.unwrapKey ?? this.config?.unwrapKey ?? "data";
|
|
136
|
-
}
|
|
137
142
|
resolveEnvelope(options) {
|
|
138
143
|
return options?.envelope ?? this.config?.envelope;
|
|
139
144
|
}
|
|
140
|
-
applyUnwrap(body, options) {
|
|
145
|
+
applyUnwrap(endpoint, body, options) {
|
|
141
146
|
const envelope = this.resolveEnvelope(options);
|
|
142
147
|
if (envelope) assertEnvelope(body, envelope);
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
148
|
+
if (options?.unwrap !== true) return body;
|
|
149
|
+
return extractKey(body, options.unwrapKey, endpoint);
|
|
145
150
|
}
|
|
146
151
|
httpOptions(options) {
|
|
147
152
|
return {
|
|
@@ -196,6 +201,6 @@ function provideRiviniaHttp(config = {}) {
|
|
|
196
201
|
function injectRiviniaHttp() {
|
|
197
202
|
return inject(RiviniaHttp);
|
|
198
203
|
}
|
|
199
|
-
export { RiviniaHttp, RiviniaHttpEnvelopeError, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
204
|
+
export { RiviniaHttp, RiviniaHttpEnvelopeError, RiviniaHttpUnwrapError, extractKey, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
200
205
|
|
|
201
206
|
//# sourceMappingURL=rivinia-http.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/http-utils.ts","../../../projects/http/src/lib/http-envelope.ts","../../../projects/http/src/lib/http-config.ts","../../../projects/http/src/lib/rivinia-http.ts","../../../projects/http/src/lib/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`); passes anything else through\n * unchanged. Unwraps EXACTLY ONE level: a payload that itself contains the\n * key (`{ data: { data: […], page: 1 } }`) is returned whole — recursive\n * unwrapping silently destroyed sibling fields (data-loss bug, fixed).\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n if (isObjectRecord(body) && hasOwn(body, key)) {\n return body[key] as T;\n }\n return body as T;\n}\n\nfunction isObjectRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction hasOwn(obj: Record<string, unknown>, key: string): boolean {\n return Object.prototype.hasOwnProperty.call(obj, key);\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Every field is optional and can be overridden per request.\n * URLs are ALWAYS used as given — the library never joins, prefixes, or\n * rewrites them. Build them at the call site from wherever you want\n * (`environment.apiUrl`, a constant, a signal …) with the exported `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Unwrap `{ [unwrapKey]: payload }` envelopes by default.\n * @default false — the library assumes nothing about your backend;\n * opt in globally here or per call with `{ unwrap: true }`.\n */\n unwrap?: boolean;\n /**\n * Envelope key unwrapped when `unwrap` is enabled.\n * @default 'data'\n */\n unwrapKey?: string;\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>(\n 'RIVINIA_HTTP_CONFIG',\n);\n\nexport const DEFAULT_UNWRAP = false;\nexport const DEFAULT_UNWRAP_KEY = 'data';\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport {\n DEFAULT_UNWRAP,\n DEFAULT_UNWRAP_KEY,\n RIVINIA_HTTP_CONFIG,\n type RiviniaHttpConfig,\n} from './http-config';\nimport {\n type QueryParamsInput,\n type ReactiveInput,\n type RiviniaDeleteOptions,\n type RiviniaRequestOptions,\n} from './http-options';\nimport { unwrapAs } from './http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from './http-envelope';\n\n/** Options for `getResource` in addition to per-request options. */\nexport interface RiviniaResourceOptions<T>\n extends Omit<RiviniaRequestOptions, 'params' | 'headers' | 'context'> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n}\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * unwrapping (`unwrap`/`unwrapKey`) and failure validation (`envelope`).\n *\n * Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? false`,\n * key: `options.unwrapKey ?? config.unwrapKey ?? 'data'`.\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRequestOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRequestOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRequestOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const unwrapKey = this.resolveKey(requestOptions);\n const unwrapEnabled = this.resolveUnwrap(requestOptions);\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope surfaces as resource.error() automatically.\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n const unwrapped = unwrapEnabled ? unwrapAs<T>(raw, unwrapKey) : (raw as T);\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveUnwrap(options?: { unwrap?: boolean }): boolean {\n return options?.unwrap ?? this.config?.unwrap ?? DEFAULT_UNWRAP;\n }\n\n private resolveKey(options?: { unwrapKey?: string }): string {\n return options?.unwrapKey ?? this.config?.unwrapKey ?? DEFAULT_UNWRAP_KEY;\n }\n\n private resolveEnvelope(options?: { envelope?: RiviniaEnvelopePolicy }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (!this.resolveUnwrap(options)) return body as T;\n return unwrapAs<T>(body, this.resolveKey(options));\n }\n\n private httpOptions(options?: RiviniaRequestOptions) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined { if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport { RiviniaHttp } from './rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp({ unwrapKey: 'data' })\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":";;;;AAIA,SAAgB,QAAQ,SAAiB,UAAgB;CACvD,IAAI,gBAAgB,KAAK,QAAQ,GAC/B,OAAO;CAET,MAAM,OAAO,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI;CAC5D,MAAM,OAAO,SAAS,WAAW,GAAG,IAAI,SAAS,MAAM,CAAC,IAAI;CAC5D,OAAO,OAAO,GAAG,KAAI,GAAI,SAAS;AACpC;AASA,SAAgB,SAAY,MAAe,KAAW;CACpD,IAAI,eAAe,IAAI,KAAK,OAAO,MAAM,GAAG,GAC1C,OAAO,KAAK;CAEd,OAAO;AACT;AAEA,SAAS,eAAe,OAAc;CACpC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,OAAO,KAA8B,KAAW;CACvD,OAAO,OAAO,UAAU,eAAe,KAAK,KAAK,GAAG;AACtD;AC3BA,IAAa,2BAAb,cAA8C,MAAK;CAExC;CAEA;CAET,YAAY,MAAe,MAAa;EACtC,MACE,OAAO,SAAS,YAAY,KAAK,SAAS,IACtC,kCAAkC,SAClC,kDAAkD;EAExD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACD;AAiBD,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAG1B,SAAgB,eAAe,MAAe,QAA6B;CACzE,IAAI,OAAO,mBAAmB,OAAO;CACrC,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG;CACtE,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,SAAS;CACf,IAAI,EAAE,cAAc,WAAW,OAAO,gBAAgB,OAAO;CAE7D,MAAM,IAAI,yBAAyB,MAAM,OADxB,OAAO,YAAY,kBACqB;AAC3D;ACJA,MAAa,sBAAsB,IAAI,eACrC,qBAAqB;ACgBvB,IAAa,cAAb,MAAa,YAAW;CACL,OAAO,OAAO,UAAU;CACxB,SAAS,OAA0B,qBAAqB,EACvE,UAAU,KACX,CAAA;CAED,IAAO,UAAkB,SAA+B;EACtD,OAAO,KAAK,KACT,IAAa,KAAK,IAAI,QAAQ,GAAG,KAAK,YAAY,OAAO,CAAC,CAAC,CAC3D,KAAK,KAAK,SAAS,KAAK,YAAe,MAAM,OAAO,CAAC,CAAC;CAC3D;CAEA,KAAQ,UAAkB,MAAgB,SAA+B;EACvE,OAAO,KAAK,KACT,KAAc,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CAClE,KAAK,KAAK,QAAQ,KAAK,YAAe,KAAK,OAAO,CAAC,CAAC;CACzD;CAEA,IAAO,UAAkB,MAAgB,SAA+B;EACtE,OAAO,KAAK,KACT,IAAa,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CACjE,KAAK,KAAK,QAAQ,KAAK,YAAe,KAAK,OAAO,CAAC,CAAC;CACzD;CAEA,MAAS,UAAkB,MAAgB,SAA+B;EACxE,OAAO,KAAK,KACT,MAAe,KAAK,IAAI,QAAQ,GAAG,MAAM,KAAK,YAAY,OAAO,CAAC,CAAC,CACnE,KAAK,KAAK,QAAQ,KAAK,YAAe,KAAK,OAAO,CAAC,CAAC;CACzD;CAEA,OAAU,UAAkB,SAA8B;EACxD,OAAO,KAAK,KACT,OAAgB,KAAK,IAAI,QAAQ,GAAG;GACnC,GAAG,KAAK,YAAY,OAAO;GAC3B,MAAM,SAAS;EAChB,CAAA,CAAC,CACD,KAAK,KAAK,QAAQ,KAAK,YAAe,KAAK,OAAO,CAAC,CAAC;CACzD;CAGA,QAAQ,UAAkB,SAA+B;EACvD,OAAO,KAAK,KAAK,IAAI,KAAK,IAAI,QAAQ,GAAG;GACvC,GAAG,KAAK,YAAY,OAAO;GAC3B,cAAc;EACf,CAAA;CACH;CAGA,QAAQ,UAAkB,SAA+B;EACvD,OAAO,KAAK,KAAK,IAAI,KAAK,IAAI,QAAQ,GAAG;GACvC,GAAG,KAAK,YAAY,OAAO;GAC3B,cAAc;EACf,CAAA;CACH;CAQA,YAAe,UAAkB,SAA+B;EAC9D,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,OAAO,KAAK,KACT,IAAO,KAAK,IAAI,QAAQ,GAAG;GAC1B,GAAG,KAAK,YAAY,OAAO;GAC3B,SAAS;EACV,CAAA,CAAC,CACD,KACC,KAAK,QAAO;GACV,IAAI,UAAU,eAAe,IAAI,MAAM,QAAQ;GAC/C,OAAO;EACT,CAAC,CAAC;CAER;CAMA,aACE,UACA,MACA,SAA+B;EAE/B,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,OAAO,KAAK,KACT,KAAQ,KAAK,IAAI,QAAQ,GAAG,MAAM;GACjC,GAAG,KAAK,YAAY,OAAO;GAC3B,SAAS;EACV,CAAA,CAAC,CACD,KACC,KAAK,QAAO;GACV,IAAI,UAAU,eAAe,IAAI,MAAM,QAAQ;GAC/C,OAAO;EACT,CAAC,CAAC;CAER;CASA,YACE,UACA,SAAmC;EAEnC,MAAM,EAAE,cAAc,OAAO,gBAAgB,GAAG,mBAAmB,WAAW,CAAA;EAC9E,MAAM,SACJ,OAAO,aAAa,cAAc,CAAC,SAAS,QAAQ,IAChD,SAAS,QAAQ,IAChB;EACP,MAAM,gBAAe;GACnB,MAAM,KAAK,OAAO,WAAW,WAAW,SAAS,OAAM;GACvD,IAAI,OAAO,QAAW,OAAO;GAC7B,OAAO;IACL,KAAK,KAAK,IAAI,EAAE;IAChB,QAAQ,KAAK,SAAS,aAAa,eAAe,MAAM,CAAC;IACzD,SAAS,aAAa,eAAe,OAAO;IAC5C,SAAS,aAAa,eAAe,OAAO;IAC5C,iBAAiB,eAAe;IAChC,GAAI,mBAAmB,SAAY,EAAE,eAAc,IAAK,CAAA;GACzD;EACH;EACA,MAAM,YAAY,KAAK,WAAW,cAAc;EAChD,MAAM,gBAAgB,KAAK,cAAc,cAAc;EACvD,MAAM,WAAW,KAAK,gBAAgB,cAAc;EACpD,MAAM,OAAO,EAAgB,aAA6B;EAG1D,MAAM,cAAc,QAAmB;GACrC,IAAI,UAAU,eAAe,KAAK,QAAQ;GAC1C,MAAM,YAAY,gBAAgB,SAAY,KAAK,SAAS,IAAK;GACjE,OAAO,QAAQ,MAAM,SAAS,IAAI;EACpC;EACA,OAAO,iBAAiB,SACpB,aAAgB,SAAS;GAAE,GAAG;GAAM,OAAO;EAAU,CAAE,IACvD,aAAgB,SAAS;GACvB,GAAG;GACW;GACd,OAAO;EACR,CAAA;CACP;CAEQ,IAAI,UAAgB;EAG1B,OAAO;CACT;CAEQ,cAAc,SAA8B;EAClD,OAAO,SAAS,UAAU,KAAK,QAAQ,UAAA;CACzC;CAEQ,WAAW,SAAgC;EACjD,OAAO,SAAS,aAAa,KAAK,QAAQ,aAAA;CAC5C;CAEQ,gBAAgB,SAA8C;EACpE,OAAO,SAAS,YAAY,KAAK,QAAQ;CAC3C;CAEQ,YAAe,MAAe,SAA+B;EACnE,MAAM,WAAW,KAAK,gBAAgB,OAAO;EAC7C,IAAI,UAAU,eAAe,MAAM,QAAQ;EAC3C,IAAI,CAAC,KAAK,cAAc,OAAO,GAAG,OAAO;EACzC,OAAO,SAAY,MAAM,KAAK,WAAW,OAAO,CAAC;CACnD;CAEQ,YAAY,SAA+B;EACjD,OAAO;GACL,QAAQ,KAAK,SAAS,SAAS,MAAM;GACrC,SAAS,SAAS;GAClB,SAAS,SAAS,WAAW,IAAI,YAAW;GAC5C,iBAAiB,SAAS;EAC3B;CACH;CAEQ,SAAS,OAAwB;EAA+B,IAAI,CAAC,OAAO,OAAO;EACzF,IAAI,iBAAiB,YAAY,OAAO;EACxC,IAAI,SAAS,IAAI,WAAU;EAC3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAC7C,IAAI,UAAU,UAAa,UAAU,MACnC,SAAS,OAAO,IAAI,KAAK,OAAO,KAAK,CAAC;EAG1C,OAAO;CACT;CA9LW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAW,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAX,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAW,CAAA;;AAAX,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAW,YAAA,CAAA,EADvB,MAAA,QAAA,CAAA;;AAsMD,SAAS,aAAgB,OAAmC;CAC1D,IAAI,UAAU,QAAW,OAAO;CAChC,IAAI,SAAS,KAAK,GAAG,OAAQ,MAAmB;CAChD,IAAI,OAAO,UAAU,YAAY,OAAQ,MAAiB;CAC1D,OAAO;AACT;AClPA,SAAgB,mBAAmB,SAA4B,CAAA,GAAE;CAC/D,MAAM,EAAE,cAAc,GAAG,SAAS;CAClC,OAAO,yBAAyB,CAC9B,GAAI,eAAe,CAAC,kBAAkB,GAAG,YAAY,CAAC,IAAI,CAAA,GAC1D;EAAE,SAAS;EAAqB,UAAU;CAAI,CAC/C,CAAA;AACH;AAGA,SAAgB,oBAAiB;CAC/B,OAAO,OAAO,WAAW;AAC3B"}
|
|
1
|
+
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/http-utils.ts","../../../projects/http/src/lib/http-envelope.ts","../../../projects/http/src/lib/http-config.ts","../../../projects/http/src/lib/rivinia-http.ts","../../../projects/http/src/lib/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from './http-options';\nimport { extractKey } from './http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport { RiviniaHttp } from './rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -10,7 +10,9 @@ const PROVIDE_HTTP_IMPORT = `import { provideRiviniaHttp } from '@rivinia/http';
|
|
|
10
10
|
* - Never overwrites user files; only inserts two imports + two providers.
|
|
11
11
|
* - Idempotent: safe to run twice (existing entries are detected and skipped).
|
|
12
12
|
* - Never provides the backend beyond `provideHttpClient()`; interceptors,
|
|
13
|
-
* URLs and unwrap options stay app-owned
|
|
13
|
+
* URLs and per-request unwrap options stay app-owned. Unwrapping is
|
|
14
|
+
* strictly per call (`{ unwrap: true, unwrapKey }`) — the schematic emits
|
|
15
|
+
* no unwrap configuration.
|
|
14
16
|
*/
|
|
15
17
|
function ngAdd(options) {
|
|
16
18
|
return (tree, context) => {
|
|
@@ -32,26 +34,9 @@ function ngAdd(options) {
|
|
|
32
34
|
if (!content) {
|
|
33
35
|
throw new schematics_1.SchematicsException(`[rivinia/http] ${configPath} is empty.`);
|
|
34
36
|
}
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// - unwrap === true → emit `unwrap: true`, plus the key only when custom
|
|
39
|
-
// (anything but ''/'data', which both mean the library default key).
|
|
40
|
-
// URLs are never touched: the caller owns them (environment, joinUrl, …).
|
|
41
|
-
const parts = [];
|
|
42
|
-
const unwrapEnabled = options.unwrap === true;
|
|
43
|
-
if (!unwrapEnabled) {
|
|
44
|
-
if (options.unwrapKey && options.unwrapKey !== 'data') {
|
|
45
|
-
context.logger.info(`[rivinia/http] unwrapping not enabled — custom unwrapKey '${options.unwrapKey}' ignored.`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
parts.push('unwrap: true');
|
|
50
|
-
if (options.unwrapKey && options.unwrapKey.length > 0 && options.unwrapKey !== 'data') {
|
|
51
|
-
parts.push(`unwrapKey: '${options.unwrapKey}'`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const provideArg = parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
|
|
37
|
+
// Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }`) —
|
|
38
|
+
// the schematic emits no unwrap configuration. URLs are never touched:
|
|
39
|
+
// the caller owns them (environment, joinUrl, …).
|
|
55
40
|
let changed = false;
|
|
56
41
|
if (!content.includes(HTTP_CLIENT_IMPORT)) {
|
|
57
42
|
content = insertAfterLastImport(content, HTTP_CLIENT_IMPORT);
|
|
@@ -68,7 +53,7 @@ function ngAdd(options) {
|
|
|
68
53
|
else {
|
|
69
54
|
context.logger.info('[rivinia/http] provideHttpClient() already present — skipped.');
|
|
70
55
|
}
|
|
71
|
-
const provideCall = `provideRiviniaHttp(
|
|
56
|
+
const provideCall = `provideRiviniaHttp()`;
|
|
72
57
|
if (!content.includes('provideRiviniaHttp(')) {
|
|
73
58
|
content = insertProvider(content, provideCall);
|
|
74
59
|
changed = true;
|
|
@@ -6,25 +6,9 @@
|
|
|
6
6
|
"properties": {
|
|
7
7
|
"project": {
|
|
8
8
|
"type": "string",
|
|
9
|
-
"description": "Angular CLI project name — only for multi-app workspaces. Defaults to the workspace default project.
|
|
9
|
+
"description": "Angular CLI project name — only for multi-app workspaces. Defaults to the workspace default project.",
|
|
10
10
|
"default": ""
|
|
11
|
-
},
|
|
12
|
-
"unwrapKey": {
|
|
13
|
-
"type": "string",
|
|
14
|
-
"description": "Envelope key passed as provideRiviniaHttp({ unwrapKey }). Asked only in effect when unwrapping is enabled; omit to keep the 'data' default.",
|
|
15
|
-
"x-prompt": {
|
|
16
|
-
"message": "Backend envelope key (Enter for the 'data' default)?",
|
|
17
|
-
"type": "input"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"unwrap": {
|
|
21
|
-
"type": "boolean",
|
|
22
|
-
"description": "Enable envelope unwrapping project-wide. When false, provideRiviniaHttp({ unwrap: false }) is emitted and unwrapKey is ignored.",
|
|
23
|
-
"x-prompt": {
|
|
24
|
-
"message": "Enable backend envelope unwrapping for this project?",
|
|
25
|
-
"type": "confirmation"
|
|
26
|
-
}
|
|
27
11
|
}
|
|
28
12
|
},
|
|
29
13
|
"required": []
|
|
30
|
-
}
|
|
14
|
+
}
|
package/types/rivinia-http.d.ts
CHANGED
|
@@ -34,23 +34,15 @@ declare function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): v
|
|
|
34
34
|
/**
|
|
35
35
|
* Global configuration for `@rivinia/http`.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the
|
|
38
|
+
* call) — there is intentionally no global unwrap configuration. A global
|
|
39
|
+
* key would silently change what every call returns; an explicit key keeps
|
|
40
|
+
* each call honest. URLs are ALWAYS used as given — the library never joins,
|
|
41
|
+
* prefixes, or rewrites them. Build them at the call site from wherever you
|
|
42
|
+
* want (`environment.apiUrl`, a constant, a signal …) with the exported
|
|
43
|
+
* `joinUrl`.
|
|
41
44
|
*/
|
|
42
45
|
interface RiviniaHttpConfig {
|
|
43
|
-
/**
|
|
44
|
-
* Unwrap `{ [unwrapKey]: payload }` envelopes by default.
|
|
45
|
-
* @default false — the library assumes nothing about your backend;
|
|
46
|
-
* opt in globally here or per call with `{ unwrap: true }`.
|
|
47
|
-
*/
|
|
48
|
-
unwrap?: boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Envelope key unwrapped when `unwrap` is enabled.
|
|
51
|
-
* @default 'data'
|
|
52
|
-
*/
|
|
53
|
-
unwrapKey?: string;
|
|
54
46
|
/**
|
|
55
47
|
* Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),
|
|
56
48
|
* applied via a single internal `provideHttpClient(...httpFeatures)` call.
|
|
@@ -74,8 +66,6 @@ interface RiviniaHttpConfig {
|
|
|
74
66
|
* @internal
|
|
75
67
|
*/
|
|
76
68
|
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
77
|
-
declare const DEFAULT_UNWRAP = false;
|
|
78
|
-
declare const DEFAULT_UNWRAP_KEY = "data";
|
|
79
69
|
/**
|
|
80
70
|
* A value, a signal of it, or a reactive function returning it — evaluated
|
|
81
71
|
* inside the `httpResource` request function, so signal reads stay reactive
|
|
@@ -85,30 +75,75 @@ type ReactiveInput<T> = T | Signal<T> | (() => T);
|
|
|
85
75
|
/** Shorthand for building query strings; `null`/`undefined` values are dropped. */
|
|
86
76
|
type QueryParamsInput = HttpParams | Record<string, string | number | boolean | undefined | null>;
|
|
87
77
|
/**
|
|
88
|
-
*
|
|
89
|
-
* `
|
|
78
|
+
* Unwrap contract for a single request — a discriminated union on the literal
|
|
79
|
+
* `unwrap` flag (no overloads needed: both modes return the same types).
|
|
80
|
+
*
|
|
81
|
+
* - omitted/disabled: `unwrapKey` is forbidden (`never`).
|
|
82
|
+
* - `unwrap: true`: `unwrapKey` is required. There is no default key.
|
|
83
|
+
* - a key alone, or `{ unwrap: false, unwrapKey }`, fails compilation.
|
|
84
|
+
*
|
|
85
|
+
* NOTE: excess-property checks apply to fresh literals only. A pre-built
|
|
86
|
+
* object widens `unwrap` to `boolean` — inline the options, annotate them,
|
|
87
|
+
* or use `as const`.
|
|
90
88
|
*/
|
|
91
|
-
interface
|
|
89
|
+
interface RiviniaUnwrapOn {
|
|
90
|
+
/** Extract `body[unwrapKey]`; a missing key throws `RiviniaHttpUnwrapError`. */
|
|
91
|
+
unwrap: true;
|
|
92
|
+
/** Envelope key to extract. Required, non-empty. No default. */
|
|
93
|
+
unwrapKey: string;
|
|
94
|
+
}
|
|
95
|
+
interface RiviniaUnwrapOff {
|
|
96
|
+
unwrap?: false;
|
|
97
|
+
/** Forbidden while unwrapping is off — a key alone never enables it. */
|
|
98
|
+
unwrapKey?: never;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Shared per-request fields (transport + envelope validation). Unwrap
|
|
102
|
+
* behavior is added by the contract types below — never by a global.
|
|
103
|
+
*/
|
|
104
|
+
interface RiviniaRequestBase {
|
|
92
105
|
params?: QueryParamsInput;
|
|
93
106
|
headers?: HttpHeaders | Record<string, string | string[]>;
|
|
94
107
|
/** Pass-through for app-owned interceptors (auth opt-out, error policy, ...). */
|
|
95
108
|
context?: HttpContext;
|
|
96
109
|
withCredentials?: boolean;
|
|
97
|
-
/** Enable envelope unwrapping for this call (overrides global `unwrap`). */
|
|
98
|
-
unwrap?: boolean;
|
|
99
|
-
/** Envelope key for this call (overrides global `unwrapKey`). */
|
|
100
|
-
unwrapKey?: string;
|
|
101
110
|
/**
|
|
102
111
|
* Opt-in backend-envelope validation for this call (replaces any global
|
|
103
112
|
* `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.
|
|
104
113
|
*/
|
|
105
114
|
envelope?: RiviniaEnvelopePolicy;
|
|
106
115
|
}
|
|
107
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Per-request options for methods that can unwrap (`get`/`post`/`put`/
|
|
118
|
+
* `patch`/`delete`, `getResource`). A discriminated union on the literal
|
|
119
|
+
* `unwrap` flag (no overloads needed: both modes return the same types):
|
|
120
|
+
*
|
|
121
|
+
* - omitted/disabled: `unwrapKey` is forbidden (`never`).
|
|
122
|
+
* - `unwrap: true`: `unwrapKey` is required. There is no default key.
|
|
123
|
+
* - a key alone, or `{ unwrap: false, unwrapKey }`, fails compilation.
|
|
124
|
+
*
|
|
125
|
+
* NOTE: excess-property checks apply to fresh literals only. A pre-built
|
|
126
|
+
* object widens `unwrap` to `boolean` — inline the options, annotate them,
|
|
127
|
+
* or use `as const`.
|
|
128
|
+
*/
|
|
129
|
+
type RiviniaRequestOptions = RiviniaRequestBase & (RiviniaUnwrapOn | RiviniaUnwrapOff);
|
|
130
|
+
/** Options for methods that never unwrap (blob/text/raw): unwrap fields rejected. */
|
|
131
|
+
type RiviniaRawOptions = RiviniaRequestBase & RiviniaUnwrapOff;
|
|
132
|
+
interface RiviniaDeleteOptions extends RiviniaRequestBase {
|
|
108
133
|
body?: unknown;
|
|
109
134
|
}
|
|
110
|
-
/**
|
|
111
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Delete accepts the same unwrap contract as the other unwrapping methods:
|
|
137
|
+
* compose the base delete shape with the union (an `interface extends`
|
|
138
|
+
* a union is illegal, and `Omit` over a union would destroy the
|
|
139
|
+
* discrimination — so the union is applied here explicitly).
|
|
140
|
+
*/
|
|
141
|
+
type RiviniaDeleteRequestOptions = RiviniaDeleteOptions & (RiviniaUnwrapOn | RiviniaUnwrapOff);
|
|
142
|
+
/**
|
|
143
|
+
* Reactive per-request fields for `getResource` (re-evaluated on every
|
|
144
|
+
* request; everything else is read once per call site).
|
|
145
|
+
*/
|
|
146
|
+
interface RiviniaResourceBase<T> {
|
|
112
147
|
/**
|
|
113
148
|
* Query params re-evaluated on every request: value, signal, or reactive
|
|
114
149
|
* function. Anything else in these options is read once per call site —
|
|
@@ -117,6 +152,7 @@ interface RiviniaResourceOptions<T> extends Omit<RiviniaRequestOptions, 'params'
|
|
|
117
152
|
params?: ReactiveInput<QueryParamsInput>;
|
|
118
153
|
headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;
|
|
119
154
|
context?: ReactiveInput<HttpContext>;
|
|
155
|
+
withCredentials?: boolean;
|
|
120
156
|
/** Value while idle/loading. Defaults to `undefined`. */
|
|
121
157
|
defaultValue?: T;
|
|
122
158
|
/**
|
|
@@ -130,17 +166,26 @@ interface RiviniaResourceOptions<T> extends Omit<RiviniaRequestOptions, 'params'
|
|
|
130
166
|
* plain Observable methods — use `HttpClient` events there directly.
|
|
131
167
|
*/
|
|
132
168
|
reportProgress?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Opt-in backend-envelope validation for this resource (replaces any
|
|
171
|
+
* global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.
|
|
172
|
+
*/
|
|
173
|
+
envelope?: RiviniaEnvelopePolicy;
|
|
133
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* `getResource` options: reactive base + the same per-call unwrap contract
|
|
177
|
+
* as the plain methods (composed explicitly — `Omit` over a union would
|
|
178
|
+
* destroy the discrimination).
|
|
179
|
+
*/
|
|
180
|
+
type RiviniaResourceOptions<T> = RiviniaResourceBase<T> & (RiviniaUnwrapOn | RiviniaUnwrapOff);
|
|
134
181
|
/**
|
|
135
182
|
* Headless typed HTTP client.
|
|
136
183
|
*
|
|
137
184
|
* Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches
|
|
138
185
|
* and `provideHttpClient()` testing all keep working). Owns no backend:
|
|
139
186
|
* URLs pass through untouched; the library adds opt-in envelope handling —
|
|
140
|
-
* unwrapping (`unwrap
|
|
141
|
-
*
|
|
142
|
-
* Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? false`,
|
|
143
|
-
* key: `options.unwrapKey ?? config.unwrapKey ?? 'data'`.
|
|
187
|
+
* per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no
|
|
188
|
+
* default) and failure validation (`envelope`).
|
|
144
189
|
*/
|
|
145
190
|
export declare class RiviniaHttp {
|
|
146
191
|
private readonly http;
|
|
@@ -149,23 +194,23 @@ export declare class RiviniaHttp {
|
|
|
149
194
|
post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
150
195
|
put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
151
196
|
patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
152
|
-
delete<T>(endpoint: string, options?:
|
|
197
|
+
delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T>;
|
|
153
198
|
/** Binary download. Never unwrapped — envelopes don't apply to blobs. */
|
|
154
|
-
getBlob(endpoint: string, options?:
|
|
199
|
+
getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob>;
|
|
155
200
|
/** Plain-text download. Never unwrapped. */
|
|
156
|
-
getText(endpoint: string, options?:
|
|
201
|
+
getText(endpoint: string, options?: RiviniaRawOptions): Observable<string>;
|
|
157
202
|
/**
|
|
158
203
|
* Full response access (status, headers — e.g. pagination counts or a 201
|
|
159
204
|
* `Location`). Raw body (no unwrapping), but the configured/per-call
|
|
160
205
|
* `envelope` validation still applies to the body — a globally declared
|
|
161
206
|
* failure policy must hold on every JSON path, not just the unwrapped ones.
|
|
162
207
|
*/
|
|
163
|
-
getResponse<T>(endpoint: string, options?:
|
|
208
|
+
getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>>;
|
|
164
209
|
/**
|
|
165
210
|
* Full response access for creates (status, `Location` header). Same
|
|
166
211
|
* envelope-validation rule as `getResponse`; body otherwise raw.
|
|
167
212
|
*/
|
|
168
|
-
postResponse<T>(endpoint: string, body?: unknown, options?:
|
|
213
|
+
postResponse<T>(endpoint: string, body?: unknown, options?: RiviniaRawOptions): Observable<HttpResponse<T>>;
|
|
169
214
|
/**
|
|
170
215
|
* Reactive GET as an `httpResource` (stable since Angular v22).
|
|
171
216
|
* The endpoint can be a static string, a signal, or a reactive function
|
|
@@ -175,8 +220,6 @@ export declare class RiviniaHttp {
|
|
|
175
220
|
*/
|
|
176
221
|
getResource<T>(endpoint: string | Signal<string | undefined> | (() => string | undefined), options?: RiviniaResourceOptions<T>): HttpResourceRef<T | undefined>;
|
|
177
222
|
private url;
|
|
178
|
-
private resolveUnwrap;
|
|
179
|
-
private resolveKey;
|
|
180
223
|
private resolveEnvelope;
|
|
181
224
|
private applyUnwrap;
|
|
182
225
|
private httpOptions;
|
|
@@ -189,12 +232,49 @@ export declare class RiviniaHttp {
|
|
|
189
232
|
* Absolute `http(s)` endpoints pass through untouched.
|
|
190
233
|
*/
|
|
191
234
|
export declare function joinUrl(baseUrl: string, endpoint: string): string;
|
|
235
|
+
/**
|
|
236
|
+
* Typed extraction failure. Thrown when explicit unwrapping
|
|
237
|
+
* (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object
|
|
238
|
+
* body, or missing property. Never thrown when unwrapping is disabled.
|
|
239
|
+
* In `getResource` this surfaces as `resource.error()`.
|
|
240
|
+
*/
|
|
241
|
+
export declare class RiviniaHttpUnwrapError extends Error {
|
|
242
|
+
/** The requested envelope key. */
|
|
243
|
+
readonly key: string;
|
|
244
|
+
/** The endpoint being read, when known (never the body). */
|
|
245
|
+
readonly endpoint: string | undefined;
|
|
246
|
+
/**
|
|
247
|
+
* The raw response body. Attached for diagnosis (a `parse` hook or logger
|
|
248
|
+
* can inspect it) — it may carry PII, so never log it blindly and never
|
|
249
|
+
* interpolate it into messages.
|
|
250
|
+
*/
|
|
251
|
+
readonly body: unknown;
|
|
252
|
+
constructor(key: string, body: unknown, endpoint?: string, reason?: string);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Strict single-level extraction shared by every unwrapping path (plain
|
|
256
|
+
* methods + `getResource`). Rules:
|
|
257
|
+
* - the key must be a non-empty string (an empty key is never intentional);
|
|
258
|
+
* - the body must be a non-null, non-array object;
|
|
259
|
+
* - presence is an OWN-property check (`hasOwnProperty`) — inherited
|
|
260
|
+
* properties (`'toString' in {}`) never count;
|
|
261
|
+
* - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is
|
|
262
|
+
* delivered unchanged — truthiness never decides success;
|
|
263
|
+
* - anything else throws `RiviniaHttpUnwrapError`. The original body is
|
|
264
|
+
* never silently returned after an explicit extraction fails.
|
|
265
|
+
*
|
|
266
|
+
* Limit, stated openly: presence proves nothing about the value matching an
|
|
267
|
+
* arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook
|
|
268
|
+
* (Zod/Valibot) when the backend is untyped.
|
|
269
|
+
*/
|
|
270
|
+
export declare function extractKey(body: unknown, key: string, endpoint?: string): unknown;
|
|
192
271
|
/**
|
|
193
272
|
* Extracts `body[key]` when the backend wraps payloads in an envelope
|
|
194
|
-
* (e.g. `{ data, error, isSuccess }`)
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
273
|
+
* (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object
|
|
274
|
+
* body, or an empty key throws `RiviniaHttpUnwrapError` — the original body
|
|
275
|
+
* is never silently passed through after an explicit extraction request.
|
|
276
|
+
* Unwraps EXACTLY ONE level: a payload that itself contains the key
|
|
277
|
+
* (`{ data: { data: […], page: 1 } }`) is returned whole.
|
|
198
278
|
*/
|
|
199
279
|
export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
200
280
|
/**
|
|
@@ -213,10 +293,10 @@ export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
|
213
293
|
*
|
|
214
294
|
* @example
|
|
215
295
|
* ```ts
|
|
216
|
-
* provideRiviniaHttp(
|
|
296
|
+
* provideRiviniaHttp()
|
|
217
297
|
* ```
|
|
218
298
|
*/
|
|
219
299
|
export declare function provideRiviniaHttp(config?: RiviniaHttpConfig): EnvironmentProviders;
|
|
220
300
|
/** Shorthand for `inject(RiviniaHttp)`. */
|
|
221
301
|
export declare function injectRiviniaHttp(): RiviniaHttp;
|
|
222
|
-
export type { QueryParamsInput, ReactiveInput, RiviniaDeleteOptions, RiviniaEnvelopePolicy, RiviniaHttpConfig, RiviniaRequestOptions, RiviniaResourceOptions };
|
|
302
|
+
export type { QueryParamsInput, ReactiveInput, RiviniaDeleteOptions, RiviniaDeleteRequestOptions, RiviniaEnvelopePolicy, RiviniaHttpConfig, RiviniaRawOptions, RiviniaRequestBase, RiviniaRequestOptions, RiviniaResourceBase, RiviniaResourceOptions, RiviniaUnwrapOff, RiviniaUnwrapOn };
|