@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 CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # @rivinia/http
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@rivinia/http.svg)](https://www.npmjs.com/package/@rivinia/http)
4
+ [![license](https://img.shields.io/npm/l/@rivinia/http.svg)](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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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 };