@rivinia/http 0.2.0 → 0.4.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 +28 -5
- package/fesm2022/rivinia-http.mjs +73 -20
- package/fesm2022/rivinia-http.mjs.map +1 -1
- package/package.json +1 -1
- package/types/rivinia-http.d.ts +83 -10
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.4.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
|
|
@@ -103,16 +103,39 @@ export class MixedService {
|
|
|
103
103
|
`getResource` applies the same rules through `parse` (your own `parse` runs
|
|
104
104
|
after unwrapping — ideal for Zod/Valibot validation).
|
|
105
105
|
|
|
106
|
+
## Envelope failure validation (opt-in)
|
|
107
|
+
|
|
108
|
+
Backends that report failure inside HTTP 200 (`{ data: null, isSuccess: false }`)
|
|
109
|
+
must never flow into `next()` as success — critical in payment flows:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
provideRiviniaHttp({ envelope: {} }); // whole app (isSuccess/error keys)
|
|
113
|
+
this.api.get(url, { envelope: { successKey: 'ok', errorKey: 'message' } });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A failed envelope throws `RiviniaHttpEnvelopeError` (`body` + `code` attached;
|
|
117
|
+
in `getResource` it surfaces as `resource.error()`). Validation runs on the raw
|
|
118
|
+
body before unwrapping, independent of `unwrap`. Per-call policy replaces any
|
|
119
|
+
global one wholesale.
|
|
120
|
+
|
|
106
121
|
## API
|
|
107
122
|
|
|
108
123
|
| Export | Kind | Purpose |
|
|
109
124
|
|---|---|---|
|
|
110
|
-
| `provideRiviniaHttp(config?)` | provider | `{ unwrap?=false, unwrapKey?='data', httpFeatures? }` |
|
|
111
|
-
| `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getResource` |
|
|
125
|
+
| `provideRiviniaHttp(config?)` | provider | `{ unwrap?=false, unwrapKey?='data', envelope?, httpFeatures? }` |
|
|
126
|
+
| `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getText`, `getResponse`, `postResponse`, `getResource` |
|
|
112
127
|
| `injectRiviniaHttp()` | function | shorthand for `inject(RiviniaHttp)` |
|
|
113
128
|
| `joinUrl(base, endpoint)` | util | slash-safe join; absolute URLs pass through |
|
|
114
|
-
| `unwrapAs(body, key)` | util |
|
|
115
|
-
| types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `QueryParamsInput` |
|
|
129
|
+
| `unwrapAs(body, key)` | util | single-level envelope extraction (nested keys preserved) |
|
|
130
|
+
| types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `RiviniaEnvelopePolicy`, `ReactiveInput`, `QueryParamsInput` |
|
|
131
|
+
|
|
132
|
+
`getResponse`/`postResponse` return the raw `HttpResponse` (status + headers,
|
|
133
|
+
body never unwrapped — compose `unwrapAs` yourself if needed). A configured
|
|
134
|
+
`envelope` policy still validates the body on these paths: a globally declared
|
|
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).
|
|
116
139
|
|
|
117
140
|
`getBlob` is never unwrapped. `HttpContext` passes through untouched, so
|
|
118
141
|
app interceptors (auth opt-out, error toasts) keep working. For caching,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { InjectionToken, Service, inject, makeEnvironmentProviders } from "@angular/core";
|
|
2
|
+
import { InjectionToken, Service, computed, inject, isSignal, makeEnvironmentProviders } from "@angular/core";
|
|
3
3
|
import { HttpClient, HttpContext, HttpParams, httpResource, provideHttpClient } from "@angular/common/http";
|
|
4
4
|
import { map } from "rxjs";
|
|
5
5
|
function joinUrl(baseUrl, endpoint) {
|
|
@@ -9,7 +9,7 @@ function joinUrl(baseUrl, endpoint) {
|
|
|
9
9
|
return path ? `${base}/${path}` : base;
|
|
10
10
|
}
|
|
11
11
|
function unwrapAs(body, key) {
|
|
12
|
-
if (isObjectRecord(body) && hasOwn(body, key)) return
|
|
12
|
+
if (isObjectRecord(body) && hasOwn(body, key)) return body[key];
|
|
13
13
|
return body;
|
|
14
14
|
}
|
|
15
15
|
function isObjectRecord(value) {
|
|
@@ -18,6 +18,26 @@ function isObjectRecord(value) {
|
|
|
18
18
|
function hasOwn(obj, key) {
|
|
19
19
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
20
20
|
}
|
|
21
|
+
var RiviniaHttpEnvelopeError = class extends Error {
|
|
22
|
+
body;
|
|
23
|
+
code;
|
|
24
|
+
constructor(body, code) {
|
|
25
|
+
super(typeof code === "string" && code.length > 0 ? `[rivinia/http] Request failed: ${code}` : "[rivinia/http] Request failed (backend envelope)");
|
|
26
|
+
this.name = "RiviniaHttpEnvelopeError";
|
|
27
|
+
this.body = body;
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const DEFAULT_SUCCESS_KEY = "isSuccess";
|
|
32
|
+
const DEFAULT_ERROR_KEY = "error";
|
|
33
|
+
function assertEnvelope(body, policy) {
|
|
34
|
+
if (policy.throwOnFailure === false) return;
|
|
35
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return;
|
|
36
|
+
const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;
|
|
37
|
+
const record = body;
|
|
38
|
+
if (!(successKey in record) || record[successKey] !== false) return;
|
|
39
|
+
throw new RiviniaHttpEnvelopeError(body, record[policy.errorKey ?? DEFAULT_ERROR_KEY]);
|
|
40
|
+
}
|
|
21
41
|
const RIVINIA_HTTP_CONFIG = new InjectionToken("RIVINIA_HTTP_CONFIG");
|
|
22
42
|
var RiviniaHttp = class RiviniaHttp {
|
|
23
43
|
http = inject(HttpClient);
|
|
@@ -46,41 +66,63 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
46
66
|
responseType: "blob"
|
|
47
67
|
});
|
|
48
68
|
}
|
|
69
|
+
getText(endpoint, options) {
|
|
70
|
+
return this.http.get(this.url(endpoint), {
|
|
71
|
+
...this.httpOptions(options),
|
|
72
|
+
responseType: "text"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
getResponse(endpoint, options) {
|
|
76
|
+
const envelope = this.resolveEnvelope(options);
|
|
77
|
+
return this.http.get(this.url(endpoint), {
|
|
78
|
+
...this.httpOptions(options),
|
|
79
|
+
observe: "response"
|
|
80
|
+
}).pipe(map((res) => {
|
|
81
|
+
if (envelope) assertEnvelope(res.body, envelope);
|
|
82
|
+
return res;
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
postResponse(endpoint, body, options) {
|
|
86
|
+
const envelope = this.resolveEnvelope(options);
|
|
87
|
+
return this.http.post(this.url(endpoint), body, {
|
|
88
|
+
...this.httpOptions(options),
|
|
89
|
+
observe: "response"
|
|
90
|
+
}).pipe(map((res) => {
|
|
91
|
+
if (envelope) assertEnvelope(res.body, envelope);
|
|
92
|
+
return res;
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
49
95
|
getResource(endpoint, options) {
|
|
50
96
|
const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};
|
|
97
|
+
const source = typeof endpoint === "function" && !isSignal(endpoint) ? computed(endpoint) : endpoint;
|
|
51
98
|
const request = () => {
|
|
52
|
-
const ep = typeof
|
|
99
|
+
const ep = typeof source === "string" ? source : source();
|
|
53
100
|
if (ep === void 0) return void 0;
|
|
54
101
|
return {
|
|
55
102
|
url: this.url(ep),
|
|
56
|
-
params: this.toParams(requestOptions.params),
|
|
57
|
-
headers: requestOptions.headers,
|
|
58
|
-
context: requestOptions.context,
|
|
103
|
+
params: this.toParams(resolveInput(requestOptions.params)),
|
|
104
|
+
headers: resolveInput(requestOptions.headers),
|
|
105
|
+
context: resolveInput(requestOptions.context),
|
|
59
106
|
withCredentials: requestOptions.withCredentials,
|
|
60
107
|
...reportProgress !== void 0 ? { reportProgress } : {}
|
|
61
108
|
};
|
|
62
109
|
};
|
|
63
110
|
const unwrapKey = this.resolveKey(requestOptions);
|
|
64
111
|
const unwrapEnabled = this.resolveUnwrap(requestOptions);
|
|
112
|
+
const envelope = this.resolveEnvelope(requestOptions);
|
|
65
113
|
const base = { defaultValue };
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}) : httpResource(request, {
|
|
72
|
-
...base,
|
|
73
|
-
defaultValue,
|
|
74
|
-
parse: applyParse
|
|
75
|
-
});
|
|
76
|
-
}
|
|
114
|
+
const applyParse = (raw) => {
|
|
115
|
+
if (envelope) assertEnvelope(raw, envelope);
|
|
116
|
+
const unwrapped = unwrapEnabled ? unwrapAs(raw, unwrapKey) : raw;
|
|
117
|
+
return parse ? parse(unwrapped) : unwrapped;
|
|
118
|
+
};
|
|
77
119
|
return defaultValue === void 0 ? httpResource(request, {
|
|
78
120
|
...base,
|
|
79
|
-
|
|
121
|
+
parse: applyParse
|
|
80
122
|
}) : httpResource(request, {
|
|
81
123
|
...base,
|
|
82
124
|
defaultValue,
|
|
83
|
-
|
|
125
|
+
parse: applyParse
|
|
84
126
|
});
|
|
85
127
|
}
|
|
86
128
|
url(endpoint) {
|
|
@@ -92,7 +134,12 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
92
134
|
resolveKey(options) {
|
|
93
135
|
return options?.unwrapKey ?? this.config?.unwrapKey ?? "data";
|
|
94
136
|
}
|
|
137
|
+
resolveEnvelope(options) {
|
|
138
|
+
return options?.envelope ?? this.config?.envelope;
|
|
139
|
+
}
|
|
95
140
|
applyUnwrap(body, options) {
|
|
141
|
+
const envelope = this.resolveEnvelope(options);
|
|
142
|
+
if (envelope) assertEnvelope(body, envelope);
|
|
96
143
|
if (!this.resolveUnwrap(options)) return body;
|
|
97
144
|
return unwrapAs(body, this.resolveKey(options));
|
|
98
145
|
}
|
|
@@ -133,6 +180,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
133
180
|
type: RiviniaHttp,
|
|
134
181
|
decorators: [{ type: Service }]
|
|
135
182
|
});
|
|
183
|
+
function resolveInput(value) {
|
|
184
|
+
if (value === void 0) return void 0;
|
|
185
|
+
if (isSignal(value)) return value();
|
|
186
|
+
if (typeof value === "function") return value();
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
136
189
|
function provideRiviniaHttp(config = {}) {
|
|
137
190
|
const { httpFeatures, ...rest } = config;
|
|
138
191
|
return makeEnvironmentProviders([...httpFeatures ? [provideHttpClient(...httpFeatures)] : [], {
|
|
@@ -143,6 +196,6 @@ function provideRiviniaHttp(config = {}) {
|
|
|
143
196
|
function injectRiviniaHttp() {
|
|
144
197
|
return inject(RiviniaHttp);
|
|
145
198
|
}
|
|
146
|
-
export { RiviniaHttp, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
199
|
+
export { RiviniaHttp, RiviniaHttpEnvelopeError, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
147
200
|
|
|
148
201
|
//# 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, inject, 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 * Re-fetches when the endpoint signal changes; `undefined` disables.\n * Unwrap applies through `parse`, composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined>,\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const request = () => {\n const ep = typeof endpoint === 'string' ? endpoint : endpoint();\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 * 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":"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"}
|
package/package.json
CHANGED
package/types/rivinia-http.d.ts
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
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
|
*
|
|
@@ -33,6 +62,12 @@ interface RiviniaHttpConfig {
|
|
|
33
62
|
* `HttpClient` (fetch backend) is injectable by default.
|
|
34
63
|
*/
|
|
35
64
|
httpFeatures?: HttpFeature<HttpFeatureKind>[];
|
|
65
|
+
/**
|
|
66
|
+
* Opt-in backend-envelope validation applied to every call unless the call
|
|
67
|
+
* provides its own `envelope` (which replaces this wholesale). See
|
|
68
|
+
* `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.
|
|
69
|
+
*/
|
|
70
|
+
envelope?: RiviniaEnvelopePolicy;
|
|
36
71
|
}
|
|
37
72
|
/**
|
|
38
73
|
* Internal configuration token. Consumers must use `provideRiviniaHttp()`.
|
|
@@ -41,6 +76,12 @@ interface RiviniaHttpConfig {
|
|
|
41
76
|
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
42
77
|
declare const DEFAULT_UNWRAP = false;
|
|
43
78
|
declare const DEFAULT_UNWRAP_KEY = "data";
|
|
79
|
+
/**
|
|
80
|
+
* A value, a signal of it, or a reactive function returning it — evaluated
|
|
81
|
+
* inside the `httpResource` request function, so signal reads stay reactive
|
|
82
|
+
* (same contract as `httpResource` itself).
|
|
83
|
+
*/
|
|
84
|
+
type ReactiveInput<T> = T | Signal<T> | (() => T);
|
|
44
85
|
/** Shorthand for building query strings; `null`/`undefined` values are dropped. */
|
|
45
86
|
type QueryParamsInput = HttpParams | Record<string, string | number | boolean | undefined | null>;
|
|
46
87
|
/**
|
|
@@ -57,12 +98,25 @@ interface RiviniaRequestOptions {
|
|
|
57
98
|
unwrap?: boolean;
|
|
58
99
|
/** Envelope key for this call (overrides global `unwrapKey`). */
|
|
59
100
|
unwrapKey?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Opt-in backend-envelope validation for this call (replaces any global
|
|
103
|
+
* `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.
|
|
104
|
+
*/
|
|
105
|
+
envelope?: RiviniaEnvelopePolicy;
|
|
60
106
|
}
|
|
61
107
|
interface RiviniaDeleteOptions extends RiviniaRequestOptions {
|
|
62
108
|
body?: unknown;
|
|
63
109
|
}
|
|
64
110
|
/** Options for `getResource` in addition to per-request options. */
|
|
65
|
-
interface RiviniaResourceOptions<T> extends RiviniaRequestOptions {
|
|
111
|
+
interface RiviniaResourceOptions<T> extends Omit<RiviniaRequestOptions, 'params' | 'headers' | 'context'> {
|
|
112
|
+
/**
|
|
113
|
+
* Query params re-evaluated on every request: value, signal, or reactive
|
|
114
|
+
* function. Anything else in these options is read once per call site —
|
|
115
|
+
* only the URL, params, headers and context are reactive.
|
|
116
|
+
*/
|
|
117
|
+
params?: ReactiveInput<QueryParamsInput>;
|
|
118
|
+
headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;
|
|
119
|
+
context?: ReactiveInput<HttpContext>;
|
|
66
120
|
/** Value while idle/loading. Defaults to `undefined`. */
|
|
67
121
|
defaultValue?: T;
|
|
68
122
|
/**
|
|
@@ -82,10 +136,10 @@ interface RiviniaResourceOptions<T> extends RiviniaRequestOptions {
|
|
|
82
136
|
*
|
|
83
137
|
* Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches
|
|
84
138
|
* and `provideHttpClient()` testing all keep working). Owns no backend:
|
|
85
|
-
*
|
|
86
|
-
*
|
|
139
|
+
* URLs pass through untouched; the library adds opt-in envelope handling —
|
|
140
|
+
* unwrapping (`unwrap`/`unwrapKey`) and failure validation (`envelope`).
|
|
87
141
|
*
|
|
88
|
-
* Unwrap resolution per call: `options.unwrap ?? config.unwrap ??
|
|
142
|
+
* Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? false`,
|
|
89
143
|
* key: `options.unwrapKey ?? config.unwrapKey ?? 'data'`.
|
|
90
144
|
*/
|
|
91
145
|
export declare class RiviniaHttp {
|
|
@@ -98,15 +152,32 @@ export declare class RiviniaHttp {
|
|
|
98
152
|
delete<T>(endpoint: string, options?: RiviniaDeleteOptions): Observable<T>;
|
|
99
153
|
/** Binary download. Never unwrapped — envelopes don't apply to blobs. */
|
|
100
154
|
getBlob(endpoint: string, options?: RiviniaRequestOptions): Observable<Blob>;
|
|
155
|
+
/** Plain-text download. Never unwrapped. */
|
|
156
|
+
getText(endpoint: string, options?: RiviniaRequestOptions): Observable<string>;
|
|
157
|
+
/**
|
|
158
|
+
* Full response access (status, headers — e.g. pagination counts or a 201
|
|
159
|
+
* `Location`). Raw body (no unwrapping), but the configured/per-call
|
|
160
|
+
* `envelope` validation still applies to the body — a globally declared
|
|
161
|
+
* failure policy must hold on every JSON path, not just the unwrapped ones.
|
|
162
|
+
*/
|
|
163
|
+
getResponse<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<HttpResponse<T>>;
|
|
164
|
+
/**
|
|
165
|
+
* Full response access for creates (status, `Location` header). Same
|
|
166
|
+
* envelope-validation rule as `getResponse`; body otherwise raw.
|
|
167
|
+
*/
|
|
168
|
+
postResponse<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<HttpResponse<T>>;
|
|
101
169
|
/**
|
|
102
170
|
* Reactive GET as an `httpResource` (stable since Angular v22).
|
|
103
|
-
*
|
|
104
|
-
*
|
|
171
|
+
* The endpoint can be a static string, a signal, or a reactive function
|
|
172
|
+
* (same as `httpResource` itself) — it re-fetches on change, and
|
|
173
|
+
* `undefined` disables the request. Unwrap applies through `parse`,
|
|
174
|
+
* composed before a user `parse` if given.
|
|
105
175
|
*/
|
|
106
|
-
getResource<T>(endpoint: string | Signal<string | undefined
|
|
176
|
+
getResource<T>(endpoint: string | Signal<string | undefined> | (() => string | undefined), options?: RiviniaResourceOptions<T>): HttpResourceRef<T | undefined>;
|
|
107
177
|
private url;
|
|
108
178
|
private resolveUnwrap;
|
|
109
179
|
private resolveKey;
|
|
180
|
+
private resolveEnvelope;
|
|
110
181
|
private applyUnwrap;
|
|
111
182
|
private httpOptions;
|
|
112
183
|
private toParams;
|
|
@@ -121,7 +192,9 @@ export declare function joinUrl(baseUrl: string, endpoint: string): string;
|
|
|
121
192
|
/**
|
|
122
193
|
* Extracts `body[key]` when the backend wraps payloads in an envelope
|
|
123
194
|
* (e.g. `{ data, error, isSuccess }`); passes anything else through
|
|
124
|
-
* unchanged.
|
|
195
|
+
* unchanged. Unwraps EXACTLY ONE level: a payload that itself contains the
|
|
196
|
+
* key (`{ data: { data: […], page: 1 } }`) is returned whole — recursive
|
|
197
|
+
* unwrapping silently destroyed sibling fields (data-loss bug, fixed).
|
|
125
198
|
*/
|
|
126
199
|
export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
127
200
|
/**
|
|
@@ -146,4 +219,4 @@ export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
|
146
219
|
export declare function provideRiviniaHttp(config?: RiviniaHttpConfig): EnvironmentProviders;
|
|
147
220
|
/** Shorthand for `inject(RiviniaHttp)`. */
|
|
148
221
|
export declare function injectRiviniaHttp(): RiviniaHttp;
|
|
149
|
-
export type { QueryParamsInput, RiviniaDeleteOptions, RiviniaHttpConfig, RiviniaRequestOptions, RiviniaResourceOptions };
|
|
222
|
+
export type { QueryParamsInput, ReactiveInput, RiviniaDeleteOptions, RiviniaEnvelopePolicy, RiviniaHttpConfig, RiviniaRequestOptions, RiviniaResourceOptions };
|