@rivinia/http 0.0.0-stage → 0.2.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 +134 -2
- package/fesm2022/rivinia-http.mjs +148 -0
- package/fesm2022/rivinia-http.mjs.map +1 -0
- package/package.json +33 -3
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.cjs +132 -0
- package/schematics/ng-add/schema.json +30 -0
- package/types/rivinia-http.d.ts +149 -0
package/README.md
CHANGED
|
@@ -1,3 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rivinia/http
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@rivinia/http)
|
|
4
|
+
[](https://www.npmjs.com/package/@rivinia/http)
|
|
5
|
+
|
|
6
|
+
Headless typed HTTP client for Angular 22+. Configurable envelope
|
|
7
|
+
unwrapping, plain Observables **and** `httpResource` — URLs always used as
|
|
8
|
+
given, zero styling, zero backend opinions, interceptors left to your app.
|
|
9
|
+
|
|
10
|
+
- Version: **0.2.0**
|
|
11
|
+
- Requires: **Angular `^22.0.0`**, `rxjs` (same range Angular uses), `tslib`
|
|
12
|
+
- Since Angular 21 `HttpClient` (fetch backend) is injectable with zero setup.
|
|
13
|
+
Call `provideHttpClient()` / pass `httpFeatures` only when you configure
|
|
14
|
+
interceptors or other backend features.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @rivinia/http
|
|
20
|
+
ng add @rivinia/http # interactive: asks about unwrapping + key, then wires app.config.ts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start — pick ONE backend style
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// app.config.ts — style A: nothing to configure (Angular ≥21 default)
|
|
27
|
+
providers: [provideRiviniaHttp()];
|
|
28
|
+
|
|
29
|
+
// style B: single call with interceptors (do NOT also call provideHttpClient)
|
|
30
|
+
providers: [provideRiviniaHttp({ httpFeatures: [withInterceptors([authInterceptor])] })];
|
|
31
|
+
|
|
32
|
+
// style C: explicit backend config, like the official setup guide
|
|
33
|
+
providers: [provideHttpClient(withInterceptors([authInterceptor])), provideRiviniaHttp()];
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## URLs: yours, always
|
|
37
|
+
|
|
38
|
+
The library never joins, prefixes, or rewrites URLs. Compose them at the
|
|
39
|
+
call site from wherever you want — typically your environment files
|
|
40
|
+
(the BRT pattern):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/environments/environment.ts / environment.prod.ts
|
|
44
|
+
export const environment = { apiUrl: 'https://api.example.com' };
|
|
45
|
+
|
|
46
|
+
// feature service
|
|
47
|
+
import { joinUrl } from '@rivinia/http';
|
|
48
|
+
|
|
49
|
+
export class TripService {
|
|
50
|
+
private api = injectRiviniaHttp();
|
|
51
|
+
private url = (path: string) => joinUrl(environment.apiUrl, path);
|
|
52
|
+
|
|
53
|
+
list = () => this.api.get<Trip[]>(this.url('trips'));
|
|
54
|
+
create = (dto: CreateTrip) => this.api.post<Trip>(this.url('trips'), dto);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reactive resource (auto refetch, loading/error signals)
|
|
58
|
+
export class TripPage {
|
|
59
|
+
page = signal(1);
|
|
60
|
+
trips = this.api.getResource<Trip[]>(() => this.url(`trips?page=${this.page()}`));
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
@if (trips.hasValue()) {
|
|
66
|
+
@for (t of trips.value(); track t.id) { <p>{{ t.title }}</p> }
|
|
67
|
+
} @else if (trips.isLoading()) {
|
|
68
|
+
<p>Loading…</p>
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Envelope unwrapping (opt-in, default off)
|
|
73
|
+
|
|
74
|
+
The library assumes nothing about your backend: responses pass through raw
|
|
75
|
+
unless unwrapping is enabled. Backends that wrap payloads
|
|
76
|
+
(`{ data, error, isSuccess }`) opt in globally or per call:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
provideRiviniaHttp({ unwrap: true }); // whole app, 'data' key
|
|
80
|
+
provideRiviniaHttp({ unwrap: true, unwrapKey: 'result' }); // whole app, custom key
|
|
81
|
+
this.api.get<Legacy>(url, { unwrap: true, unwrapKey: 'result' }); // this call only
|
|
82
|
+
this.api.get<Raw>(url); // raw body (default)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
NOTE: a key alone never enables unwrapping — only `unwrap: true` does,
|
|
86
|
+
at either level. Different shapes per API in the same app:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export class MixedService {
|
|
90
|
+
private api = injectRiviniaHttp();
|
|
91
|
+
|
|
92
|
+
// main backend: { data: … } (global unwrap: true applies, nothing to say)
|
|
93
|
+
trips = () => this.api.get<Trip[]>(fullUrl('trips'));
|
|
94
|
+
|
|
95
|
+
// legacy backend: { result: … } (per-call key, unwrapping already on globally)
|
|
96
|
+
legacy = () => this.api.get<Old[]>(fullUrl('old'), { unwrapKey: 'result' });
|
|
97
|
+
|
|
98
|
+
// webhook endpoint: raw body despite the enabled global (per-call kill-switch)
|
|
99
|
+
ping = () => this.api.get<string>(fullUrl('ping'), { unwrap: false });
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`getResource` applies the same rules through `parse` (your own `parse` runs
|
|
104
|
+
after unwrapping — ideal for Zod/Valibot validation).
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
| Export | Kind | Purpose |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `provideRiviniaHttp(config?)` | provider | `{ unwrap?=false, unwrapKey?='data', httpFeatures? }` |
|
|
111
|
+
| `RiviniaHttp` | `@Service()` | `get/post/put/patch/delete`, `getBlob`, `getResource` |
|
|
112
|
+
| `injectRiviniaHttp()` | function | shorthand for `inject(RiviniaHttp)` |
|
|
113
|
+
| `joinUrl(base, endpoint)` | util | slash-safe join; absolute URLs pass through |
|
|
114
|
+
| `unwrapAs(body, key)` | util | recursive envelope extraction |
|
|
115
|
+
| types | types | `RiviniaHttpConfig`, `RiviniaRequestOptions`, `RiviniaDeleteOptions`, `RiviniaResourceOptions`, `QueryParamsInput` |
|
|
116
|
+
|
|
117
|
+
`getBlob` is never unwrapped. `HttpContext` passes through untouched, so
|
|
118
|
+
app interceptors (auth opt-out, error toasts) keep working. For caching,
|
|
119
|
+
polling, retry and TanStack Query interop see [RECIPES.md](./RECIPES.md).
|
|
120
|
+
|
|
121
|
+
## Testing your app code
|
|
122
|
+
|
|
123
|
+
Standard Angular tooling — nothing custom to learn:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
TestBed.configureTestingModule({
|
|
127
|
+
providers: [provideHttpClient(), provideHttpClientTesting(), provideRiviniaHttp()],
|
|
128
|
+
});
|
|
129
|
+
api.get('https://api.test/users').subscribe();
|
|
130
|
+
TestBed.inject(HttpTestingController).expectOne('https://api.test/users').flush({ data: [] });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { InjectionToken, Service, inject, makeEnvironmentProviders } from "@angular/core";
|
|
3
|
+
import { HttpClient, HttpContext, HttpParams, httpResource, provideHttpClient } from "@angular/common/http";
|
|
4
|
+
import { map } from "rxjs";
|
|
5
|
+
function joinUrl(baseUrl, endpoint) {
|
|
6
|
+
if (/^https?:\/\//i.test(endpoint)) return endpoint;
|
|
7
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
8
|
+
const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
|
|
9
|
+
return path ? `${base}/${path}` : base;
|
|
10
|
+
}
|
|
11
|
+
function unwrapAs(body, key) {
|
|
12
|
+
if (isObjectRecord(body) && hasOwn(body, key)) return unwrapAs(body[key], key);
|
|
13
|
+
return body;
|
|
14
|
+
}
|
|
15
|
+
function isObjectRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function hasOwn(obj, key) {
|
|
19
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
20
|
+
}
|
|
21
|
+
const RIVINIA_HTTP_CONFIG = new InjectionToken("RIVINIA_HTTP_CONFIG");
|
|
22
|
+
var RiviniaHttp = class RiviniaHttp {
|
|
23
|
+
http = inject(HttpClient);
|
|
24
|
+
config = inject(RIVINIA_HTTP_CONFIG, { optional: true });
|
|
25
|
+
get(endpoint, options) {
|
|
26
|
+
return this.http.get(this.url(endpoint), this.httpOptions(options)).pipe(map((body) => this.applyUnwrap(body, options)));
|
|
27
|
+
}
|
|
28
|
+
post(endpoint, body, options) {
|
|
29
|
+
return this.http.post(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
30
|
+
}
|
|
31
|
+
put(endpoint, body, options) {
|
|
32
|
+
return this.http.put(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
33
|
+
}
|
|
34
|
+
patch(endpoint, body, options) {
|
|
35
|
+
return this.http.patch(this.url(endpoint), body, this.httpOptions(options)).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
36
|
+
}
|
|
37
|
+
delete(endpoint, options) {
|
|
38
|
+
return this.http.delete(this.url(endpoint), {
|
|
39
|
+
...this.httpOptions(options),
|
|
40
|
+
body: options?.body
|
|
41
|
+
}).pipe(map((res) => this.applyUnwrap(res, options)));
|
|
42
|
+
}
|
|
43
|
+
getBlob(endpoint, options) {
|
|
44
|
+
return this.http.get(this.url(endpoint), {
|
|
45
|
+
...this.httpOptions(options),
|
|
46
|
+
responseType: "blob"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
getResource(endpoint, options) {
|
|
50
|
+
const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};
|
|
51
|
+
const request = () => {
|
|
52
|
+
const ep = typeof endpoint === "string" ? endpoint : endpoint();
|
|
53
|
+
if (ep === void 0) return void 0;
|
|
54
|
+
return {
|
|
55
|
+
url: this.url(ep),
|
|
56
|
+
params: this.toParams(requestOptions.params),
|
|
57
|
+
headers: requestOptions.headers,
|
|
58
|
+
context: requestOptions.context,
|
|
59
|
+
withCredentials: requestOptions.withCredentials,
|
|
60
|
+
...reportProgress !== void 0 ? { reportProgress } : {}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const unwrapKey = this.resolveKey(requestOptions);
|
|
64
|
+
const unwrapEnabled = this.resolveUnwrap(requestOptions);
|
|
65
|
+
const base = { defaultValue };
|
|
66
|
+
if (unwrapEnabled) {
|
|
67
|
+
const applyParse = (raw) => parse ? parse(unwrapAs(raw, unwrapKey)) : unwrapAs(raw, unwrapKey);
|
|
68
|
+
return defaultValue === void 0 ? httpResource(request, {
|
|
69
|
+
...base,
|
|
70
|
+
parse: applyParse
|
|
71
|
+
}) : httpResource(request, {
|
|
72
|
+
...base,
|
|
73
|
+
defaultValue,
|
|
74
|
+
parse: applyParse
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return defaultValue === void 0 ? httpResource(request, {
|
|
78
|
+
...base,
|
|
79
|
+
...parse ? { parse } : {}
|
|
80
|
+
}) : httpResource(request, {
|
|
81
|
+
...base,
|
|
82
|
+
defaultValue,
|
|
83
|
+
...parse ? { parse } : {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
url(endpoint) {
|
|
87
|
+
return endpoint;
|
|
88
|
+
}
|
|
89
|
+
resolveUnwrap(options) {
|
|
90
|
+
return options?.unwrap ?? this.config?.unwrap ?? false;
|
|
91
|
+
}
|
|
92
|
+
resolveKey(options) {
|
|
93
|
+
return options?.unwrapKey ?? this.config?.unwrapKey ?? "data";
|
|
94
|
+
}
|
|
95
|
+
applyUnwrap(body, options) {
|
|
96
|
+
if (!this.resolveUnwrap(options)) return body;
|
|
97
|
+
return unwrapAs(body, this.resolveKey(options));
|
|
98
|
+
}
|
|
99
|
+
httpOptions(options) {
|
|
100
|
+
return {
|
|
101
|
+
params: this.toParams(options?.params),
|
|
102
|
+
headers: options?.headers,
|
|
103
|
+
context: options?.context ?? new HttpContext(),
|
|
104
|
+
withCredentials: options?.withCredentials
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
toParams(input) {
|
|
108
|
+
if (!input) return void 0;
|
|
109
|
+
if (input instanceof HttpParams) return input;
|
|
110
|
+
let params = new HttpParams();
|
|
111
|
+
for (const [key, value] of Object.entries(input)) if (value !== void 0 && value !== null) params = params.set(key, String(value));
|
|
112
|
+
return params;
|
|
113
|
+
}
|
|
114
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
115
|
+
minVersion: "12.0.0",
|
|
116
|
+
version: "22.2.2",
|
|
117
|
+
ngImport: i0,
|
|
118
|
+
type: RiviniaHttp,
|
|
119
|
+
deps: [],
|
|
120
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
121
|
+
});
|
|
122
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
123
|
+
minVersion: "22.0.0",
|
|
124
|
+
version: "22.2.2",
|
|
125
|
+
ngImport: i0,
|
|
126
|
+
type: RiviniaHttp
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
130
|
+
minVersion: "12.0.0",
|
|
131
|
+
version: "22.2.2",
|
|
132
|
+
ngImport: i0,
|
|
133
|
+
type: RiviniaHttp,
|
|
134
|
+
decorators: [{ type: Service }]
|
|
135
|
+
});
|
|
136
|
+
function provideRiviniaHttp(config = {}) {
|
|
137
|
+
const { httpFeatures, ...rest } = config;
|
|
138
|
+
return makeEnvironmentProviders([...httpFeatures ? [provideHttpClient(...httpFeatures)] : [], {
|
|
139
|
+
provide: RIVINIA_HTTP_CONFIG,
|
|
140
|
+
useValue: rest
|
|
141
|
+
}]);
|
|
142
|
+
}
|
|
143
|
+
function injectRiviniaHttp() {
|
|
144
|
+
return inject(RiviniaHttp);
|
|
145
|
+
}
|
|
146
|
+
export { RiviniaHttp, injectRiviniaHttp, joinUrl, provideRiviniaHttp, unwrapAs };
|
|
147
|
+
|
|
148
|
+
//# sourceMappingURL=rivinia-http.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/http",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Headless typed HTTP client for Angular: envelope unwrapping, plain Observables and httpResource.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"http",
|
|
8
|
+
"http-client",
|
|
9
|
+
"signals",
|
|
10
|
+
"http-resource",
|
|
11
|
+
"typescript"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@angular/common": "^22.0.0",
|
|
16
|
+
"@angular/core": "^22.0.0",
|
|
17
|
+
"rxjs": "^6.5.3 || ^7.4.0"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"tslib": "^2.3.0"
|
|
21
|
+
},
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"schematics": "./schematics/collection.json",
|
|
24
|
+
"module": "fesm2022/rivinia-http.mjs",
|
|
25
|
+
"typings": "types/rivinia-http.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
"./package.json": {
|
|
28
|
+
"default": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./types/rivinia-http.d.ts",
|
|
32
|
+
"default": "./fesm2022/rivinia-http.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"type": "module"
|
|
6
36
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
|
|
3
|
+
"schematics": {
|
|
4
|
+
"ng-add": {
|
|
5
|
+
"description": "Add provideHttpClient() and provideRiviniaHttp() to the application config.",
|
|
6
|
+
"factory": "./ng-add/index.cjs#ngAdd",
|
|
7
|
+
"schema": "./ng-add/schema.json"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ngAdd = ngAdd;
|
|
4
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
5
|
+
const HTTP_CLIENT_IMPORT = `import { provideHttpClient } from '@angular/common/http';`;
|
|
6
|
+
const PROVIDE_HTTP_IMPORT = `import { provideRiviniaHttp } from '@rivinia/http';`;
|
|
7
|
+
/**
|
|
8
|
+
* `ng add @rivinia/http` — wires the library into a standalone app config.
|
|
9
|
+
*
|
|
10
|
+
* - Never overwrites user files; only inserts two imports + two providers.
|
|
11
|
+
* - Idempotent: safe to run twice (existing entries are detected and skipped).
|
|
12
|
+
* - Never provides the backend beyond `provideHttpClient()`; interceptors,
|
|
13
|
+
* URLs and unwrap options stay app-owned (see schema options).
|
|
14
|
+
*/
|
|
15
|
+
function ngAdd(options) {
|
|
16
|
+
return (tree, context) => {
|
|
17
|
+
let configPath;
|
|
18
|
+
try {
|
|
19
|
+
configPath = findAppConfig(tree, options.project);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
context.logger.warn(`[rivinia/http] workspace lookup failed (${error instanceof Error ? error.message : error}) — skipping. ` +
|
|
23
|
+
`Add provideHttpClient() and provideRiviniaHttp() manually (see README).`);
|
|
24
|
+
return tree;
|
|
25
|
+
}
|
|
26
|
+
if (!configPath || !tree.exists(configPath)) {
|
|
27
|
+
context.logger.warn(`[rivinia/http] app.config.ts not found — skipping. ` +
|
|
28
|
+
`Add provideHttpClient() and provideRiviniaHttp() manually (see README).`);
|
|
29
|
+
return tree;
|
|
30
|
+
}
|
|
31
|
+
let content = tree.read(configPath)?.toString('utf-8') ?? '';
|
|
32
|
+
if (!content) {
|
|
33
|
+
throw new schematics_1.SchematicsException(`[rivinia/http] ${configPath} is empty.`);
|
|
34
|
+
}
|
|
35
|
+
// Deterministic mapping, no ambiguity (library default: disabled):
|
|
36
|
+
// - unwrap !== true → emit nothing about unwrapping (raw passthrough).
|
|
37
|
+
// A custom key given alongside is ignored, said plainly in logs.
|
|
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(', ')} }` : '';
|
|
55
|
+
let changed = false;
|
|
56
|
+
if (!content.includes(HTTP_CLIENT_IMPORT)) {
|
|
57
|
+
content = insertAfterLastImport(content, HTTP_CLIENT_IMPORT);
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (!content.includes(PROVIDE_HTTP_IMPORT)) {
|
|
61
|
+
content = insertAfterLastImport(content, PROVIDE_HTTP_IMPORT);
|
|
62
|
+
changed = true;
|
|
63
|
+
}
|
|
64
|
+
if (!content.includes('provideHttpClient(')) {
|
|
65
|
+
content = insertProvider(content, 'provideHttpClient()');
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
context.logger.info('[rivinia/http] provideHttpClient() already present — skipped.');
|
|
70
|
+
}
|
|
71
|
+
const provideCall = `provideRiviniaHttp(${provideArg})`;
|
|
72
|
+
if (!content.includes('provideRiviniaHttp(')) {
|
|
73
|
+
content = insertProvider(content, provideCall);
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
context.logger.info('[rivinia/http] provideRiviniaHttp() already present — skipped.');
|
|
78
|
+
}
|
|
79
|
+
if (changed) {
|
|
80
|
+
tree.overwrite(configPath, content);
|
|
81
|
+
context.logger.info(`[rivinia/http] wired into ${configPath}.`);
|
|
82
|
+
}
|
|
83
|
+
return tree;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function defaultProjectName(tree, requested) {
|
|
87
|
+
if (requested)
|
|
88
|
+
return requested;
|
|
89
|
+
const angularJson = tree.read('/angular.json')?.toString('utf-8');
|
|
90
|
+
if (!angularJson) {
|
|
91
|
+
throw new schematics_1.SchematicsException('[rivinia/http] angular.json not found.');
|
|
92
|
+
}
|
|
93
|
+
const workspace = JSON.parse(angularJson);
|
|
94
|
+
if (workspace.defaultProject && typeof workspace.defaultProject === 'string') {
|
|
95
|
+
return workspace.defaultProject;
|
|
96
|
+
}
|
|
97
|
+
const names = Object.keys(workspace.projects ?? {});
|
|
98
|
+
if (names.length === 0) {
|
|
99
|
+
throw new schematics_1.SchematicsException('[rivinia/http] no projects in angular.json.');
|
|
100
|
+
}
|
|
101
|
+
return names[0];
|
|
102
|
+
}
|
|
103
|
+
function findAppConfig(tree, requestedProject) {
|
|
104
|
+
const angularJson = tree.read('/angular.json')?.toString('utf-8');
|
|
105
|
+
if (!angularJson)
|
|
106
|
+
return null;
|
|
107
|
+
const workspace = JSON.parse(angularJson);
|
|
108
|
+
const name = defaultProjectName(tree, requestedProject);
|
|
109
|
+
const project = workspace.projects?.[name];
|
|
110
|
+
if (!project)
|
|
111
|
+
return null;
|
|
112
|
+
const sourceRoot = project.sourceRoot ?? (project.root ? `${project.root}/src` : 'src');
|
|
113
|
+
return `/${sourceRoot}/app/app.config.ts`;
|
|
114
|
+
}
|
|
115
|
+
function insertAfterLastImport(content, line) {
|
|
116
|
+
const lines = content.split('\n');
|
|
117
|
+
let idx = -1;
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
if (/^\s*import\s/.test(lines[i]))
|
|
120
|
+
idx = i;
|
|
121
|
+
}
|
|
122
|
+
lines.splice(idx + 1, 0, line);
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
function insertProvider(content, call) {
|
|
126
|
+
const match = /providers\s*:\s*\[/.exec(content);
|
|
127
|
+
if (!match) {
|
|
128
|
+
throw new schematics_1.SchematicsException('[rivinia/http] no `providers: [` array found in app.config.ts — add providers manually.');
|
|
129
|
+
}
|
|
130
|
+
const at = match.index + match[0].length;
|
|
131
|
+
return `${content.slice(0, at)}\n ${call},${content.slice(at)}`;
|
|
132
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "rivinia-http-ng-add",
|
|
4
|
+
"title": "Rivinia HTTP ng-add options",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"project": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Angular CLI project name — only for multi-app workspaces. Defaults to the workspace default project. Unrelated to unwrap scope.",
|
|
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
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef } from "@angular/common/http";
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
import { EnvironmentProviders, InjectionToken, Signal } from "@angular/core";
|
|
4
|
+
import { Observable } from "rxjs";
|
|
5
|
+
/**
|
|
6
|
+
* Global configuration for `@rivinia/http`.
|
|
7
|
+
*
|
|
8
|
+
* Every field is optional and can be overridden per request.
|
|
9
|
+
* URLs are ALWAYS used as given — the library never joins, prefixes, or
|
|
10
|
+
* rewrites them. Build them at the call site from wherever you want
|
|
11
|
+
* (`environment.apiUrl`, a constant, a signal …) with the exported `joinUrl`.
|
|
12
|
+
*/
|
|
13
|
+
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
|
+
/**
|
|
26
|
+
* Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),
|
|
27
|
+
* applied via a single internal `provideHttpClient(...httpFeatures)` call.
|
|
28
|
+
*
|
|
29
|
+
* Use this INSTEAD of calling `provideHttpClient()` yourself — calling both
|
|
30
|
+
* registers the built-in XSRF interceptor twice (verified against Angular
|
|
31
|
+
* 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call
|
|
32
|
+
* dedup). Omit it entirely on Angular ≥21 when you need no features:
|
|
33
|
+
* `HttpClient` (fetch backend) is injectable by default.
|
|
34
|
+
*/
|
|
35
|
+
httpFeatures?: HttpFeature<HttpFeatureKind>[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Internal configuration token. Consumers must use `provideRiviniaHttp()`.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
42
|
+
declare const DEFAULT_UNWRAP = false;
|
|
43
|
+
declare const DEFAULT_UNWRAP_KEY = "data";
|
|
44
|
+
/** Shorthand for building query strings; `null`/`undefined` values are dropped. */
|
|
45
|
+
type QueryParamsInput = HttpParams | Record<string, string | number | boolean | undefined | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Per-request options. `unwrap` / `unwrapKey` override the global
|
|
48
|
+
* `provideRiviniaHttp()` configuration for this call only.
|
|
49
|
+
*/
|
|
50
|
+
interface RiviniaRequestOptions {
|
|
51
|
+
params?: QueryParamsInput;
|
|
52
|
+
headers?: HttpHeaders | Record<string, string | string[]>;
|
|
53
|
+
/** Pass-through for app-owned interceptors (auth opt-out, error policy, ...). */
|
|
54
|
+
context?: HttpContext;
|
|
55
|
+
withCredentials?: boolean;
|
|
56
|
+
/** Enable envelope unwrapping for this call (overrides global `unwrap`). */
|
|
57
|
+
unwrap?: boolean;
|
|
58
|
+
/** Envelope key for this call (overrides global `unwrapKey`). */
|
|
59
|
+
unwrapKey?: string;
|
|
60
|
+
}
|
|
61
|
+
interface RiviniaDeleteOptions extends RiviniaRequestOptions {
|
|
62
|
+
body?: unknown;
|
|
63
|
+
}
|
|
64
|
+
/** Options for `getResource` in addition to per-request options. */
|
|
65
|
+
interface RiviniaResourceOptions<T> extends RiviniaRequestOptions {
|
|
66
|
+
/** Value while idle/loading. Defaults to `undefined`. */
|
|
67
|
+
defaultValue?: T;
|
|
68
|
+
/**
|
|
69
|
+
* Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.
|
|
70
|
+
* Use for runtime validation (e.g. Zod) or shaping.
|
|
71
|
+
*/
|
|
72
|
+
parse?: (value: unknown) => T;
|
|
73
|
+
/**
|
|
74
|
+
* Forwarded to the `httpResource` request object only (enables the
|
|
75
|
+
* resource `progress` signal for downloads). Has no effect on the
|
|
76
|
+
* plain Observable methods — use `HttpClient` events there directly.
|
|
77
|
+
*/
|
|
78
|
+
reportProgress?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Headless typed HTTP client.
|
|
82
|
+
*
|
|
83
|
+
* Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches
|
|
84
|
+
* and `provideHttpClient()` testing all keep working). Owns no backend:
|
|
85
|
+
* the app provides `HttpClient`, this library only adds base-URL joining
|
|
86
|
+
* plus configurable envelope unwrapping.
|
|
87
|
+
*
|
|
88
|
+
* Unwrap resolution per call: `options.unwrap ?? config.unwrap ?? true`,
|
|
89
|
+
* key: `options.unwrapKey ?? config.unwrapKey ?? 'data'`.
|
|
90
|
+
*/
|
|
91
|
+
export declare class RiviniaHttp {
|
|
92
|
+
private readonly http;
|
|
93
|
+
private readonly config;
|
|
94
|
+
get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T>;
|
|
95
|
+
post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
96
|
+
put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
97
|
+
patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T>;
|
|
98
|
+
delete<T>(endpoint: string, options?: RiviniaDeleteOptions): Observable<T>;
|
|
99
|
+
/** Binary download. Never unwrapped — envelopes don't apply to blobs. */
|
|
100
|
+
getBlob(endpoint: string, options?: RiviniaRequestOptions): Observable<Blob>;
|
|
101
|
+
/**
|
|
102
|
+
* Reactive GET as an `httpResource` (stable since Angular v22).
|
|
103
|
+
* Re-fetches when the endpoint signal changes; `undefined` disables.
|
|
104
|
+
* Unwrap applies through `parse`, composed before a user `parse` if given.
|
|
105
|
+
*/
|
|
106
|
+
getResource<T>(endpoint: string | Signal<string | undefined>, options?: RiviniaResourceOptions<T>): HttpResourceRef<T | undefined>;
|
|
107
|
+
private url;
|
|
108
|
+
private resolveUnwrap;
|
|
109
|
+
private resolveKey;
|
|
110
|
+
private applyUnwrap;
|
|
111
|
+
private httpOptions;
|
|
112
|
+
private toParams;
|
|
113
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaHttp, never>;
|
|
114
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Joins a base URL and an endpoint without producing double slashes.
|
|
118
|
+
* Absolute `http(s)` endpoints pass through untouched.
|
|
119
|
+
*/
|
|
120
|
+
export declare function joinUrl(baseUrl: string, endpoint: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Extracts `body[key]` when the backend wraps payloads in an envelope
|
|
123
|
+
* (e.g. `{ data, error, isSuccess }`); passes anything else through
|
|
124
|
+
* unchanged. Recursive, so nested envelopes collapse.
|
|
125
|
+
*/
|
|
126
|
+
export declare function unwrapAs<T>(body: unknown, key: string): T;
|
|
127
|
+
/**
|
|
128
|
+
* Configures `@rivinia/http`.
|
|
129
|
+
*
|
|
130
|
+
* URLs are always used as given — build them at the call site from wherever
|
|
131
|
+
* you want (see the exported `joinUrl` + your `environment.apiUrl`).
|
|
132
|
+
*
|
|
133
|
+
* Three backend styles (pick ONE):
|
|
134
|
+
* - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).
|
|
135
|
+
* - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single
|
|
136
|
+
* call that also configures the backend. Do NOT additionally call
|
|
137
|
+
* `provideHttpClient()` (double registration duplicates the XSRF interceptor).
|
|
138
|
+
* - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,
|
|
139
|
+
* keeps backend config visibly in app code (this is what `ng add` generates).
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* provideRiviniaHttp({ unwrapKey: 'data' })
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export declare function provideRiviniaHttp(config?: RiviniaHttpConfig): EnvironmentProviders;
|
|
147
|
+
/** Shorthand for `inject(RiviniaHttp)`. */
|
|
148
|
+
export declare function injectRiviniaHttp(): RiviniaHttp;
|
|
149
|
+
export type { QueryParamsInput, RiviniaDeleteOptions, RiviniaHttpConfig, RiviniaRequestOptions, RiviniaResourceOptions };
|