@dudousxd/nestjs-inertia-client 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 ADDED
@@ -0,0 +1,72 @@
1
+ # Changelog — @dudousxd/nestjs-inertia-client
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c5878e3`](https://github.com/DavideCarvalho/nestjs-inertia/commit/c5878e3f8827d9e89710df0154ea76996b6db62a) - First public release — Inertia.js v3 adapter for NestJS.
8
+
9
+ - Core: InertiaModule.forRoot/forRootAsync/forFeature, @Inertia decorator, Inertia.optional/defer/merge/always markers, CSRF with tokenContext, SSR support, Express + Fastify adapters
10
+ - Vite: setupInertiaVite + nestInertia plugin, @inertia/@vite/@inertiaHead shell directives
11
+ - Codegen: nestjs-inertia init (full scaffold + auto-patch), auto-watch in dev, static AST discovery, class-validator DTO support, Route/Path type helpers, @As hierarchical naming
12
+ - Client: defineContract + @ApplyContract, typed Link for React/Vue/Svelte with context providers, createFetcher, SSR hydration, rich error messages
13
+ - Testing: expectInertia matchers, assertInertia, InertiaTestingModule, fakes
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`c5878e3`](https://github.com/DavideCarvalho/nestjs-inertia/commit/c5878e3f8827d9e89710df0154ea76996b6db62a)]:
18
+ - @dudousxd/nestjs-inertia@1.0.0
19
+
20
+ For the full repository changelog see [`../../CHANGELOG.md`](../../CHANGELOG.md).
21
+
22
+ ## 0.9.0-alpha.0 — 2026-05-22
23
+
24
+ ### Changed
25
+
26
+ - Broadened peer deps: `@inertiajs/react ^2||^3`, `@inertiajs/vue3 ^2||^3`, `react ^18||^19`
27
+ - Svelte `Link` component migrated to Svelte 5 Runes API (`$props` / `$derived` / children snippet)
28
+ - `Link.svelte` is now copied to `dist/svelte/` as part of the build
29
+ - Version bump to `0.9.0-alpha.0` (Inertia v3 monorepo coordination)
30
+
31
+ ## 0.8.0-alpha.0 — 2026-05-22
32
+
33
+ ### Added
34
+
35
+ - **Typed `<Link>` for React** (`/react` subpath) — `<Link route="..." routeParams={{...}}>` with full TypeScript autocompletion; `routeParams` is omitted when the route has no dynamic segments
36
+ - **Typed `<Link>` for Vue 3** (`/vue` subpath) — same typed API via a Vue 3 component; wraps `@inertiajs/vue3`'s `Link`
37
+ - **Typed `<Link>` for Svelte** (`/svelte` subpath) — same typed API as a Svelte 5 component; wraps `@inertiajs/svelte`'s `Link`
38
+ - **`setRouteResolver(fn)`** — boot-time helper to wire the codegen-emitted `route()` function into all typed `Link` components; call once in your entry file
39
+ - **`RegistryRoutes` consumption** — `Link` components read typed route names and params from the `InertiaRegistry` augmentation emitted by codegen `init`
40
+
41
+ ### Changed
42
+
43
+ - Version bump to `0.8.0-alpha.0`
44
+
45
+ ## 0.7.0-alpha.0 — 2026-05-22
46
+
47
+ ### Changed
48
+
49
+ - Bundled with example app, CI workflows, Changesets, MIT LICENSE, and slim docs.
50
+
51
+ ## [0.6.0-alpha.0] - 2026-05-22
52
+
53
+ ### Added
54
+
55
+ - **Initial release** of `@dudousxd/nestjs-inertia-client`
56
+ - **`Contract` builders** — `Contract.get`, `.post`, `.put`, `.patch`, `.delete`; each accepts a URL path and a definition with optional `query` / `body` and required `response` Zod schemas, returning a typed `ContractDef`
57
+ - **`@ApplyContract(contractDef)`** — NestJS method decorator that stores the contract under `CONTRACT_METADATA` (`Reflect` metadata) on the handler; enables codegen contract discovery and `api.ts` emission
58
+ - **`CONTRACT_METADATA` symbol** + **`getContract(target, key)`** helper for reading contract metadata
59
+ - **`createFetcher(opts?): Fetcher`** — thin `fetch` wrapper
60
+ - `buildUrl` path-param interpolation (`:param` → value) + `URLSearchParams` query-string serialization
61
+ - JSON body encoding (`Content-Type: application/json`) and `FormData` passthrough (no `Content-Type` override)
62
+ - `Accept: application/json` default header
63
+ - `ApiHttpError` thrown on non-2xx responses, with static `fromResponse(res)` async factory
64
+ - `onError` hook called before re-throwing
65
+ - Pluggable `fetch` implementation via `opts.fetch` (useful in tests and SSR)
66
+ - HTTP 204 → returns `undefined`
67
+ - **`ApiHttpError`** — error class with `.status: number`, `.body: unknown`, and `.response: Response`
68
+ - **`invalidate(queryClient, queryKey)`** — convenience wrapper around `queryClient.invalidateQueries`
69
+ - **SSR hydration** (`./ssr` subpath export)
70
+ - `hydrateClientFromInertia(page)` — creates a `QueryClient` pre-seeded from `page.props._initialQueries`
71
+ - `seedInitialQueries(qc)` — serialises the full `QueryClient` cache into the `_initialQueries` array for Inertia shared props
72
+ - **Full Vitest test suite** — 47 tests covering all exports
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Davide Carvalho
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 ADDED
@@ -0,0 +1,274 @@
1
+ # @dudousxd/nestjs-inertia-client
2
+
3
+ Tuyau-style typed HTTP client for `@dudousxd/nestjs-inertia`, built on TanStack Query v5 core.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@dudousxd/nestjs-inertia-client)](https://www.npmjs.com/package/@dudousxd/nestjs-inertia-client)
6
+
7
+ > Alpha — in active development. API may change before 1.0.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @dudousxd/nestjs-inertia-client @tanstack/query-core zod
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Define a Contract
18
+
19
+ ```ts
20
+ import { defineContract } from '@dudousxd/nestjs-inertia-client';
21
+ import { z } from 'zod';
22
+
23
+ export const listUsersContract = defineContract({
24
+ query: z.object({ page: z.number().optional() }),
25
+ response: z.array(z.object({ id: z.string(), name: z.string() })),
26
+ });
27
+
28
+ export const createUserContract = defineContract({
29
+ body: z.object({ name: z.string(), email: z.string().email() }),
30
+ response: z.object({ id: z.string(), name: z.string() }),
31
+ });
32
+ ```
33
+
34
+ Contracts carry **no** `name`, `method`, or `path` — these are all routing concerns handled by NestJS decorators and codegen.
35
+
36
+ ### 2. How the API name is derived
37
+
38
+ The API name is composed from a **class portion** and a **method portion**, joined with a dot:
39
+
40
+ - **Class portion**: class-level `@As(...)` value if present, otherwise the class name with `Controller` stripped and first letter lowercased.
41
+ - **Method portion**: method-level `@As(...)` value if present, otherwise the method name.
42
+ - **Final name**: `${classPortion}.${methodPortion}`
43
+
44
+ | Class-level `@As` | Method-level `@As` | Derived API name |
45
+ |---|---|---|
46
+ | absent | absent | `<classNameStripped>.<methodName>` (default) |
47
+ | `@As('users')` | absent | `users.<methodName>` |
48
+ | absent | `@As('directory')` | `<classNameStripped>.directory` |
49
+ | `@As('users')` | `@As('directory')` | `users.directory` |
50
+ | `@As('users.admin')` | `@As('list')` | `users.admin.list` |
51
+
52
+ Examples:
53
+
54
+ | Controller class | Method | Derived API name |
55
+ |------------------|--------|-----------------|
56
+ | `UsersController` | `list` | `users.list` → `api.users.list` |
57
+ | `UsersController` | `create` | `users.create` → `api.users.create` |
58
+ | `AdminUsersController` | `list` | `adminUsers.list` → `api.adminUsers.list` |
59
+
60
+ To override, use `@As` at the class level, method level, or both:
61
+
62
+ ```ts
63
+ import { As } from '@dudousxd/nestjs-inertia-client';
64
+
65
+ // Class-level @As sets the class portion for all methods
66
+ @Controller('/api/users')
67
+ @As('users')
68
+ class UsersController {
69
+ @Get()
70
+ @ApplyContract(listUsersContract)
71
+ list() { ... } // → 'users.list'
72
+
73
+ @Get('/top')
74
+ @ApplyContract(listUsersContract)
75
+ @As('directory') // → 'users.directory'
76
+ listDirectory() { ... }
77
+ }
78
+ ```
79
+
80
+ ### 3. Bind a Contract to a NestJS Handler with `@ApplyContract`
81
+
82
+ ```ts
83
+ import { Controller, Get, Post } from '@nestjs/common';
84
+ import { ApplyContract } from '@dudousxd/nestjs-inertia-client';
85
+ import { listUsersContract, createUserContract } from './contracts.js';
86
+
87
+ @Controller()
88
+ export class UserController {
89
+ @Get('/users')
90
+ @ApplyContract(listUsersContract)
91
+ listUsers() { /* ... */ }
92
+
93
+ @Post('/users')
94
+ @ApplyContract(createUserContract)
95
+ createUser() { /* ... */ }
96
+ }
97
+ ```
98
+
99
+ `@ApplyContract` only attaches the contract metadata (`CONTRACT_METADATA`) — it does **not** set the NestJS routing path or HTTP method. Always pair it with a NestJS HTTP verb decorator (`@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`). `@dudousxd/nestjs-inertia-codegen` reads both the verb decorator and the contract to emit a typed `api.ts`.
100
+
101
+ ### Alternative: class-validator DTOs (no `defineContract` needed)
102
+
103
+ If you already use class-validator DTOs and `@nestjs/swagger`, the codegen reads types automatically — no `defineContract` required:
104
+
105
+ ```ts
106
+ import { Body, Controller, Get, Post, Query } from '@nestjs/common';
107
+ import { ApiResponse } from '@nestjs/swagger';
108
+
109
+ class ListPostsQuery { page?: number; }
110
+ class PostDto { id: string; title: string; }
111
+ class CreatePostBody { title: string; content: string; }
112
+
113
+ @Controller('/api/posts')
114
+ export class PostsController {
115
+ @Get()
116
+ @ApiResponse({ type: [PostDto] })
117
+ list(@Query() query: ListPostsQuery): Promise<PostDto[]> { ... }
118
+
119
+ @Post()
120
+ @ApiResponse({ type: PostDto })
121
+ create(@Body() body: CreatePostBody): Promise<PostDto> { ... }
122
+ }
123
+ ```
124
+
125
+ The codegen extracts `@Body()` → body type, `@Query()` → query type, `@ApiResponse({ type })` → response type, and return type annotation as a fallback. When `@ApplyContract` is present, Zod schemas take full priority and DTO extraction is skipped for that method.
126
+
127
+ ### 4. Create a Fetcher and Call Endpoints
128
+
129
+ ```ts
130
+ import { createFetcher } from '@dudousxd/nestjs-inertia-client';
131
+
132
+ const fetcher = createFetcher({
133
+ baseUrl: 'http://localhost:3000',
134
+ headers: () => ({
135
+ Authorization: `Bearer ${getToken()}`,
136
+ }),
137
+ });
138
+
139
+ // GET /users?page=1
140
+ const users = await fetcher.get<User[]>('/users', { query: { page: 1 } });
141
+
142
+ // POST /users
143
+ const newUser = await fetcher.post<User>('/users', {
144
+ body: { name: 'Alice', email: 'alice@example.com' },
145
+ });
146
+ ```
147
+
148
+ The generated `api.ts` (emitted by `nestjs-inertia codegen`) wraps `createFetcher` with full request/response types derived from your contracts.
149
+
150
+ ### 5. Handle Errors
151
+
152
+ ```ts
153
+ import { ApiHttpError } from '@dudousxd/nestjs-inertia-client';
154
+
155
+ try {
156
+ await fetcher.post('/users', { body: { name: '' } });
157
+ } catch (err) {
158
+ if (err instanceof ApiHttpError) {
159
+ console.error(err.status, err.body);
160
+ }
161
+ }
162
+ ```
163
+
164
+ ## Type helpers (generated `api.ts`)
165
+
166
+ The generated `api.ts` exports `Route.*` and `Path.*` namespaces for compile-time access to request/response shapes:
167
+
168
+ ```ts
169
+ import type { Route, Path } from '.nestjs-inertia/api.js';
170
+
171
+ // by contract name
172
+ type UserList = Route.Response<'users.list'>;
173
+ type CreateReq = Route.Request<'users.create'>;
174
+ // → { body: ...; query: ...; params: ... }
175
+
176
+ // by HTTP method + URL
177
+ type ListResp = Path.Response<'GET', '/api/users'>;
178
+ type CreateBody = Path.Body<'POST', '/api/users'>;
179
+ ```
180
+
181
+ Use `Route.*` and `Path.*` — they are the canonical type helpers.
182
+
183
+ ## SSR Hydration
184
+
185
+ Import SSR helpers from the `/ssr` subpath:
186
+
187
+ ```ts
188
+ import {
189
+ hydrateClientFromInertia,
190
+ seedInitialQueries,
191
+ } from '@dudousxd/nestjs-inertia-client/ssr';
192
+ import { QueryClient } from '@tanstack/query-core';
193
+ ```
194
+
195
+ ### Server side (NestJS)
196
+
197
+ ```ts
198
+ // In your Inertia controller, seed the QueryClient and attach its cache to shared props
199
+ const qc = new QueryClient();
200
+ await qc.prefetchQuery({ queryKey: ['users'], queryFn: fetchUsers });
201
+
202
+ return inertia.render('Dashboard', {
203
+ _initialQueries: seedInitialQueries(qc),
204
+ });
205
+ ```
206
+
207
+ ### Client side
208
+
209
+ ```ts
210
+ // In your client entry point, rehydrate from Inertia's page props
211
+ const page = window.__INERTIA_PAGE__; // or however you access the Inertia page object
212
+ const queryClient = hydrateClientFromInertia(page);
213
+ ```
214
+
215
+ This avoids a second network round-trip for data the server already fetched during SSR.
216
+
217
+ ## API Reference
218
+
219
+ ### `defineContract(def)`
220
+
221
+ Creates a typed contract definition. Accepts:
222
+
223
+ | Field | Required | Description |
224
+ |---|---|---|
225
+ | `response` | yes | Zod schema for the response body |
226
+ | `query` | no | Zod schema for URL query parameters |
227
+ | `body` | no | Zod schema for the request body |
228
+ | `params` | no | Zod schema for path parameters |
229
+ | `error` | no | Zod schema for error responses |
230
+
231
+ No `name`, `method`, or `path` — naming and routing come from NestJS decorators and codegen derivation.
232
+
233
+ ### `@As(name)`
234
+
235
+ Override the auto-derived route name at the controller **class** or **method** level (or both). When applied at both levels the values compose: `${classAs}.${methodAs}`. Each dot-separated segment must match `/^[a-z][a-zA-Z0-9]*$/`.
236
+
237
+ ### `@ApplyContract(contractDef, opts?)`
238
+
239
+ NestJS method decorator. Attaches the contract to the handler via `Reflect` metadata under `CONTRACT_METADATA`. Does **not** set HTTP method or path — always combine with `@Get`, `@Post`, etc.
240
+
241
+ Options:
242
+
243
+ | Option | Default | Description |
244
+ |---|---|---|
245
+ | `validate` | `false` | When `true`, installs a `ContractValidationPipe` that validates `body` and `query` against Zod schemas at runtime |
246
+
247
+ ### `createFetcher(opts?): Fetcher`
248
+
249
+ Creates a typed fetch wrapper. Options:
250
+
251
+ | Option | Type | Description |
252
+ |---|---|---|
253
+ | `baseUrl` | `string` | Prepended to every request path |
254
+ | `headers` | `() => Record<string, string>` | Dynamic headers (auth tokens, etc.) |
255
+ | `fetch` | `typeof fetch` | Custom fetch implementation (useful in tests) |
256
+ | `onError` | `(err: ApiHttpError) => void` | Called before an `ApiHttpError` is thrown |
257
+
258
+ ### `ApiHttpError`
259
+
260
+ Thrown when the server responds with a non-2xx status. Properties: `.status: number`, `.body: unknown`.
261
+
262
+ ### `invalidate(queryClient, queryKey)`
263
+
264
+ Convenience wrapper around `queryClient.invalidateQueries({ queryKey })`.
265
+
266
+ ## See Also
267
+
268
+ - Design spec: [`docs/superpowers/specs/2026-05-22-nestjs-inertia-plan-d-design.md`](../../docs/superpowers/specs/2026-05-22-nestjs-inertia-plan-d-design.md)
269
+ - Codegen (emits `api.ts`): [`packages/codegen/README.md`](../codegen/README.md)
270
+ - Implementation plan: [`docs/superpowers/plans/2026-05-22-nestjs-inertia-plan-d-client.md`](../../docs/superpowers/plans/2026-05-22-nestjs-inertia-plan-d-client.md)
271
+
272
+ ## License
273
+
274
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ ApiHttpError: () => ApiHttpError,
25
+ ApplyContract: () => ApplyContract,
26
+ As: () => As,
27
+ CONTRACT_METADATA: () => CONTRACT_METADATA,
28
+ ContractValidationPipe: () => ContractValidationPipe,
29
+ ROUTE_NAME_METADATA: () => ROUTE_NAME_METADATA,
30
+ VERSION: () => VERSION,
31
+ buildUrl: () => buildUrl,
32
+ createFetcher: () => createFetcher,
33
+ defineContract: () => defineContract,
34
+ getContract: () => getContract,
35
+ invalidate: () => invalidate
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+
39
+ // src/fetcher/errors.ts
40
+ var ApiHttpError = class _ApiHttpError extends Error {
41
+ static {
42
+ __name(this, "ApiHttpError");
43
+ }
44
+ status;
45
+ statusText;
46
+ body;
47
+ constructor(status, statusText, body) {
48
+ super(`HTTP ${status} ${statusText}`), this.status = status, this.statusText = statusText, this.body = body;
49
+ this.name = "ApiHttpError";
50
+ }
51
+ get isUnauthorized() {
52
+ return this.status === 401;
53
+ }
54
+ get isForbidden() {
55
+ return this.status === 403;
56
+ }
57
+ get isNotFound() {
58
+ return this.status === 404;
59
+ }
60
+ get isClient() {
61
+ return this.status >= 400 && this.status < 500;
62
+ }
63
+ get isServer() {
64
+ return this.status >= 500;
65
+ }
66
+ /**
67
+ * Returns a JSON-serializable representation of the error.
68
+ * The `body` field is **redacted by default** to prevent accidental logging of
69
+ * sensitive response bodies (e.g. API error payloads that may contain PII).
70
+ *
71
+ * Pass `verbose = true` to include the full body in the output.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // Safe: body is redacted
76
+ * JSON.stringify(err); // { ..., body: '[redacted]' }
77
+ *
78
+ * // Verbose: includes full body (use only in trusted contexts)
79
+ * JSON.stringify(err.toJSON(true)); // { ..., body: { message: '...' } }
80
+ * ```
81
+ */
82
+ toJSON(verbose = false) {
83
+ return {
84
+ name: this.name,
85
+ message: this.message,
86
+ status: this.status,
87
+ statusText: this.statusText,
88
+ body: verbose ? this.body : "[redacted \u2014 pass verbose=true to include]"
89
+ };
90
+ }
91
+ static async fromResponse(res) {
92
+ const ct = res.headers.get("content-type") ?? "";
93
+ const body = ct.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
94
+ return new _ApiHttpError(res.status, res.statusText, body);
95
+ }
96
+ };
97
+
98
+ // src/fetcher/url-builder.ts
99
+ function buildUrl(path, opts = {}, baseUrl) {
100
+ let resolved = path.replace(/:(\w+)/g, (_match, key) => {
101
+ const val = opts.params?.[key];
102
+ if (val === void 0 || val === null) {
103
+ throw new Error(`Missing param: ${key}`);
104
+ }
105
+ return encodeURIComponent(String(val));
106
+ });
107
+ const qs = new URLSearchParams();
108
+ if (opts.query) {
109
+ for (const [k, v] of Object.entries(opts.query)) {
110
+ if (v !== void 0) {
111
+ qs.set(k, String(v));
112
+ }
113
+ }
114
+ }
115
+ const qsStr = qs.toString();
116
+ if (qsStr) resolved += `?${qsStr}`;
117
+ if (baseUrl) {
118
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
119
+ return base + resolved;
120
+ }
121
+ return resolved;
122
+ }
123
+ __name(buildUrl, "buildUrl");
124
+
125
+ // src/fetcher/fetcher.ts
126
+ function isFormData(b) {
127
+ return typeof FormData !== "undefined" && b instanceof FormData;
128
+ }
129
+ __name(isFormData, "isFormData");
130
+ function createFetcher(opts = {}) {
131
+ const fetchImpl = opts.fetch ?? globalThis.fetch;
132
+ const baseUrl = opts.baseUrl ?? "";
133
+ async function request(method, path, ro = {}) {
134
+ if (!fetchImpl) {
135
+ throw new Error("No fetch implementation: pass opts.fetch or set globalThis.fetch");
136
+ }
137
+ const url = buildUrl(path, ro, baseUrl);
138
+ const headers = {
139
+ ...opts.headers?.()
140
+ };
141
+ let body = void 0;
142
+ if (ro.body !== void 0) {
143
+ if (isFormData(ro.body)) {
144
+ body = ro.body;
145
+ } else {
146
+ body = JSON.stringify(ro.body);
147
+ headers["content-type"] = "application/json";
148
+ }
149
+ }
150
+ if (!headers.accept) {
151
+ headers.accept = "application/json";
152
+ }
153
+ const res = await fetchImpl(url, {
154
+ method,
155
+ headers,
156
+ ...body !== void 0 ? {
157
+ body
158
+ } : {}
159
+ });
160
+ if (!res.ok) {
161
+ const err = await ApiHttpError.fromResponse(res);
162
+ opts.onError?.(err);
163
+ throw err;
164
+ }
165
+ if (res.status === 204) return void 0;
166
+ const ct = res.headers.get("content-type") ?? "";
167
+ if (ct.includes("application/json")) return await res.json();
168
+ return await res.text();
169
+ }
170
+ __name(request, "request");
171
+ return {
172
+ get: /* @__PURE__ */ __name((p, ro) => request("GET", p, ro), "get"),
173
+ post: /* @__PURE__ */ __name((p, ro) => request("POST", p, ro), "post"),
174
+ put: /* @__PURE__ */ __name((p, ro) => request("PUT", p, ro), "put"),
175
+ patch: /* @__PURE__ */ __name((p, ro) => request("PATCH", p, ro), "patch"),
176
+ delete: /* @__PURE__ */ __name((p, ro) => request("DELETE", p, ro), "delete")
177
+ };
178
+ }
179
+ __name(createFetcher, "createFetcher");
180
+
181
+ // src/contract/contract.ts
182
+ function defineContract(def) {
183
+ return def;
184
+ }
185
+ __name(defineContract, "defineContract");
186
+
187
+ // src/contract/apply-contract.decorator.ts
188
+ var import_common2 = require("@nestjs/common");
189
+
190
+ // src/contract/contract-validation.pipe.ts
191
+ var import_common = require("@nestjs/common");
192
+ function _ts_decorate(decorators, target, key, desc) {
193
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
194
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
195
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
196
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
197
+ }
198
+ __name(_ts_decorate, "_ts_decorate");
199
+ function _ts_metadata(k, v) {
200
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
201
+ }
202
+ __name(_ts_metadata, "_ts_metadata");
203
+ var ContractValidationPipe = class {
204
+ static {
205
+ __name(this, "ContractValidationPipe");
206
+ }
207
+ contract;
208
+ constructor(contract) {
209
+ this.contract = contract;
210
+ }
211
+ transform(value, metadata) {
212
+ let schema;
213
+ if (metadata.type === "body" && this.contract.body) {
214
+ schema = this.contract.body;
215
+ } else if (metadata.type === "query" && this.contract.query) {
216
+ schema = this.contract.query;
217
+ } else {
218
+ return value;
219
+ }
220
+ const parsed = schema.safeParse(value);
221
+ if (!parsed.success) {
222
+ throw new import_common.BadRequestException({
223
+ message: "Contract validation failed",
224
+ issues: parsed.error.issues
225
+ });
226
+ }
227
+ return parsed.data;
228
+ }
229
+ };
230
+ ContractValidationPipe = _ts_decorate([
231
+ (0, import_common.Injectable)(),
232
+ _ts_metadata("design:type", Function),
233
+ _ts_metadata("design:paramtypes", [
234
+ typeof C === "undefined" ? Object : C
235
+ ])
236
+ ], ContractValidationPipe);
237
+
238
+ // src/contract/metadata.ts
239
+ var CONTRACT_METADATA = /* @__PURE__ */ Symbol.for("nestjs-inertia:contract");
240
+ function getContract(target) {
241
+ if (typeof target !== "function") return void 0;
242
+ return Reflect.getMetadata(CONTRACT_METADATA, target) ?? void 0;
243
+ }
244
+ __name(getContract, "getContract");
245
+
246
+ // src/contract/apply-contract.decorator.ts
247
+ function ApplyContract(c, opts = {}) {
248
+ const decorators = [
249
+ (0, import_common2.SetMetadata)(CONTRACT_METADATA, c)
250
+ ];
251
+ if (opts.validate) {
252
+ decorators.push((0, import_common2.UsePipes)(new ContractValidationPipe(c)));
253
+ }
254
+ return (0, import_common2.applyDecorators)(...decorators);
255
+ }
256
+ __name(ApplyContract, "ApplyContract");
257
+
258
+ // src/contract/as.decorator.ts
259
+ var import_common3 = require("@nestjs/common");
260
+ var ROUTE_NAME_METADATA = /* @__PURE__ */ Symbol.for("nestjs-inertia:route-name");
261
+ var As = /* @__PURE__ */ __name((name) => (0, import_common3.SetMetadata)(ROUTE_NAME_METADATA, name), "As");
262
+
263
+ // src/invalidate.ts
264
+ function invalidate(qc, name, queryArgs) {
265
+ const queryKey = queryArgs === void 0 ? [
266
+ name
267
+ ] : [
268
+ name,
269
+ queryArgs
270
+ ];
271
+ return qc.invalidateQueries({
272
+ queryKey
273
+ });
274
+ }
275
+ __name(invalidate, "invalidate");
276
+
277
+ // src/index.ts
278
+ var VERSION = "1.0.0";
279
+ // Annotate the CommonJS export names for ESM import in node:
280
+ 0 && (module.exports = {
281
+ ApiHttpError,
282
+ ApplyContract,
283
+ As,
284
+ CONTRACT_METADATA,
285
+ ContractValidationPipe,
286
+ ROUTE_NAME_METADATA,
287
+ VERSION,
288
+ buildUrl,
289
+ createFetcher,
290
+ defineContract,
291
+ getContract,
292
+ invalidate
293
+ });
294
+ //# sourceMappingURL=index.cjs.map