@rivinia/http 0.3.0 → 0.5.0
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 +94 -29
- package/fesm2022/rivinia-http.mjs +96 -39
- 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 +192 -41
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Headless typed HTTP client for Angular 22+. Configurable envelope
|
|
|
7
7
|
unwrapping, plain Observables **and** `httpResource` — URLs always used as
|
|
8
8
|
given, zero styling, zero backend opinions, interceptors left to your app.
|
|
9
9
|
|
|
10
|
-
- Version: **0.
|
|
10
|
+
- Version: **0.5.0**
|
|
11
11
|
- Requires: **Angular `^22.0.0`**, `rxjs` (same range Angular uses), `tslib`
|
|
12
12
|
- Since Angular 21 `HttpClient` (fetch backend) is injectable with zero setup.
|
|
13
13
|
Call `provideHttpClient()` / pass `httpFeatures` only when you configure
|
|
@@ -17,7 +17,7 @@ given, zero styling, zero backend opinions, interceptors left to your app.
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install @rivinia/http
|
|
20
|
-
ng add @rivinia/http #
|
|
20
|
+
ng add @rivinia/http # wires app.config.ts (no questions: unwrapping is per call)
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Quick start — pick ONE backend style
|
|
@@ -62,62 +62,127 @@ export class TripPage {
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
```html
|
|
65
|
-
@if (trips.hasValue()) {
|
|
66
|
-
|
|
67
|
-
} @else if (trips.isLoading()) {
|
|
68
|
-
|
|
65
|
+
@if (trips.hasValue()) { @for (t of trips.value(); track t.id) {
|
|
66
|
+
<p>{{ t.title }}</p>
|
|
67
|
+
} } @else if (trips.isLoading()) {
|
|
68
|
+
<p>Loading…</p>
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
## Envelope unwrapping (
|
|
72
|
+
## Envelope unwrapping (explicit, per call)
|
|
73
73
|
|
|
74
74
|
The library assumes nothing about your backend: responses pass through raw
|
|
75
|
-
unless
|
|
76
|
-
|
|
75
|
+
unless the call opts in. There is no global unwrap configuration and no
|
|
76
|
+
default key — every unwrapping call states both facts:
|
|
77
77
|
|
|
78
78
|
```ts
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.api.get<Legacy>(url, { unwrap: true, unwrapKey: 'result' }); // this call only
|
|
82
|
-
this.api.get<Raw>(url); // raw body (default)
|
|
79
|
+
this.api.get<Raw>(url); // raw body (default)
|
|
80
|
+
this.api.get<Legacy>(url, { unwrap: true, unwrapKey: 'result' }); // explicit
|
|
83
81
|
```
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
The type system enforces the contract: `{ unwrap: true }` without a key,
|
|
84
|
+
a key alone, and `{ unwrap: false, unwrapKey }` all fail compilation.
|
|
85
|
+
Different shapes per API in the same app:
|
|
87
86
|
|
|
88
87
|
```ts
|
|
89
88
|
export class MixedService {
|
|
90
89
|
private api = injectRiviniaHttp();
|
|
91
90
|
|
|
92
|
-
// main backend: { data: … } (
|
|
93
|
-
trips = () => this.api.get<Trip[]>(fullUrl('trips'));
|
|
91
|
+
// main backend: { data: … } (explicit key per call)
|
|
92
|
+
trips = () => this.api.get<Trip[]>(fullUrl('trips'), { unwrap: true, unwrapKey: 'data' });
|
|
94
93
|
|
|
95
|
-
// legacy backend: { result: … }
|
|
96
|
-
legacy = () => this.api.get<Old[]>(fullUrl('old'), { unwrapKey: 'result' });
|
|
94
|
+
// legacy backend: { result: … }
|
|
95
|
+
legacy = () => this.api.get<Old[]>(fullUrl('old'), { unwrap: true, unwrapKey: 'result' });
|
|
97
96
|
|
|
98
|
-
// webhook endpoint: raw body
|
|
99
|
-
ping = () => this.api.get<string>(fullUrl('ping')
|
|
97
|
+
// webhook endpoint: raw body (default — nothing to say)
|
|
98
|
+
ping = () => this.api.get<string>(fullUrl('ping'));
|
|
100
99
|
}
|
|
101
100
|
```
|
|
102
101
|
|
|
102
|
+
A missing key never passes the body through: it throws
|
|
103
|
+
`RiviniaHttpUnwrapError` (with `key` + `endpoint`; the raw `body` is attached
|
|
104
|
+
for diagnosis but never interpolated into the message). Catch it like any
|
|
105
|
+
HTTP error — in `getResource` it surfaces as `resource.error()`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
this.api.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' }).subscribe({
|
|
109
|
+
next: (trips) => (this.list = trips),
|
|
110
|
+
error: (e) => this.fail(e), // RiviniaHttpUnwrapError | HttpErrorResponse
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
103
114
|
`getResource` applies the same rules through `parse` (your own `parse` runs
|
|
104
115
|
after unwrapping — ideal for Zod/Valibot validation).
|
|
105
116
|
|
|
117
|
+
## Envelope failure validation (opt-in)
|
|
118
|
+
|
|
119
|
+
Backends that report failure inside HTTP 200 (`{ data: null, isSuccess: false }`)
|
|
120
|
+
must never flow into `next()` as success — critical in payment flows:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
provideRiviniaHttp({ envelope: {} }); // whole app (isSuccess/error keys)
|
|
124
|
+
this.api.get(url, { envelope: { successKey: 'ok', errorKey: 'message' } });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A failed envelope throws `RiviniaHttpEnvelopeError` (`body` + `code` attached;
|
|
128
|
+
in `getResource` it surfaces as `resource.error()`). Validation runs on the raw
|
|
129
|
+
body before unwrapping, independent of `unwrap`. Per-call policy replaces any
|
|
130
|
+
global one wholesale.
|
|
131
|
+
|
|
106
132
|
## API
|
|
107
133
|
|
|
108
|
-
| Export
|
|
109
|
-
|
|
110
|
-
| `provideRiviniaHttp(config?)` | provider
|
|
111
|
-
| `RiviniaHttp`
|
|
112
|
-
| `injectRiviniaHttp()`
|
|
113
|
-
| `joinUrl(base, endpoint)`
|
|
114
|
-
| `unwrapAs(body, key)`
|
|
115
|
-
|
|
|
134
|
+
| Export | Kind | Purpose |
|
|
135
|
+
| ----------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `provideRiviniaHttp(config?)` | provider | `{ envelope?, httpFeatures? }` (no unwrap config — per call only) |
|
|
137
|
+
| `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, `getResource` |
|
|
138
|
+
| `injectRiviniaHttp()` | function | shorthand for `inject(RiviniaHttp)` |
|
|
139
|
+
| `joinUrl(base, endpoint)` | util | slash-safe join; absolute URLs pass through |
|
|
140
|
+
| `unwrapAs(body, key)` | util | strict single-level extraction — throws `RiviniaHttpUnwrapError` on missing key (nested keys preserved) |
|
|
141
|
+
| `extractKey(body, key)` | util | `unwrapAs` without the generic cast (same strictness) |
|
|
142
|
+
| types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaRawOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `RiviniaEnvelopePolicy`, `ReactiveInput`, `QueryParamsInput` |
|
|
143
|
+
|
|
144
|
+
`getResponse`/`postResponse` return the raw `HttpResponse` (status + headers,
|
|
145
|
+
body never unwrapped — and their options REJECT unwrap fields at compile
|
|
146
|
+
time via `RiviniaRawOptions`; compose `unwrapAs` yourself if needed). A configured
|
|
147
|
+
`envelope` policy still validates the body on these paths: a globally declared
|
|
148
|
+
failure rule holds on every JSON path, never just the unwrapped ones. `getText`
|
|
149
|
+
returns raw text; binary/text bodies cannot carry envelopes, so the policy does
|
|
150
|
+
not apply there. In `getResource`, the URL, `params`, `headers` and `context`
|
|
151
|
+
accept values, signals or reactive functions (everything else is static).
|
|
116
152
|
|
|
117
153
|
`getBlob` is never unwrapped. `HttpContext` passes through untouched, so
|
|
118
154
|
app interceptors (auth opt-out, error toasts) keep working. For caching,
|
|
119
155
|
polling, retry and TanStack Query interop see [RECIPES.md](./RECIPES.md).
|
|
120
156
|
|
|
157
|
+
## Type holes (honest, by design)
|
|
158
|
+
|
|
159
|
+
`unwrapAs<T>` is an unchecked cast — a wrong `T` compiles and mistypes the
|
|
160
|
+
payload at runtime. What IS checked is generic flow plus the unwrap contract
|
|
161
|
+
itself (see `http-type-safety.spec.ts` `@ts-expect-error` tests). For untyped
|
|
162
|
+
backends, pair unwrapping with `envelope` validation or a `parse` hook
|
|
163
|
+
(Zod/Valibot — see [RECIPES.md](./RECIPES.md)).
|
|
164
|
+
|
|
165
|
+
## Migration from global unwrapping (0.4.0 → 0.5.0, breaking)
|
|
166
|
+
|
|
167
|
+
Global `unwrap`/`unwrapKey` configuration is removed. Previously:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
provideRiviniaHttp({ unwrap: true, unwrapKey: 'data' });
|
|
171
|
+
this.api.get<Trip[]>(url); // arrived unwrapped
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Now every unwrapping call states its key explicitly:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
provideRiviniaHttp();
|
|
178
|
+
this.api.get<Trip[]>(url, { unwrap: true, unwrapKey: 'data' });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Migrate each applicable call, or the body arrives wrapped (raw). Missing
|
|
182
|
+
keys now throw `RiviniaHttpUnwrapError` instead of passing the body through.
|
|
183
|
+
`ng add` no longer asks about unwrapping and emits bare
|
|
184
|
+
`provideRiviniaHttp()` — there is no automatic migration of call sites.
|
|
185
|
+
|
|
121
186
|
## Testing your app code
|
|
122
187
|
|
|
123
188
|
Standard Angular tooling — nothing custom to learn:
|
|
@@ -8,37 +8,69 @@ 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
|
-
|
|
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];
|
|
14
29
|
}
|
|
15
|
-
function
|
|
16
|
-
return
|
|
30
|
+
function unwrapAs(body, key) {
|
|
31
|
+
return extractKey(body, key);
|
|
17
32
|
}
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
var RiviniaHttpEnvelopeError = class extends Error {
|
|
34
|
+
body;
|
|
35
|
+
code;
|
|
36
|
+
constructor(body, code) {
|
|
37
|
+
super(typeof code === "string" && code.length > 0 ? `[rivinia/http] Request failed: ${code}` : "[rivinia/http] Request failed (backend envelope)");
|
|
38
|
+
this.name = "RiviniaHttpEnvelopeError";
|
|
39
|
+
this.body = body;
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const DEFAULT_SUCCESS_KEY = "isSuccess";
|
|
44
|
+
const DEFAULT_ERROR_KEY = "error";
|
|
45
|
+
function assertEnvelope(body, policy) {
|
|
46
|
+
if (policy.throwOnFailure === false) return;
|
|
47
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return;
|
|
48
|
+
const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;
|
|
49
|
+
const record = body;
|
|
50
|
+
if (!(successKey in record) || record[successKey] !== false) return;
|
|
51
|
+
throw new RiviniaHttpEnvelopeError(body, record[policy.errorKey ?? DEFAULT_ERROR_KEY]);
|
|
20
52
|
}
|
|
21
53
|
const RIVINIA_HTTP_CONFIG = new InjectionToken("RIVINIA_HTTP_CONFIG");
|
|
22
54
|
var RiviniaHttp = class RiviniaHttp {
|
|
23
55
|
http = inject(HttpClient);
|
|
24
56
|
config = inject(RIVINIA_HTTP_CONFIG, { optional: true });
|
|
25
57
|
get(endpoint, options) {
|
|
26
|
-
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)));
|
|
27
59
|
}
|
|
28
60
|
post(endpoint, body, options) {
|
|
29
|
-
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)));
|
|
30
62
|
}
|
|
31
63
|
put(endpoint, body, options) {
|
|
32
|
-
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)));
|
|
33
65
|
}
|
|
34
66
|
patch(endpoint, body, options) {
|
|
35
|
-
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)));
|
|
36
68
|
}
|
|
37
69
|
delete(endpoint, options) {
|
|
38
70
|
return this.http.delete(this.url(endpoint), {
|
|
39
71
|
...this.httpOptions(options),
|
|
40
72
|
body: options?.body
|
|
41
|
-
}).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
73
|
+
}).pipe(map((res) => this.applyUnwrap(endpoint, res, options)));
|
|
42
74
|
}
|
|
43
75
|
getBlob(endpoint, options) {
|
|
44
76
|
return this.http.get(this.url(endpoint), {
|
|
@@ -46,6 +78,32 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
46
78
|
responseType: "blob"
|
|
47
79
|
});
|
|
48
80
|
}
|
|
81
|
+
getText(endpoint, options) {
|
|
82
|
+
return this.http.get(this.url(endpoint), {
|
|
83
|
+
...this.httpOptions(options),
|
|
84
|
+
responseType: "text"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
getResponse(endpoint, options) {
|
|
88
|
+
const envelope = this.resolveEnvelope(options);
|
|
89
|
+
return this.http.get(this.url(endpoint), {
|
|
90
|
+
...this.httpOptions(options),
|
|
91
|
+
observe: "response"
|
|
92
|
+
}).pipe(map((res) => {
|
|
93
|
+
if (envelope) assertEnvelope(res.body, envelope);
|
|
94
|
+
return res;
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
postResponse(endpoint, body, options) {
|
|
98
|
+
const envelope = this.resolveEnvelope(options);
|
|
99
|
+
return this.http.post(this.url(endpoint), body, {
|
|
100
|
+
...this.httpOptions(options),
|
|
101
|
+
observe: "response"
|
|
102
|
+
}).pipe(map((res) => {
|
|
103
|
+
if (envelope) assertEnvelope(res.body, envelope);
|
|
104
|
+
return res;
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
49
107
|
getResource(endpoint, options) {
|
|
50
108
|
const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};
|
|
51
109
|
const source = typeof endpoint === "function" && !isSignal(endpoint) ? computed(endpoint) : endpoint;
|
|
@@ -54,48 +112,41 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
54
112
|
if (ep === void 0) return void 0;
|
|
55
113
|
return {
|
|
56
114
|
url: this.url(ep),
|
|
57
|
-
params: this.toParams(requestOptions.params),
|
|
58
|
-
headers: requestOptions.headers,
|
|
59
|
-
context: requestOptions.context,
|
|
115
|
+
params: this.toParams(resolveInput(requestOptions.params)),
|
|
116
|
+
headers: resolveInput(requestOptions.headers),
|
|
117
|
+
context: resolveInput(requestOptions.context),
|
|
60
118
|
withCredentials: requestOptions.withCredentials,
|
|
61
119
|
...reportProgress !== void 0 ? { reportProgress } : {}
|
|
62
120
|
};
|
|
63
121
|
};
|
|
64
|
-
const
|
|
65
|
-
const unwrapEnabled = this.resolveUnwrap(requestOptions);
|
|
122
|
+
const envelope = this.resolveEnvelope(requestOptions);
|
|
66
123
|
const base = { defaultValue };
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
...base,
|
|
74
|
-
defaultValue,
|
|
75
|
-
parse: applyParse
|
|
76
|
-
});
|
|
77
|
-
}
|
|
124
|
+
const applyParse = (raw) => {
|
|
125
|
+
if (envelope) assertEnvelope(raw, envelope);
|
|
126
|
+
if (requestOptions.unwrap !== true) return parse ? parse(raw) : raw;
|
|
127
|
+
const unwrapped = extractKey(raw, requestOptions.unwrapKey);
|
|
128
|
+
return parse ? parse(unwrapped) : unwrapped;
|
|
129
|
+
};
|
|
78
130
|
return defaultValue === void 0 ? httpResource(request, {
|
|
79
131
|
...base,
|
|
80
|
-
|
|
132
|
+
parse: applyParse
|
|
81
133
|
}) : httpResource(request, {
|
|
82
134
|
...base,
|
|
83
135
|
defaultValue,
|
|
84
|
-
|
|
136
|
+
parse: applyParse
|
|
85
137
|
});
|
|
86
138
|
}
|
|
87
139
|
url(endpoint) {
|
|
88
140
|
return endpoint;
|
|
89
141
|
}
|
|
90
|
-
|
|
91
|
-
return options?.
|
|
142
|
+
resolveEnvelope(options) {
|
|
143
|
+
return options?.envelope ?? this.config?.envelope;
|
|
92
144
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return unwrapAs(body, this.resolveKey(options));
|
|
145
|
+
applyUnwrap(endpoint, body, options) {
|
|
146
|
+
const envelope = this.resolveEnvelope(options);
|
|
147
|
+
if (envelope) assertEnvelope(body, envelope);
|
|
148
|
+
if (options?.unwrap !== true) return body;
|
|
149
|
+
return extractKey(body, options.unwrapKey, endpoint);
|
|
99
150
|
}
|
|
100
151
|
httpOptions(options) {
|
|
101
152
|
return {
|
|
@@ -134,6 +185,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
134
185
|
type: RiviniaHttp,
|
|
135
186
|
decorators: [{ type: Service }]
|
|
136
187
|
});
|
|
188
|
+
function resolveInput(value) {
|
|
189
|
+
if (value === void 0) return void 0;
|
|
190
|
+
if (isSignal(value)) return value();
|
|
191
|
+
if (typeof value === "function") return value();
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
137
194
|
function provideRiviniaHttp(config = {}) {
|
|
138
195
|
const { httpFeatures, ...rest } = config;
|
|
139
196
|
return makeEnvironmentProviders([...httpFeatures ? [provideHttpClient(...httpFeatures)] : [], {
|
|
@@ -144,6 +201,6 @@ function provideRiviniaHttp(config = {}) {
|
|
|
144
201
|
function injectRiviniaHttp() {
|
|
145
202
|
return inject(RiviniaHttp);
|
|
146
203
|
}
|
|
147
|
-
export { RiviniaHttp, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
204
|
+
export { RiviniaHttp, RiviniaHttpEnvelopeError, RiviniaHttpUnwrapError, extractKey, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
148
205
|
|
|
149
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-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. Recursive, so nested envelopes collapse.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n if (isObjectRecord(body) && hasOwn(body, key)) {\n return unwrapAs<T>(body[key], key);\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","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\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\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 HttpResourceRef,\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 RiviniaDeleteOptions,\n type RiviniaRequestOptions,\n} from './http-options';\nimport { unwrapAs } from './http-utils';\n\n/** Options for `getResource` in addition to per-request options. */\nexport interface RiviniaResourceOptions<T> extends RiviniaRequestOptions {\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 * the app provides `HttpClient`, this library only adds base-URL joining\n * plus configurable envelope unwrapping.\n *\n * Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? true`,\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 /**\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(requestOptions.params),\n headers: requestOptions.headers,\n context: 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 base = { defaultValue: defaultValue as T | undefined };\n if (unwrapEnabled) {\n const applyParse = (raw: unknown): T =>\n parse ? parse(unwrapAs<T>(raw, unwrapKey)) : unwrapAs<T>(raw, unwrapKey);\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 return defaultValue === undefined\n ? httpResource<T>(request, { ...base, ...(parse ? { parse } : {}) })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n ...(parse ? { parse } : {}),\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?: RiviniaRequestOptions): boolean {\n return options?.unwrap ?? this.config?.unwrap ?? DEFAULT_UNWRAP;\n }\n\n private resolveKey(options?: RiviniaRequestOptions): string {\n return options?.unwrapKey ?? this.config?.unwrapKey ?? DEFAULT_UNWRAP_KEY;\n }\n\n private applyUnwrap<T>(body: unknown, options?: RiviniaRequestOptions): T {\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 {\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","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":"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"}
|
|
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
|
@@ -1,27 +1,48 @@
|
|
|
1
|
-
import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef } from "@angular/common/http";
|
|
1
|
+
import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef, HttpResponse } from "@angular/common/http";
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
import { EnvironmentProviders, InjectionToken, Signal } from "@angular/core";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
5
|
+
/**
|
|
6
|
+
* Typed envelope failure. Thrown when a backend signals failure inside a
|
|
7
|
+
* successful HTTP response (e.g. `{ data: null, isSuccess: false }` with
|
|
8
|
+
* HTTP 200) and the envelope policy opts into throwing. In `getResource`
|
|
9
|
+
* this surfaces as `resource.error()`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RiviniaHttpEnvelopeError extends Error {
|
|
12
|
+
/** The raw response body. */
|
|
13
|
+
readonly body: unknown;
|
|
14
|
+
/** The machine-readable failure code (`body[errorKey]`), if any. */
|
|
15
|
+
readonly code: unknown;
|
|
16
|
+
constructor(body: unknown, code: unknown);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Opt-in backend-envelope validation. When present (globally or per call),
|
|
20
|
+
* a response object whose `successKey` is strictly `false` throws
|
|
21
|
+
* `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.
|
|
22
|
+
* Runs on the raw body before unwrapping, independent of `unwrap`.
|
|
23
|
+
*/
|
|
24
|
+
interface RiviniaEnvelopePolicy {
|
|
25
|
+
/** Body key holding the success flag. @default 'isSuccess' */
|
|
26
|
+
readonly successKey?: string;
|
|
27
|
+
/** Body key holding the failure code. @default 'error' */
|
|
28
|
+
readonly errorKey?: string;
|
|
29
|
+
/** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */
|
|
30
|
+
readonly throwOnFailure?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Throws when the envelope signals failure and the policy opts in. */
|
|
33
|
+
declare function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void;
|
|
5
34
|
/**
|
|
6
35
|
* Global configuration for `@rivinia/http`.
|
|
7
36
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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`.
|
|
12
44
|
*/
|
|
13
45
|
interface RiviniaHttpConfig {
|
|
14
|
-
/**
|
|
15
|
-
* Unwrap `{ [unwrapKey]: payload }` envelopes by default.
|
|
16
|
-
* @default false — the library assumes nothing about your backend;
|
|
17
|
-
* opt in globally here or per call with `{ unwrap: true }`.
|
|
18
|
-
*/
|
|
19
|
-
unwrap?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Envelope key unwrapped when `unwrap` is enabled.
|
|
22
|
-
* @default 'data'
|
|
23
|
-
*/
|
|
24
|
-
unwrapKey?: string;
|
|
25
46
|
/**
|
|
26
47
|
* Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),
|
|
27
48
|
* applied via a single internal `provideHttpClient(...httpFeatures)` call.
|
|
@@ -33,36 +54,105 @@ interface RiviniaHttpConfig {
|
|
|
33
54
|
* `HttpClient` (fetch backend) is injectable by default.
|
|
34
55
|
*/
|
|
35
56
|
httpFeatures?: HttpFeature<HttpFeatureKind>[];
|
|
57
|
+
/**
|
|
58
|
+
* Opt-in backend-envelope validation applied to every call unless the call
|
|
59
|
+
* provides its own `envelope` (which replaces this wholesale). See
|
|
60
|
+
* `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.
|
|
61
|
+
*/
|
|
62
|
+
envelope?: RiviniaEnvelopePolicy;
|
|
36
63
|
}
|
|
37
64
|
/**
|
|
38
65
|
* Internal configuration token. Consumers must use `provideRiviniaHttp()`.
|
|
39
66
|
* @internal
|
|
40
67
|
*/
|
|
41
68
|
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
/**
|
|
70
|
+
* A value, a signal of it, or a reactive function returning it — evaluated
|
|
71
|
+
* inside the `httpResource` request function, so signal reads stay reactive
|
|
72
|
+
* (same contract as `httpResource` itself).
|
|
73
|
+
*/
|
|
74
|
+
type ReactiveInput<T> = T | Signal<T> | (() => T);
|
|
44
75
|
/** Shorthand for building query strings; `null`/`undefined` values are dropped. */
|
|
45
76
|
type QueryParamsInput = HttpParams | Record<string, string | number | boolean | undefined | null>;
|
|
46
77
|
/**
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
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`.
|
|
88
|
+
*/
|
|
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.
|
|
49
103
|
*/
|
|
50
|
-
interface
|
|
104
|
+
interface RiviniaRequestBase {
|
|
51
105
|
params?: QueryParamsInput;
|
|
52
106
|
headers?: HttpHeaders | Record<string, string | string[]>;
|
|
53
107
|
/** Pass-through for app-owned interceptors (auth opt-out, error policy, ...). */
|
|
54
108
|
context?: HttpContext;
|
|
55
109
|
withCredentials?: boolean;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Opt-in backend-envelope validation for this call (replaces any global
|
|
112
|
+
* `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.
|
|
113
|
+
*/
|
|
114
|
+
envelope?: RiviniaEnvelopePolicy;
|
|
60
115
|
}
|
|
61
|
-
|
|
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 {
|
|
62
133
|
body?: unknown;
|
|
63
134
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
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> {
|
|
147
|
+
/**
|
|
148
|
+
* Query params re-evaluated on every request: value, signal, or reactive
|
|
149
|
+
* function. Anything else in these options is read once per call site —
|
|
150
|
+
* only the URL, params, headers and context are reactive.
|
|
151
|
+
*/
|
|
152
|
+
params?: ReactiveInput<QueryParamsInput>;
|
|
153
|
+
headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;
|
|
154
|
+
context?: ReactiveInput<HttpContext>;
|
|
155
|
+
withCredentials?: boolean;
|
|
66
156
|
/** Value while idle/loading. Defaults to `undefined`. */
|
|
67
157
|
defaultValue?: T;
|
|
68
158
|
/**
|
|
@@ -76,17 +166,26 @@ interface RiviniaResourceOptions<T> extends RiviniaRequestOptions {
|
|
|
76
166
|
* plain Observable methods — use `HttpClient` events there directly.
|
|
77
167
|
*/
|
|
78
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;
|
|
79
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);
|
|
80
181
|
/**
|
|
81
182
|
* Headless typed HTTP client.
|
|
82
183
|
*
|
|
83
184
|
* Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches
|
|
84
185
|
* and `provideHttpClient()` testing all keep working). Owns no backend:
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? true`,
|
|
89
|
-
* key: `options.unwrapKey ?? config.unwrapKey ?? 'data'`.
|
|
186
|
+
* URLs pass through untouched; the library adds opt-in envelope handling —
|
|
187
|
+
* per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no
|
|
188
|
+
* default) and failure validation (`envelope`).
|
|
90
189
|
*/
|
|
91
190
|
export declare class RiviniaHttp {
|
|
92
191
|
private readonly http;
|
|
@@ -95,9 +194,23 @@ export declare class RiviniaHttp {
|
|
|
95
194
|
post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
96
195
|
put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
97
196
|
patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
98
|
-
delete<T>(endpoint: string, options?:
|
|
197
|
+
delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T>;
|
|
99
198
|
/** Binary download. Never unwrapped — envelopes don't apply to blobs. */
|
|
100
|
-
getBlob(endpoint: string, options?:
|
|
199
|
+
getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob>;
|
|
200
|
+
/** Plain-text download. Never unwrapped. */
|
|
201
|
+
getText(endpoint: string, options?: RiviniaRawOptions): Observable<string>;
|
|
202
|
+
/**
|
|
203
|
+
* Full response access (status, headers — e.g. pagination counts or a 201
|
|
204
|
+
* `Location`). Raw body (no unwrapping), but the configured/per-call
|
|
205
|
+
* `envelope` validation still applies to the body — a globally declared
|
|
206
|
+
* failure policy must hold on every JSON path, not just the unwrapped ones.
|
|
207
|
+
*/
|
|
208
|
+
getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>>;
|
|
209
|
+
/**
|
|
210
|
+
* Full response access for creates (status, `Location` header). Same
|
|
211
|
+
* envelope-validation rule as `getResponse`; body otherwise raw.
|
|
212
|
+
*/
|
|
213
|
+
postResponse<T>(endpoint: string, body?: unknown, options?: RiviniaRawOptions): Observable<HttpResponse<T>>;
|
|
101
214
|
/**
|
|
102
215
|
* Reactive GET as an `httpResource` (stable since Angular v22).
|
|
103
216
|
* The endpoint can be a static string, a signal, or a reactive function
|
|
@@ -107,8 +220,7 @@ export declare class RiviniaHttp {
|
|
|
107
220
|
*/
|
|
108
221
|
getResource<T>(endpoint: string | Signal<string | undefined> | (() => string | undefined), options?: RiviniaResourceOptions<T>): HttpResourceRef<T | undefined>;
|
|
109
222
|
private url;
|
|
110
|
-
private
|
|
111
|
-
private resolveKey;
|
|
223
|
+
private resolveEnvelope;
|
|
112
224
|
private applyUnwrap;
|
|
113
225
|
private httpOptions;
|
|
114
226
|
private toParams;
|
|
@@ -120,10 +232,49 @@ export declare class RiviniaHttp {
|
|
|
120
232
|
* Absolute `http(s)` endpoints pass through untouched.
|
|
121
233
|
*/
|
|
122
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;
|
|
123
271
|
/**
|
|
124
272
|
* Extracts `body[key]` when the backend wraps payloads in an envelope
|
|
125
|
-
* (e.g. `{ data, error, isSuccess }`)
|
|
126
|
-
*
|
|
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.
|
|
127
278
|
*/
|
|
128
279
|
export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
129
280
|
/**
|
|
@@ -142,10 +293,10 @@ export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
|
142
293
|
*
|
|
143
294
|
* @example
|
|
144
295
|
* ```ts
|
|
145
|
-
* provideRiviniaHttp(
|
|
296
|
+
* provideRiviniaHttp()
|
|
146
297
|
* ```
|
|
147
298
|
*/
|
|
148
299
|
export declare function provideRiviniaHttp(config?: RiviniaHttpConfig): EnvironmentProviders;
|
|
149
300
|
/** Shorthand for `inject(RiviniaHttp)`. */
|
|
150
301
|
export declare function injectRiviniaHttp(): RiviniaHttp;
|
|
151
|
-
export type { QueryParamsInput, RiviniaDeleteOptions, RiviniaHttpConfig, RiviniaRequestOptions, RiviniaResourceOptions };
|
|
302
|
+
export type { QueryParamsInput, ReactiveInput, RiviniaDeleteOptions, RiviniaDeleteRequestOptions, RiviniaEnvelopePolicy, RiviniaHttpConfig, RiviniaRawOptions, RiviniaRequestBase, RiviniaRequestOptions, RiviniaResourceBase, RiviniaResourceOptions, RiviniaUnwrapOff, RiviniaUnwrapOn };
|