@rivinia/http 0.5.3 → 1.0.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +9 -1
- package/fesm2022/rivinia-http.mjs +5 -5
- package/fesm2022/rivinia-http.mjs.map +1 -1
- package/package.json +13 -1
- package/types/rivinia-http.d.ts +1 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- First stable release. URL joining collapses repeated boundary slashes; request URLs otherwise remain consumer-controlled.
|
|
6
|
+
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
- Fix `joinUrl` so repeated leading and trailing slashes are collapsed to one boundary slash while query strings are preserved.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rivinia contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
4
4
|
[](https://www.npmjs.com/package/@rivinia/http)
|
|
5
5
|
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/http/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
6
10
|
`@rivinia/http` is a thin, typed Angular HTTP client for plain `Observable`
|
|
7
11
|
requests and reactive `httpResource` requests. It leaves URLs, backend
|
|
8
12
|
contracts, and interceptors under app control. Response bodies pass through
|
|
9
13
|
unchanged unless a request explicitly asks to unwrap one.
|
|
10
14
|
|
|
11
|
-
- Package version: **0.
|
|
15
|
+
- Package version: **1.0.0**
|
|
12
16
|
- Requires Angular `^22.0.0` and RxJS `^6.5.3 || ^7.4.0`; `tslib` is installed
|
|
13
17
|
as a dependency.
|
|
14
18
|
|
|
@@ -402,3 +406,7 @@ call sites. Without the per-request options, the response remains raw.
|
|
|
402
406
|
## License
|
|
403
407
|
|
|
404
408
|
MIT.
|
|
409
|
+
|
|
410
|
+
## Further reading
|
|
411
|
+
|
|
412
|
+
[Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/http/v1.0.0/docs/decisions/D026-consumer-driven-http-0-3-0-reactive-function-endpoints.md).
|
|
@@ -4,8 +4,8 @@ import { HttpClient, HttpContext, HttpParams, httpResource, provideHttpClient }
|
|
|
4
4
|
import { map } from "rxjs";
|
|
5
5
|
function joinUrl(baseUrl, endpoint) {
|
|
6
6
|
if (/^https?:\/\//i.test(endpoint)) return endpoint;
|
|
7
|
-
const base = baseUrl.
|
|
8
|
-
const path = endpoint.
|
|
7
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
8
|
+
const path = endpoint.replace(/^\/+/, "");
|
|
9
9
|
return path ? `${base}/${path}` : base;
|
|
10
10
|
}
|
|
11
11
|
var RiviniaHttpUnwrapError = class extends Error {
|
|
@@ -165,7 +165,7 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
165
165
|
}
|
|
166
166
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
167
167
|
minVersion: "12.0.0",
|
|
168
|
-
version: "22.2.
|
|
168
|
+
version: "22.2.3",
|
|
169
169
|
ngImport: i0,
|
|
170
170
|
type: RiviniaHttp,
|
|
171
171
|
deps: [],
|
|
@@ -173,14 +173,14 @@ var RiviniaHttp = class RiviniaHttp {
|
|
|
173
173
|
});
|
|
174
174
|
static ɵprov = i0.ɵɵngDeclareService({
|
|
175
175
|
minVersion: "22.0.0",
|
|
176
|
-
version: "22.2.
|
|
176
|
+
version: "22.2.3",
|
|
177
177
|
ngImport: i0,
|
|
178
178
|
type: RiviniaHttp
|
|
179
179
|
});
|
|
180
180
|
};
|
|
181
181
|
i0.ɵɵngDeclareClassMetadata({
|
|
182
182
|
minVersion: "12.0.0",
|
|
183
|
-
version: "22.2.
|
|
183
|
+
version: "22.2.3",
|
|
184
184
|
ngImport: i0,
|
|
185
185
|
type: RiviniaHttp,
|
|
186
186
|
decorators: [{ type: Service }]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/http-utils.ts","../../../projects/http/src/lib/http-envelope.ts","../../../projects/http/src/lib/http-config.ts","../../../projects/http/src/lib/rivinia-http.ts","../../../projects/http/src/lib/provide-http.ts"],"sourcesContent":["/**\n * Joins a base URL and an endpoint without producing double slashes.\n * Absolute `http(s)` endpoints pass through untouched.\n */\nexport function joinUrl(baseUrl: string, endpoint: string): string {\n if (/^https?:\\/\\//i.test(endpoint)) {\n return endpoint;\n }\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n const path = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from './http-options';\nimport { extractKey } from './http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from './http-config';\nimport { RiviniaHttp } from './rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rivinia-http.mjs","names":[],"sources":["../../../projects/http/src/lib/utils/http-utils.ts","../../../projects/http/src/lib/models/http-envelope.ts","../../../projects/http/src/lib/models/http-config.ts","../../../projects/http/src/lib/services/rivinia-http.ts","../../../projects/http/src/lib/providers/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.replace(/\\/+$/, '');\n const path = endpoint.replace(/^\\/+/, '');\n return path ? `${base}/${path}` : base;\n}\n\n/**\n * Typed extraction failure. Thrown when explicit unwrapping\n * (`{ unwrap: true, unwrapKey }`) cannot be performed: empty key, non-object\n * body, or missing property. Never thrown when unwrapping is disabled.\n * In `getResource` this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpUnwrapError extends Error {\n /** The requested envelope key. */\n readonly key: string;\n /** The endpoint being read, when known (never the body). */\n readonly endpoint: string | undefined;\n /**\n * The raw response body. Attached for diagnosis (a `parse` hook or logger\n * can inspect it) — it may carry PII, so never log it blindly and never\n * interpolate it into messages.\n */\n readonly body: unknown;\n\n constructor(key: string, body: unknown, endpoint?: string, reason?: string) {\n super(\n `[rivinia/http] Cannot unwrap key \"${key}\"` +\n (endpoint ? ` from ${endpoint}` : '') +\n (reason ? `: ${reason}` : ': property missing'),\n );\n this.name = 'RiviniaHttpUnwrapError';\n this.key = key;\n this.endpoint = endpoint;\n this.body = body;\n }\n}\n\n/**\n * Strict single-level extraction shared by every unwrapping path (plain\n * methods + `getResource`). Rules:\n * - the key must be a non-empty string (an empty key is never intentional);\n * - the body must be a non-null, non-array object;\n * - presence is an OWN-property check (`hasOwnProperty`) — inherited\n * properties (`'toString' in {}`) never count;\n * - a present-but-falsy value (`null`, `undefined`, `false`, `0`, `''`) is\n * delivered unchanged — truthiness never decides success;\n * - anything else throws `RiviniaHttpUnwrapError`. The original body is\n * never silently returned after an explicit extraction fails.\n *\n * Limit, stated openly: presence proves nothing about the value matching an\n * arbitrary generic `T`. Pair with `envelope` validation or a `parse` hook\n * (Zod/Valibot) when the backend is untyped.\n */\nexport function extractKey(body: unknown, key: string, endpoint?: string): unknown {\n if (typeof key !== 'string' || key.length === 0) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'invalid key');\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint, 'response is not an object');\n }\n const record = body as Record<string, unknown>;\n if (!Object.prototype.hasOwnProperty.call(record, key)) {\n throw new RiviniaHttpUnwrapError(key, body, endpoint);\n }\n return record[key];\n}\n\n/**\n * Extracts `body[key]` when the backend wraps payloads in an envelope\n * (e.g. `{ data, error, isSuccess }`). Strict: a missing key, a non-object\n * body, or an empty key throws `RiviniaHttpUnwrapError` — the original body\n * is never silently passed through after an explicit extraction request.\n * Unwraps EXACTLY ONE level: a payload that itself contains the key\n * (`{ data: { data: […], page: 1 } }`) is returned whole.\n */\nexport function unwrapAs<T>(body: unknown, key: string): T {\n return extractKey(body, key) as T;\n}\n","/**\n * Typed envelope failure. Thrown when a backend signals failure inside a\n * successful HTTP response (e.g. `{ data: null, isSuccess: false }` with\n * HTTP 200) and the envelope policy opts into throwing. In `getResource`\n * this surfaces as `resource.error()`.\n */\nexport class RiviniaHttpEnvelopeError extends Error {\n /** The raw response body. */\n readonly body: unknown;\n /** The machine-readable failure code (`body[errorKey]`), if any. */\n readonly code: unknown;\n\n constructor(body: unknown, code: unknown) {\n super(\n typeof code === 'string' && code.length > 0\n ? `[rivinia/http] Request failed: ${code}`\n : '[rivinia/http] Request failed (backend envelope)',\n );\n this.name = 'RiviniaHttpEnvelopeError';\n this.body = body;\n this.code = code;\n }\n}\n\n/**\n * Opt-in backend-envelope validation. When present (globally or per call),\n * a response object whose `successKey` is strictly `false` throws\n * `RiviniaHttpEnvelopeError` instead of flowing into `next()` as success.\n * Runs on the raw body before unwrapping, independent of `unwrap`.\n */\nexport interface RiviniaEnvelopePolicy {\n /** Body key holding the success flag. @default 'isSuccess' */\n readonly successKey?: string;\n /** Body key holding the failure code. @default 'error' */\n readonly errorKey?: string;\n /** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */\n readonly throwOnFailure?: boolean;\n}\n\nconst DEFAULT_SUCCESS_KEY = 'isSuccess';\nconst DEFAULT_ERROR_KEY = 'error';\n\n/** Throws when the envelope signals failure and the policy opts in. */\nexport function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void {\n if (policy.throwOnFailure === false) return;\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return;\n const successKey = policy.successKey ?? DEFAULT_SUCCESS_KEY;\n const record = body as Record<string, unknown>;\n if (!(successKey in record) || record[successKey] !== false) return;\n const errorKey = policy.errorKey ?? DEFAULT_ERROR_KEY;\n throw new RiviniaHttpEnvelopeError(body, record[errorKey]);\n}\n","import type { HttpFeature, HttpFeatureKind } from '@angular/common/http';\nimport { InjectionToken } from '@angular/core';\nimport type { RiviniaEnvelopePolicy } from './http-envelope';\n\n/**\n * Global configuration for `@rivinia/http`.\n *\n * Unwrapping is strictly per request (`{ unwrap: true, unwrapKey }` on the\n * call) — there is intentionally no global unwrap configuration. A global\n * key would silently change what every call returns; an explicit key keeps\n * each call honest. URLs are ALWAYS used as given — the library never joins,\n * prefixes, or rewrites them. Build them at the call site from wherever you\n * want (`environment.apiUrl`, a constant, a signal …) with the exported\n * `joinUrl`.\n */\nexport interface RiviniaHttpConfig {\n /**\n * Optional `HttpClient` features (`withInterceptors(...)`, `withXhr()`, …),\n * applied via a single internal `provideHttpClient(...httpFeatures)` call.\n *\n * Use this INSTEAD of calling `provideHttpClient()` yourself — calling both\n * registers the built-in XSRF interceptor twice (verified against Angular\n * 22.2.2 source: `xsrfInterceptorFn` is a multi-provider with no cross-call\n * dedup). Omit it entirely on Angular ≥21 when you need no features:\n * `HttpClient` (fetch backend) is injectable by default.\n */\n httpFeatures?: HttpFeature<HttpFeatureKind>[];\n /**\n * Opt-in backend-envelope validation applied to every call unless the call\n * provides its own `envelope` (which replaces this wholesale). See\n * `RiviniaEnvelopePolicy`. Off by default — plain bodies flow untouched.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaHttp()`.\n * @internal\n */\nexport const RIVINIA_HTTP_CONFIG = new InjectionToken<RiviniaHttpConfig>('RIVINIA_HTTP_CONFIG');\n","import {\n HttpClient,\n HttpContext,\n HttpParams,\n httpResource,\n type HttpHeaders,\n type HttpResourceRef,\n type HttpResponse,\n} from '@angular/common/http';\nimport { Service, computed, inject, isSignal, type Signal } from '@angular/core';\nimport { map, type Observable } from 'rxjs';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport type {\n QueryParamsInput,\n ReactiveInput,\n RiviniaDeleteRequestOptions,\n RiviniaRawOptions,\n RiviniaRequestBase,\n RiviniaRequestOptions,\n RiviniaUnwrapOff,\n RiviniaUnwrapOn,\n} from '../models/http-options';\nimport { extractKey } from '../utils/http-utils';\nimport { assertEnvelope, type RiviniaEnvelopePolicy } from '../models/http-envelope';\n\n/**\n * Reactive per-request fields for `getResource` (re-evaluated on every\n * request; everything else is read once per call site).\n */\nexport interface RiviniaResourceBase<T> {\n /**\n * Query params re-evaluated on every request: value, signal, or reactive\n * function. Anything else in these options is read once per call site —\n * only the URL, params, headers and context are reactive.\n */\n params?: ReactiveInput<QueryParamsInput>;\n headers?: ReactiveInput<HttpHeaders | Record<string, string | string[]>>;\n context?: ReactiveInput<HttpContext>;\n withCredentials?: boolean;\n /** Value while idle/loading. Defaults to `undefined`. */\n defaultValue?: T;\n /**\n * Runs AFTER envelope unwrapping: `raw -> unwrap -> parse`.\n * Use for runtime validation (e.g. Zod) or shaping.\n */\n parse?: (value: unknown) => T;\n /**\n * Forwarded to the `httpResource` request object only (enables the\n * resource `progress` signal for downloads). Has no effect on the\n * plain Observable methods — use `HttpClient` events there directly.\n */\n reportProgress?: boolean;\n /**\n * Opt-in backend-envelope validation for this resource (replaces any\n * global `envelope` policy wholesale). See `RiviniaEnvelopePolicy`.\n */\n envelope?: RiviniaEnvelopePolicy;\n}\n\n/**\n * `getResource` options: reactive base + the same per-call unwrap contract\n * as the plain methods (composed explicitly — `Omit` over a union would\n * destroy the discrimination).\n */\nexport type RiviniaResourceOptions<T> = RiviniaResourceBase<T> &\n (RiviniaUnwrapOn | RiviniaUnwrapOff);\n\n/**\n * Headless typed HTTP client.\n *\n * Thin over `HttpClient` (so app interceptors, `HttpContext` escape hatches\n * and `provideHttpClient()` testing all keep working). Owns no backend:\n * URLs pass through untouched; the library adds opt-in envelope handling —\n * per-request unwrapping (`{ unwrap: true, unwrapKey }`, mandatory key, no\n * default) and failure validation (`envelope`).\n */\n@Service()\nexport class RiviniaHttp {\n private readonly http = inject(HttpClient);\n private readonly config = inject<RiviniaHttpConfig>(RIVINIA_HTTP_CONFIG, {\n optional: true,\n });\n\n get<T>(endpoint: string, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .get<unknown>(this.url(endpoint), this.httpOptions(options))\n .pipe(map((body) => this.applyUnwrap<T>(endpoint, body, options)));\n }\n\n post<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .post<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n put<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .put<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n patch<T>(endpoint: string, body?: unknown, options?: RiviniaRequestOptions): Observable<T> {\n return this.http\n .patch<unknown>(this.url(endpoint), body, this.httpOptions(options))\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n delete<T>(endpoint: string, options?: RiviniaDeleteRequestOptions): Observable<T> {\n return this.http\n .delete<unknown>(this.url(endpoint), {\n ...this.httpOptions(options),\n body: options?.body,\n })\n .pipe(map((res) => this.applyUnwrap<T>(endpoint, res, options)));\n }\n\n /** Binary download. Never unwrapped — envelopes don't apply to blobs. */\n getBlob(endpoint: string, options?: RiviniaRawOptions): Observable<Blob> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'blob',\n });\n }\n\n /** Plain-text download. Never unwrapped. */\n getText(endpoint: string, options?: RiviniaRawOptions): Observable<string> {\n return this.http.get(this.url(endpoint), {\n ...this.httpOptions(options),\n responseType: 'text',\n });\n }\n\n /**\n * Full response access (status, headers — e.g. pagination counts or a 201\n * `Location`). Raw body (no unwrapping), but the configured/per-call\n * `envelope` validation still applies to the body — a globally declared\n * failure policy must hold on every JSON path, not just the unwrapped ones.\n */\n getResponse<T>(endpoint: string, options?: RiviniaRawOptions): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .get<T>(this.url(endpoint), {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Full response access for creates (status, `Location` header). Same\n * envelope-validation rule as `getResponse`; body otherwise raw.\n */\n postResponse<T>(\n endpoint: string,\n body?: unknown,\n options?: RiviniaRawOptions,\n ): Observable<HttpResponse<T>> {\n const envelope = this.resolveEnvelope(options);\n return this.http\n .post<T>(this.url(endpoint), body, {\n ...this.httpOptions(options),\n observe: 'response',\n })\n .pipe(\n map((res) => {\n if (envelope) assertEnvelope(res.body, envelope);\n return res;\n }),\n );\n }\n\n /**\n * Reactive GET as an `httpResource` (stable since Angular v22).\n * The endpoint can be a static string, a signal, or a reactive function\n * (same as `httpResource` itself) — it re-fetches on change, and\n * `undefined` disables the request. Unwrap applies through `parse`,\n * composed before a user `parse` if given.\n */\n getResource<T>(\n endpoint: string | Signal<string | undefined> | (() => string | undefined),\n options?: RiviniaResourceOptions<T>,\n ): HttpResourceRef<T | undefined> {\n const { defaultValue, parse, reportProgress, ...requestOptions } = options ?? {};\n const source: string | Signal<string | undefined> =\n typeof endpoint === 'function' && !isSignal(endpoint)\n ? computed(endpoint)\n : (endpoint as string | Signal<string | undefined>);\n const request = () => {\n const ep = typeof source === 'string' ? source : source();\n if (ep === undefined) return undefined;\n return {\n url: this.url(ep),\n params: this.toParams(resolveInput(requestOptions.params)),\n headers: resolveInput(requestOptions.headers),\n context: resolveInput(requestOptions.context),\n withCredentials: requestOptions.withCredentials,\n ...(reportProgress !== undefined ? { reportProgress } : {}),\n };\n };\n const envelope = this.resolveEnvelope(requestOptions);\n const base = { defaultValue: defaultValue as T | undefined };\n // Pipeline order: envelope validation (raw) -> unwrap -> user parse.\n // A throwing envelope or a failed extraction surfaces as resource.error().\n const applyParse = (raw: unknown): T => {\n if (envelope) assertEnvelope(raw, envelope);\n if (requestOptions.unwrap !== true) return parse ? parse(raw as T) : (raw as T);\n const unwrapped = extractKey(raw, requestOptions.unwrapKey) as T;\n return parse ? parse(unwrapped) : unwrapped;\n };\n return defaultValue === undefined\n ? httpResource<T>(request, { ...base, parse: applyParse })\n : httpResource<T>(request, {\n ...base,\n defaultValue: defaultValue as T,\n parse: applyParse,\n });\n }\n\n private url(endpoint: string): string {\n // Deliberate passthrough: the caller owns the URL (environment, constant,\n // signal …). Use the exported `joinUrl` at the call site when composing.\n return endpoint;\n }\n\n private resolveEnvelope(options?: {\n envelope?: RiviniaEnvelopePolicy;\n }): RiviniaEnvelopePolicy | undefined {\n return options?.envelope ?? this.config?.envelope;\n }\n\n private applyUnwrap<T>(endpoint: string, body: unknown, options?: RiviniaRequestOptions): T {\n const envelope = this.resolveEnvelope(options);\n if (envelope) assertEnvelope(body, envelope);\n if (options?.unwrap !== true) return body as T;\n return extractKey(body, options.unwrapKey, endpoint) as T;\n }\n\n private httpOptions(options?: RiviniaRequestBase) {\n return {\n params: this.toParams(options?.params),\n headers: options?.headers,\n context: options?.context ?? new HttpContext(),\n withCredentials: options?.withCredentials,\n };\n }\n\n private toParams(input?: QueryParamsInput): HttpParams | undefined {\n if (!input) return undefined;\n if (input instanceof HttpParams) return input;\n let params = new HttpParams();\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined && value !== null) {\n params = params.set(key, String(value));\n }\n }\n return params;\n }\n}\n\n/**\n * Evaluates a reactive input inside the request function: signals are read\n * (tracked), plain functions are called, values pass through.\n */\nfunction resolveInput<T>(value: ReactiveInput<T> | undefined): T | undefined {\n if (value === undefined) return undefined;\n if (isSignal(value)) return (value as Signal<T>)();\n if (typeof value === 'function') return (value as () => T)();\n return value as T;\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { provideHttpClient } from '@angular/common/http';\nimport { RIVINIA_HTTP_CONFIG, type RiviniaHttpConfig } from '../models/http-config';\nimport { RiviniaHttp } from '../services/rivinia-http';\n\n/**\n * Configures `@rivinia/http`.\n *\n * URLs are always used as given — build them at the call site from wherever\n * you want (see the exported `joinUrl` + your `environment.apiUrl`).\n *\n * Three backend styles (pick ONE):\n * - `provideRiviniaHttp()` alone — Angular ≥21 already injects `HttpClient` (fetch).\n * - `provideRiviniaHttp({ httpFeatures: [withInterceptors([...])] })` — single\n * call that also configures the backend. Do NOT additionally call\n * `provideHttpClient()` (double registration duplicates the XSRF interceptor).\n * - `provideHttpClient(...)` + `provideRiviniaHttp()` separately — equivalent,\n * keeps backend config visibly in app code (this is what `ng add` generates).\n *\n * @example\n * ```ts\n * provideRiviniaHttp()\n * ```\n */\nexport function provideRiviniaHttp(config: RiviniaHttpConfig = {}): EnvironmentProviders {\n const { httpFeatures, ...rest } = config;\n return makeEnvironmentProviders([\n ...(httpFeatures ? [provideHttpClient(...httpFeatures)] : []),\n { provide: RIVINIA_HTTP_CONFIG, useValue: rest },\n ]);\n}\n\n/** Shorthand for `inject(RiviniaHttp)`. */\nexport function injectRiviniaHttp(): RiviniaHttp {\n return inject(RiviniaHttp);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Headless typed HTTP client for Angular: envelope unwrapping, plain Observables and httpResource.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
},
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"schematics": "./schematics/collection.json",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/abdelfattahqandil21-oss/Revinia.git",
|
|
28
|
+
"directory": "projects/http"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/abdelfattahqandil21-oss/Revinia/tree/http/v1.0.0/projects/http#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/abdelfattahqandil21-oss/Revinia/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
36
|
+
},
|
|
25
37
|
"module": "fesm2022/rivinia-http.mjs",
|
|
26
38
|
"typings": "types/rivinia-http.d.ts",
|
|
27
39
|
"exports": {
|
package/types/rivinia-http.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpContext, HttpFeature, HttpFeatureKind, HttpHeaders, HttpParams, HttpResourceRef, HttpResponse } from "@angular/common/http";
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
|
-
import { EnvironmentProviders,
|
|
3
|
+
import { EnvironmentProviders, Signal } from "@angular/core";
|
|
4
4
|
import { Observable } from "rxjs";
|
|
5
5
|
/**
|
|
6
6
|
* Typed envelope failure. Thrown when a backend signals failure inside a
|
|
@@ -29,8 +29,6 @@ interface RiviniaEnvelopePolicy {
|
|
|
29
29
|
/** Throw on failure. Explicit `false` disables (policy becomes passive). @default true */
|
|
30
30
|
readonly throwOnFailure?: boolean;
|
|
31
31
|
}
|
|
32
|
-
/** Throws when the envelope signals failure and the policy opts in. */
|
|
33
|
-
declare function assertEnvelope(body: unknown, policy: RiviniaEnvelopePolicy): void;
|
|
34
32
|
/**
|
|
35
33
|
* Global configuration for `@rivinia/http`.
|
|
36
34
|
*
|
|
@@ -61,11 +59,6 @@ interface RiviniaHttpConfig {
|
|
|
61
59
|
*/
|
|
62
60
|
envelope?: RiviniaEnvelopePolicy;
|
|
63
61
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Internal configuration token. Consumers must use `provideRiviniaHttp()`.
|
|
66
|
-
* @internal
|
|
67
|
-
*/
|
|
68
|
-
declare const RIVINIA_HTTP_CONFIG: InjectionToken<RiviniaHttpConfig>;
|
|
69
62
|
/**
|
|
70
63
|
* A value, a signal of it, or a reactive function returning it — evaluated
|
|
71
64
|
* inside the `httpResource` request function, so signal reads stay reactive
|