@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 +72 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/index.cjs +294 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +258 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +68 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +26 -0
- package/dist/react/index.d.ts +26 -0
- package/dist/react/index.js +41 -0
- package/dist/react/index.js.map +1 -0
- package/dist/ssr/hydrate.cjs +50 -0
- package/dist/ssr/hydrate.cjs.map +1 -0
- package/dist/ssr/hydrate.d.cts +21 -0
- package/dist/ssr/hydrate.d.ts +21 -0
- package/dist/ssr/hydrate.js +26 -0
- package/dist/ssr/hydrate.js.map +1 -0
- package/dist/svelte/Link.svelte +25 -0
- package/dist/svelte/index.cjs +101 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +21 -0
- package/dist/svelte/index.d.ts +21 -0
- package/dist/svelte/index.js +64 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +101 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +73 -0
- package/dist/vue/index.d.ts +73 -0
- package/dist/vue/index.js +73 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +118 -0
- package/src/svelte/Link.svelte +25 -0
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
|
+
[](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
|