@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/fetcher/errors.ts","../src/fetcher/url-builder.ts","../src/fetcher/fetcher.ts","../src/contract/contract.ts","../src/contract/apply-contract.decorator.ts","../src/contract/contract-validation.pipe.ts","../src/contract/metadata.ts","../src/contract/as.decorator.ts","../src/invalidate.ts"],"sourcesContent":["export const VERSION = '1.0.0';\n\nexport { createFetcher } from './fetcher/fetcher.js';\nexport { ApiHttpError } from './fetcher/errors.js';\nexport { buildUrl } from './fetcher/url-builder.js';\nexport { defineContract } from './contract/contract.js';\nexport { ApplyContract } from './contract/apply-contract.decorator.js';\nexport { As, ROUTE_NAME_METADATA } from './contract/as.decorator.js';\nexport { ContractValidationPipe } from './contract/contract-validation.pipe.js';\nexport { CONTRACT_METADATA, getContract } from './contract/metadata.js';\nexport type { ApplyContractOptions } from './contract/apply-contract.decorator.js';\nexport { invalidate } from './invalidate.js';\nexport type { FetcherOptions, Fetcher } from './fetcher/fetcher.js';\nexport type { ContractDef } from './contract/contract.js';\n// SSR helpers are available via the `./ssr` subpath export (dist/ssr/hydrate.js)\n","export class ApiHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly statusText: string,\n public readonly body: unknown,\n ) {\n super(`HTTP ${status} ${statusText}`);\n this.name = 'ApiHttpError';\n }\n\n get isUnauthorized(): boolean {\n return this.status === 401;\n }\n get isForbidden(): boolean {\n return this.status === 403;\n }\n get isNotFound(): boolean {\n return this.status === 404;\n }\n get isClient(): boolean {\n return this.status >= 400 && this.status < 500;\n }\n get isServer(): boolean {\n return this.status >= 500;\n }\n\n /**\n * Returns a JSON-serializable representation of the error.\n * The `body` field is **redacted by default** to prevent accidental logging of\n * sensitive response bodies (e.g. API error payloads that may contain PII).\n *\n * Pass `verbose = true` to include the full body in the output.\n *\n * @example\n * ```ts\n * // Safe: body is redacted\n * JSON.stringify(err); // { ..., body: '[redacted]' }\n *\n * // Verbose: includes full body (use only in trusted contexts)\n * JSON.stringify(err.toJSON(true)); // { ..., body: { message: '...' } }\n * ```\n */\n toJSON(verbose = false): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n statusText: this.statusText,\n body: verbose ? this.body : '[redacted — pass verbose=true to include]',\n };\n }\n\n static async fromResponse(res: Response): Promise<ApiHttpError> {\n const ct = res.headers.get('content-type') ?? '';\n const body = ct.includes('application/json')\n ? await res.json().catch(() => null)\n : await res.text().catch(() => '');\n return new ApiHttpError(res.status, res.statusText, body);\n }\n}\n","export interface BuildUrlOptions {\n params?: Record<string, unknown>;\n query?: Record<string, unknown>;\n}\n\n/**\n * Build a URL from a path template, path params, query params, and an optional base URL.\n *\n * Examples:\n * buildUrl('/users/:id', { params: { id: 42 } }) → '/users/42'\n * buildUrl('/users', { query: { active: true } }) → '/users?active=true'\n * buildUrl('/users', {}, 'https://api.test') → 'https://api.test/users'\n */\nexport function buildUrl(path: string, opts: BuildUrlOptions = {}, baseUrl?: string): string {\n // Interpolate path params — encodeURIComponent prevents path traversal\n // e.g. { id: '../admin' } → '/users/..%2Fadmin' not '/users/../admin'\n let resolved = path.replace(/:(\\w+)/g, (_match, key: string) => {\n const val = opts.params?.[key];\n if (val === undefined || val === null) {\n throw new Error(`Missing param: ${key}`);\n }\n return encodeURIComponent(String(val));\n });\n\n // Build query string\n const qs = new URLSearchParams();\n if (opts.query) {\n for (const [k, v] of Object.entries(opts.query)) {\n if (v !== undefined) {\n qs.set(k, String(v));\n }\n }\n }\n const qsStr = qs.toString();\n if (qsStr) resolved += `?${qsStr}`;\n\n // Prepend base URL if provided\n if (baseUrl) {\n const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n return base + resolved;\n }\n return resolved;\n}\n","import { ApiHttpError } from './errors.js';\nimport { buildUrl } from './url-builder.js';\n\nexport interface FetcherOptions {\n baseUrl?: string;\n /** Called once per request; allows dynamic auth tokens. */\n headers?: () => Record<string, string>;\n /** Injection seam for tests; default `globalThis.fetch`. */\n fetch?: typeof fetch;\n /** Invoked with the error before it is re-thrown. */\n onError?: (err: ApiHttpError) => void;\n}\n\nexport interface Fetcher {\n get<T>(path: string, opts?: RequestOpts): Promise<T>;\n post<T>(path: string, opts?: RequestOpts): Promise<T>;\n put<T>(path: string, opts?: RequestOpts): Promise<T>;\n patch<T>(path: string, opts?: RequestOpts): Promise<T>;\n delete<T>(path: string, opts?: RequestOpts): Promise<T>;\n}\n\ninterface RequestOpts {\n params?: Record<string, unknown>;\n query?: Record<string, unknown>;\n body?: unknown;\n}\n\nfunction isFormData(b: unknown): b is FormData {\n return typeof FormData !== 'undefined' && b instanceof FormData;\n}\n\nexport function createFetcher(opts: FetcherOptions = {}): Fetcher {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n const baseUrl = opts.baseUrl ?? '';\n\n async function request<T>(method: string, path: string, ro: RequestOpts = {}): Promise<T> {\n if (!fetchImpl) {\n throw new Error('No fetch implementation: pass opts.fetch or set globalThis.fetch');\n }\n const url = buildUrl(path, ro, baseUrl);\n const headers: Record<string, string> = { ...opts.headers?.() };\n let body: string | FormData | undefined = undefined;\n\n if (ro.body !== undefined) {\n if (isFormData(ro.body)) {\n body = ro.body;\n // Do NOT set Content-Type — the runtime sets it with the multipart boundary\n } else {\n body = JSON.stringify(ro.body);\n headers['content-type'] = 'application/json';\n }\n }\n\n if (!headers.accept) {\n headers.accept = 'application/json';\n }\n\n const res = await fetchImpl(url, { method, headers, ...(body !== undefined ? { body } : {}) });\n\n if (!res.ok) {\n const err = await ApiHttpError.fromResponse(res);\n opts.onError?.(err);\n throw err;\n }\n\n if (res.status === 204) return undefined as T;\n\n const ct = res.headers.get('content-type') ?? '';\n if (ct.includes('application/json')) return (await res.json()) as T;\n return (await res.text()) as unknown as T;\n }\n\n return {\n get: <T>(p: string, ro?: RequestOpts) => request<T>('GET', p, ro),\n post: <T>(p: string, ro?: RequestOpts) => request<T>('POST', p, ro),\n put: <T>(p: string, ro?: RequestOpts) => request<T>('PUT', p, ro),\n patch: <T>(p: string, ro?: RequestOpts) => request<T>('PATCH', p, ro),\n delete: <T>(p: string, ro?: RequestOpts) => request<T>('DELETE', p, ro),\n };\n}\n","import type { z } from 'zod';\n\nexport interface ContractDef<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown> {\n query?: z.ZodType<Q>;\n body?: z.ZodType<B>;\n response: z.ZodType<R>;\n params?: z.ZodType<P>;\n error?: z.ZodType<E>;\n}\n\nexport function defineContract<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown>(\n def: ContractDef<Q, B, R, P, E>,\n): ContractDef<Q, B, R, P, E> {\n return def;\n}\n","import { SetMetadata, UsePipes, applyDecorators } from '@nestjs/common';\nimport { ContractValidationPipe } from './contract-validation.pipe.js';\nimport type { ContractDef } from './contract.js';\nimport { CONTRACT_METADATA } from './metadata.js';\n\nexport interface ApplyContractOptions {\n /**\n * When `true`, validates incoming `body` and `query` against the Contract's\n * Zod schemas at runtime using a NestJS pipe.\n *\n * On validation failure a `BadRequestException` is thrown with the Zod\n * issues serialized in the response body:\n * `{ message: 'Contract validation failed', issues: ZodIssue[] }`.\n *\n * **Default: `false`** — schemas are read at codegen-time only, and no\n * runtime validation is performed. This matches the behaviour prior to\n * v0.9 and avoids breaking existing apps. Enable explicitly to enforce\n * the contract at runtime.\n */\n validate?: boolean;\n}\n\nexport function ApplyContract<C extends ContractDef>(\n c: C,\n opts: ApplyContractOptions = {},\n): MethodDecorator {\n const decorators: (ClassDecorator | MethodDecorator)[] = [SetMetadata(CONTRACT_METADATA, c)];\n\n if (opts.validate) {\n decorators.push(UsePipes(new ContractValidationPipe(c)));\n }\n\n return applyDecorators(...(decorators as MethodDecorator[]));\n}\n","import {\n type ArgumentMetadata,\n BadRequestException,\n Injectable,\n type PipeTransform,\n} from '@nestjs/common';\nimport type { ContractDef } from './contract.js';\n\n/**\n * NestJS pipe that validates incoming `body` and `query` against the\n * Contract's Zod schemas at runtime. Installed automatically when\n * `@ApplyContract(c, { validate: true })` is used.\n *\n * On validation failure throws a `BadRequestException` whose response body\n * contains `{ message: 'Contract validation failed', issues: ZodIssue[] }`.\n */\n@Injectable()\nexport class ContractValidationPipe<C extends ContractDef> implements PipeTransform {\n constructor(private readonly contract: C) {}\n\n transform(value: unknown, metadata: ArgumentMetadata): unknown {\n let schema:\n | {\n safeParse: (v: unknown) => {\n success: boolean;\n data: unknown;\n error: { issues: unknown[] };\n };\n }\n | undefined;\n\n if (metadata.type === 'body' && this.contract.body) {\n schema = this.contract.body as typeof schema;\n } else if (metadata.type === 'query' && this.contract.query) {\n schema = this.contract.query as typeof schema;\n } else {\n // Unhandled type — pass through unchanged\n return value;\n }\n\n // schema is always defined here because we checked above\n const parsed = schema!.safeParse(value);\n if (!parsed.success) {\n throw new BadRequestException({\n message: 'Contract validation failed',\n issues: parsed.error.issues,\n });\n }\n return parsed.data;\n }\n}\n","import type { ContractDef } from './contract.js';\n\nexport const CONTRACT_METADATA = Symbol.for('nestjs-inertia:contract');\n\ntype AnyContract = ContractDef<unknown, unknown, unknown, unknown, unknown>;\n\nexport function getContract(target: unknown): AnyContract | undefined {\n if (typeof target !== 'function') return undefined;\n return (Reflect.getMetadata(CONTRACT_METADATA, target) ?? undefined) as AnyContract | undefined;\n}\n","import { SetMetadata } from '@nestjs/common';\n\nexport const ROUTE_NAME_METADATA = Symbol.for('nestjs-inertia:route-name');\n\n/**\n * Override the auto-derived route name on a controller class or method.\n *\n * Codegen composes the final name as `${classPortion}.${methodPortion}`:\n * - **Class portion**: class-level `@As(...)` value if present, else the class name\n * with the `Controller` suffix stripped and first letter lowercased.\n * - **Method portion**: method-level `@As(...)` value if present, else the method name.\n *\n * Both can be multi-segment (contain dots). Each segment is validated against\n * `/^[a-z][a-zA-Z0-9]*$/`.\n *\n * @example Class-level override\n * ```ts\n * @Controller('/api/v1/crew')\n * @As('crew')\n * class CrewController {\n * @Get()\n * @ApplyContract(ListCrew)\n * list() { ... } // → 'crew.list'\n * }\n * ```\n *\n * @example Method-level override only\n * ```ts\n * @Controller('/api/v1/crew')\n * class CrewController {\n * @Get()\n * @ApplyContract(ListCrew)\n * @As('directory.fetch') // → 'crew.directory.fetch'\n * list() { ... }\n * }\n * ```\n *\n * @example Both class and method\n * ```ts\n * @Controller('/api/v1/crew')\n * @As('crew.admin')\n * class CrewController {\n * @Get()\n * @ApplyContract(ListCrew)\n * @As('top10') // → 'crew.admin.top10'\n * list() { ... }\n * }\n * ```\n */\nexport const As = (name: string): ClassDecorator & MethodDecorator =>\n SetMetadata(ROUTE_NAME_METADATA, name);\n","import type { QueryClient } from '@tanstack/query-core';\n\n/**\n * Invalidates queries matching `name` (and optionally `queryArgs`).\n *\n * - `invalidate(qc, 'users.list')` → invalidates `['users.list']`\n * - `invalidate(qc, 'users.list', { id: 1 })` → invalidates `['users.list', { id: 1 }]`\n */\nexport function invalidate(qc: QueryClient, name: string, queryArgs?: unknown): Promise<void> {\n const queryKey = queryArgs === undefined ? [name] : [name, queryArgs];\n return qc.invalidateQueries({ queryKey });\n}\n"],"mappings":"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","names":["ApiHttpError","Error","status","statusText","body","name","isUnauthorized","isForbidden","isNotFound","isClient","isServer","toJSON","verbose","message","fromResponse","res","ct","headers","get","includes","json","catch","text","buildUrl","path","opts","baseUrl","resolved","replace","_match","key","val","params","undefined","Error","encodeURIComponent","String","qs","URLSearchParams","query","k","v","Object","entries","set","qsStr","toString","base","endsWith","slice","isFormData","b","FormData","createFetcher","opts","fetchImpl","fetch","globalThis","baseUrl","request","method","path","ro","Error","url","buildUrl","headers","body","undefined","JSON","stringify","accept","res","ok","err","ApiHttpError","fromResponse","onError","status","ct","get","includes","json","text","p","post","put","patch","delete","defineContract","def","import_common","ContractValidationPipe","contract","transform","value","metadata","schema","type","body","query","parsed","safeParse","success","BadRequestException","message","issues","error","data","CONTRACT_METADATA","Symbol","for","getContract","target","undefined","Reflect","getMetadata","ApplyContract","c","opts","decorators","SetMetadata","CONTRACT_METADATA","validate","push","UsePipes","ContractValidationPipe","applyDecorators","import_common","ROUTE_NAME_METADATA","Symbol","for","As","name","SetMetadata","invalidate","qc","name","queryArgs","queryKey","undefined","invalidateQueries","VERSION"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PipeTransform, ArgumentMetadata } from '@nestjs/common';
|
|
3
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
4
|
+
|
|
5
|
+
declare class ApiHttpError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly statusText: string;
|
|
8
|
+
readonly body: unknown;
|
|
9
|
+
constructor(status: number, statusText: string, body: unknown);
|
|
10
|
+
get isUnauthorized(): boolean;
|
|
11
|
+
get isForbidden(): boolean;
|
|
12
|
+
get isNotFound(): boolean;
|
|
13
|
+
get isClient(): boolean;
|
|
14
|
+
get isServer(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a JSON-serializable representation of the error.
|
|
17
|
+
* The `body` field is **redacted by default** to prevent accidental logging of
|
|
18
|
+
* sensitive response bodies (e.g. API error payloads that may contain PII).
|
|
19
|
+
*
|
|
20
|
+
* Pass `verbose = true` to include the full body in the output.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Safe: body is redacted
|
|
25
|
+
* JSON.stringify(err); // { ..., body: '[redacted]' }
|
|
26
|
+
*
|
|
27
|
+
* // Verbose: includes full body (use only in trusted contexts)
|
|
28
|
+
* JSON.stringify(err.toJSON(true)); // { ..., body: { message: '...' } }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
toJSON(verbose?: boolean): Record<string, unknown>;
|
|
32
|
+
static fromResponse(res: Response): Promise<ApiHttpError>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FetcherOptions {
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
/** Called once per request; allows dynamic auth tokens. */
|
|
38
|
+
headers?: () => Record<string, string>;
|
|
39
|
+
/** Injection seam for tests; default `globalThis.fetch`. */
|
|
40
|
+
fetch?: typeof fetch;
|
|
41
|
+
/** Invoked with the error before it is re-thrown. */
|
|
42
|
+
onError?: (err: ApiHttpError) => void;
|
|
43
|
+
}
|
|
44
|
+
interface Fetcher {
|
|
45
|
+
get<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
46
|
+
post<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
47
|
+
put<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
48
|
+
patch<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
49
|
+
delete<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
50
|
+
}
|
|
51
|
+
interface RequestOpts {
|
|
52
|
+
params?: Record<string, unknown>;
|
|
53
|
+
query?: Record<string, unknown>;
|
|
54
|
+
body?: unknown;
|
|
55
|
+
}
|
|
56
|
+
declare function createFetcher(opts?: FetcherOptions): Fetcher;
|
|
57
|
+
|
|
58
|
+
interface BuildUrlOptions {
|
|
59
|
+
params?: Record<string, unknown>;
|
|
60
|
+
query?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a URL from a path template, path params, query params, and an optional base URL.
|
|
64
|
+
*
|
|
65
|
+
* Examples:
|
|
66
|
+
* buildUrl('/users/:id', { params: { id: 42 } }) → '/users/42'
|
|
67
|
+
* buildUrl('/users', { query: { active: true } }) → '/users?active=true'
|
|
68
|
+
* buildUrl('/users', {}, 'https://api.test') → 'https://api.test/users'
|
|
69
|
+
*/
|
|
70
|
+
declare function buildUrl(path: string, opts?: BuildUrlOptions, baseUrl?: string): string;
|
|
71
|
+
|
|
72
|
+
interface ContractDef<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown> {
|
|
73
|
+
query?: z.ZodType<Q>;
|
|
74
|
+
body?: z.ZodType<B>;
|
|
75
|
+
response: z.ZodType<R>;
|
|
76
|
+
params?: z.ZodType<P>;
|
|
77
|
+
error?: z.ZodType<E>;
|
|
78
|
+
}
|
|
79
|
+
declare function defineContract<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown>(def: ContractDef<Q, B, R, P, E>): ContractDef<Q, B, R, P, E>;
|
|
80
|
+
|
|
81
|
+
interface ApplyContractOptions {
|
|
82
|
+
/**
|
|
83
|
+
* When `true`, validates incoming `body` and `query` against the Contract's
|
|
84
|
+
* Zod schemas at runtime using a NestJS pipe.
|
|
85
|
+
*
|
|
86
|
+
* On validation failure a `BadRequestException` is thrown with the Zod
|
|
87
|
+
* issues serialized in the response body:
|
|
88
|
+
* `{ message: 'Contract validation failed', issues: ZodIssue[] }`.
|
|
89
|
+
*
|
|
90
|
+
* **Default: `false`** — schemas are read at codegen-time only, and no
|
|
91
|
+
* runtime validation is performed. This matches the behaviour prior to
|
|
92
|
+
* v0.9 and avoids breaking existing apps. Enable explicitly to enforce
|
|
93
|
+
* the contract at runtime.
|
|
94
|
+
*/
|
|
95
|
+
validate?: boolean;
|
|
96
|
+
}
|
|
97
|
+
declare function ApplyContract<C extends ContractDef>(c: C, opts?: ApplyContractOptions): MethodDecorator;
|
|
98
|
+
|
|
99
|
+
declare const ROUTE_NAME_METADATA: unique symbol;
|
|
100
|
+
/**
|
|
101
|
+
* Override the auto-derived route name on a controller class or method.
|
|
102
|
+
*
|
|
103
|
+
* Codegen composes the final name as `${classPortion}.${methodPortion}`:
|
|
104
|
+
* - **Class portion**: class-level `@As(...)` value if present, else the class name
|
|
105
|
+
* with the `Controller` suffix stripped and first letter lowercased.
|
|
106
|
+
* - **Method portion**: method-level `@As(...)` value if present, else the method name.
|
|
107
|
+
*
|
|
108
|
+
* Both can be multi-segment (contain dots). Each segment is validated against
|
|
109
|
+
* `/^[a-z][a-zA-Z0-9]*$/`.
|
|
110
|
+
*
|
|
111
|
+
* @example Class-level override
|
|
112
|
+
* ```ts
|
|
113
|
+
* @Controller('/api/v1/crew')
|
|
114
|
+
* @As('crew')
|
|
115
|
+
* class CrewController {
|
|
116
|
+
* @Get()
|
|
117
|
+
* @ApplyContract(ListCrew)
|
|
118
|
+
* list() { ... } // → 'crew.list'
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Method-level override only
|
|
123
|
+
* ```ts
|
|
124
|
+
* @Controller('/api/v1/crew')
|
|
125
|
+
* class CrewController {
|
|
126
|
+
* @Get()
|
|
127
|
+
* @ApplyContract(ListCrew)
|
|
128
|
+
* @As('directory.fetch') // → 'crew.directory.fetch'
|
|
129
|
+
* list() { ... }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @example Both class and method
|
|
134
|
+
* ```ts
|
|
135
|
+
* @Controller('/api/v1/crew')
|
|
136
|
+
* @As('crew.admin')
|
|
137
|
+
* class CrewController {
|
|
138
|
+
* @Get()
|
|
139
|
+
* @ApplyContract(ListCrew)
|
|
140
|
+
* @As('top10') // → 'crew.admin.top10'
|
|
141
|
+
* list() { ... }
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
declare const As: (name: string) => ClassDecorator & MethodDecorator;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* NestJS pipe that validates incoming `body` and `query` against the
|
|
149
|
+
* Contract's Zod schemas at runtime. Installed automatically when
|
|
150
|
+
* `@ApplyContract(c, { validate: true })` is used.
|
|
151
|
+
*
|
|
152
|
+
* On validation failure throws a `BadRequestException` whose response body
|
|
153
|
+
* contains `{ message: 'Contract validation failed', issues: ZodIssue[] }`.
|
|
154
|
+
*/
|
|
155
|
+
declare class ContractValidationPipe<C extends ContractDef> implements PipeTransform {
|
|
156
|
+
private readonly contract;
|
|
157
|
+
constructor(contract: C);
|
|
158
|
+
transform(value: unknown, metadata: ArgumentMetadata): unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare const CONTRACT_METADATA: unique symbol;
|
|
162
|
+
type AnyContract = ContractDef<unknown, unknown, unknown, unknown, unknown>;
|
|
163
|
+
declare function getContract(target: unknown): AnyContract | undefined;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Invalidates queries matching `name` (and optionally `queryArgs`).
|
|
167
|
+
*
|
|
168
|
+
* - `invalidate(qc, 'users.list')` → invalidates `['users.list']`
|
|
169
|
+
* - `invalidate(qc, 'users.list', { id: 1 })` → invalidates `['users.list', { id: 1 }]`
|
|
170
|
+
*/
|
|
171
|
+
declare function invalidate(qc: QueryClient, name: string, queryArgs?: unknown): Promise<void>;
|
|
172
|
+
|
|
173
|
+
declare const VERSION = "1.0.0";
|
|
174
|
+
|
|
175
|
+
export { ApiHttpError, ApplyContract, type ApplyContractOptions, As, CONTRACT_METADATA, type ContractDef, ContractValidationPipe, type Fetcher, type FetcherOptions, ROUTE_NAME_METADATA, VERSION, buildUrl, createFetcher, defineContract, getContract, invalidate };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PipeTransform, ArgumentMetadata } from '@nestjs/common';
|
|
3
|
+
import { QueryClient } from '@tanstack/query-core';
|
|
4
|
+
|
|
5
|
+
declare class ApiHttpError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly statusText: string;
|
|
8
|
+
readonly body: unknown;
|
|
9
|
+
constructor(status: number, statusText: string, body: unknown);
|
|
10
|
+
get isUnauthorized(): boolean;
|
|
11
|
+
get isForbidden(): boolean;
|
|
12
|
+
get isNotFound(): boolean;
|
|
13
|
+
get isClient(): boolean;
|
|
14
|
+
get isServer(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a JSON-serializable representation of the error.
|
|
17
|
+
* The `body` field is **redacted by default** to prevent accidental logging of
|
|
18
|
+
* sensitive response bodies (e.g. API error payloads that may contain PII).
|
|
19
|
+
*
|
|
20
|
+
* Pass `verbose = true` to include the full body in the output.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Safe: body is redacted
|
|
25
|
+
* JSON.stringify(err); // { ..., body: '[redacted]' }
|
|
26
|
+
*
|
|
27
|
+
* // Verbose: includes full body (use only in trusted contexts)
|
|
28
|
+
* JSON.stringify(err.toJSON(true)); // { ..., body: { message: '...' } }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
toJSON(verbose?: boolean): Record<string, unknown>;
|
|
32
|
+
static fromResponse(res: Response): Promise<ApiHttpError>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FetcherOptions {
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
/** Called once per request; allows dynamic auth tokens. */
|
|
38
|
+
headers?: () => Record<string, string>;
|
|
39
|
+
/** Injection seam for tests; default `globalThis.fetch`. */
|
|
40
|
+
fetch?: typeof fetch;
|
|
41
|
+
/** Invoked with the error before it is re-thrown. */
|
|
42
|
+
onError?: (err: ApiHttpError) => void;
|
|
43
|
+
}
|
|
44
|
+
interface Fetcher {
|
|
45
|
+
get<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
46
|
+
post<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
47
|
+
put<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
48
|
+
patch<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
49
|
+
delete<T>(path: string, opts?: RequestOpts): Promise<T>;
|
|
50
|
+
}
|
|
51
|
+
interface RequestOpts {
|
|
52
|
+
params?: Record<string, unknown>;
|
|
53
|
+
query?: Record<string, unknown>;
|
|
54
|
+
body?: unknown;
|
|
55
|
+
}
|
|
56
|
+
declare function createFetcher(opts?: FetcherOptions): Fetcher;
|
|
57
|
+
|
|
58
|
+
interface BuildUrlOptions {
|
|
59
|
+
params?: Record<string, unknown>;
|
|
60
|
+
query?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a URL from a path template, path params, query params, and an optional base URL.
|
|
64
|
+
*
|
|
65
|
+
* Examples:
|
|
66
|
+
* buildUrl('/users/:id', { params: { id: 42 } }) → '/users/42'
|
|
67
|
+
* buildUrl('/users', { query: { active: true } }) → '/users?active=true'
|
|
68
|
+
* buildUrl('/users', {}, 'https://api.test') → 'https://api.test/users'
|
|
69
|
+
*/
|
|
70
|
+
declare function buildUrl(path: string, opts?: BuildUrlOptions, baseUrl?: string): string;
|
|
71
|
+
|
|
72
|
+
interface ContractDef<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown> {
|
|
73
|
+
query?: z.ZodType<Q>;
|
|
74
|
+
body?: z.ZodType<B>;
|
|
75
|
+
response: z.ZodType<R>;
|
|
76
|
+
params?: z.ZodType<P>;
|
|
77
|
+
error?: z.ZodType<E>;
|
|
78
|
+
}
|
|
79
|
+
declare function defineContract<Q = unknown, B = unknown, R = unknown, P = unknown, E = unknown>(def: ContractDef<Q, B, R, P, E>): ContractDef<Q, B, R, P, E>;
|
|
80
|
+
|
|
81
|
+
interface ApplyContractOptions {
|
|
82
|
+
/**
|
|
83
|
+
* When `true`, validates incoming `body` and `query` against the Contract's
|
|
84
|
+
* Zod schemas at runtime using a NestJS pipe.
|
|
85
|
+
*
|
|
86
|
+
* On validation failure a `BadRequestException` is thrown with the Zod
|
|
87
|
+
* issues serialized in the response body:
|
|
88
|
+
* `{ message: 'Contract validation failed', issues: ZodIssue[] }`.
|
|
89
|
+
*
|
|
90
|
+
* **Default: `false`** — schemas are read at codegen-time only, and no
|
|
91
|
+
* runtime validation is performed. This matches the behaviour prior to
|
|
92
|
+
* v0.9 and avoids breaking existing apps. Enable explicitly to enforce
|
|
93
|
+
* the contract at runtime.
|
|
94
|
+
*/
|
|
95
|
+
validate?: boolean;
|
|
96
|
+
}
|
|
97
|
+
declare function ApplyContract<C extends ContractDef>(c: C, opts?: ApplyContractOptions): MethodDecorator;
|
|
98
|
+
|
|
99
|
+
declare const ROUTE_NAME_METADATA: unique symbol;
|
|
100
|
+
/**
|
|
101
|
+
* Override the auto-derived route name on a controller class or method.
|
|
102
|
+
*
|
|
103
|
+
* Codegen composes the final name as `${classPortion}.${methodPortion}`:
|
|
104
|
+
* - **Class portion**: class-level `@As(...)` value if present, else the class name
|
|
105
|
+
* with the `Controller` suffix stripped and first letter lowercased.
|
|
106
|
+
* - **Method portion**: method-level `@As(...)` value if present, else the method name.
|
|
107
|
+
*
|
|
108
|
+
* Both can be multi-segment (contain dots). Each segment is validated against
|
|
109
|
+
* `/^[a-z][a-zA-Z0-9]*$/`.
|
|
110
|
+
*
|
|
111
|
+
* @example Class-level override
|
|
112
|
+
* ```ts
|
|
113
|
+
* @Controller('/api/v1/crew')
|
|
114
|
+
* @As('crew')
|
|
115
|
+
* class CrewController {
|
|
116
|
+
* @Get()
|
|
117
|
+
* @ApplyContract(ListCrew)
|
|
118
|
+
* list() { ... } // → 'crew.list'
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Method-level override only
|
|
123
|
+
* ```ts
|
|
124
|
+
* @Controller('/api/v1/crew')
|
|
125
|
+
* class CrewController {
|
|
126
|
+
* @Get()
|
|
127
|
+
* @ApplyContract(ListCrew)
|
|
128
|
+
* @As('directory.fetch') // → 'crew.directory.fetch'
|
|
129
|
+
* list() { ... }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* @example Both class and method
|
|
134
|
+
* ```ts
|
|
135
|
+
* @Controller('/api/v1/crew')
|
|
136
|
+
* @As('crew.admin')
|
|
137
|
+
* class CrewController {
|
|
138
|
+
* @Get()
|
|
139
|
+
* @ApplyContract(ListCrew)
|
|
140
|
+
* @As('top10') // → 'crew.admin.top10'
|
|
141
|
+
* list() { ... }
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
declare const As: (name: string) => ClassDecorator & MethodDecorator;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* NestJS pipe that validates incoming `body` and `query` against the
|
|
149
|
+
* Contract's Zod schemas at runtime. Installed automatically when
|
|
150
|
+
* `@ApplyContract(c, { validate: true })` is used.
|
|
151
|
+
*
|
|
152
|
+
* On validation failure throws a `BadRequestException` whose response body
|
|
153
|
+
* contains `{ message: 'Contract validation failed', issues: ZodIssue[] }`.
|
|
154
|
+
*/
|
|
155
|
+
declare class ContractValidationPipe<C extends ContractDef> implements PipeTransform {
|
|
156
|
+
private readonly contract;
|
|
157
|
+
constructor(contract: C);
|
|
158
|
+
transform(value: unknown, metadata: ArgumentMetadata): unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
declare const CONTRACT_METADATA: unique symbol;
|
|
162
|
+
type AnyContract = ContractDef<unknown, unknown, unknown, unknown, unknown>;
|
|
163
|
+
declare function getContract(target: unknown): AnyContract | undefined;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Invalidates queries matching `name` (and optionally `queryArgs`).
|
|
167
|
+
*
|
|
168
|
+
* - `invalidate(qc, 'users.list')` → invalidates `['users.list']`
|
|
169
|
+
* - `invalidate(qc, 'users.list', { id: 1 })` → invalidates `['users.list', { id: 1 }]`
|
|
170
|
+
*/
|
|
171
|
+
declare function invalidate(qc: QueryClient, name: string, queryArgs?: unknown): Promise<void>;
|
|
172
|
+
|
|
173
|
+
declare const VERSION = "1.0.0";
|
|
174
|
+
|
|
175
|
+
export { ApiHttpError, ApplyContract, type ApplyContractOptions, As, CONTRACT_METADATA, type ContractDef, ContractValidationPipe, type Fetcher, type FetcherOptions, ROUTE_NAME_METADATA, VERSION, buildUrl, createFetcher, defineContract, getContract, invalidate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/fetcher/errors.ts
|
|
5
|
+
var ApiHttpError = class _ApiHttpError extends Error {
|
|
6
|
+
static {
|
|
7
|
+
__name(this, "ApiHttpError");
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
statusText;
|
|
11
|
+
body;
|
|
12
|
+
constructor(status, statusText, body) {
|
|
13
|
+
super(`HTTP ${status} ${statusText}`), this.status = status, this.statusText = statusText, this.body = body;
|
|
14
|
+
this.name = "ApiHttpError";
|
|
15
|
+
}
|
|
16
|
+
get isUnauthorized() {
|
|
17
|
+
return this.status === 401;
|
|
18
|
+
}
|
|
19
|
+
get isForbidden() {
|
|
20
|
+
return this.status === 403;
|
|
21
|
+
}
|
|
22
|
+
get isNotFound() {
|
|
23
|
+
return this.status === 404;
|
|
24
|
+
}
|
|
25
|
+
get isClient() {
|
|
26
|
+
return this.status >= 400 && this.status < 500;
|
|
27
|
+
}
|
|
28
|
+
get isServer() {
|
|
29
|
+
return this.status >= 500;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns a JSON-serializable representation of the error.
|
|
33
|
+
* The `body` field is **redacted by default** to prevent accidental logging of
|
|
34
|
+
* sensitive response bodies (e.g. API error payloads that may contain PII).
|
|
35
|
+
*
|
|
36
|
+
* Pass `verbose = true` to include the full body in the output.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* // Safe: body is redacted
|
|
41
|
+
* JSON.stringify(err); // { ..., body: '[redacted]' }
|
|
42
|
+
*
|
|
43
|
+
* // Verbose: includes full body (use only in trusted contexts)
|
|
44
|
+
* JSON.stringify(err.toJSON(true)); // { ..., body: { message: '...' } }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
toJSON(verbose = false) {
|
|
48
|
+
return {
|
|
49
|
+
name: this.name,
|
|
50
|
+
message: this.message,
|
|
51
|
+
status: this.status,
|
|
52
|
+
statusText: this.statusText,
|
|
53
|
+
body: verbose ? this.body : "[redacted \u2014 pass verbose=true to include]"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
static async fromResponse(res) {
|
|
57
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
58
|
+
const body = ct.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
|
|
59
|
+
return new _ApiHttpError(res.status, res.statusText, body);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/fetcher/url-builder.ts
|
|
64
|
+
function buildUrl(path, opts = {}, baseUrl) {
|
|
65
|
+
let resolved = path.replace(/:(\w+)/g, (_match, key) => {
|
|
66
|
+
const val = opts.params?.[key];
|
|
67
|
+
if (val === void 0 || val === null) {
|
|
68
|
+
throw new Error(`Missing param: ${key}`);
|
|
69
|
+
}
|
|
70
|
+
return encodeURIComponent(String(val));
|
|
71
|
+
});
|
|
72
|
+
const qs = new URLSearchParams();
|
|
73
|
+
if (opts.query) {
|
|
74
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
75
|
+
if (v !== void 0) {
|
|
76
|
+
qs.set(k, String(v));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const qsStr = qs.toString();
|
|
81
|
+
if (qsStr) resolved += `?${qsStr}`;
|
|
82
|
+
if (baseUrl) {
|
|
83
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
84
|
+
return base + resolved;
|
|
85
|
+
}
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
__name(buildUrl, "buildUrl");
|
|
89
|
+
|
|
90
|
+
// src/fetcher/fetcher.ts
|
|
91
|
+
function isFormData(b) {
|
|
92
|
+
return typeof FormData !== "undefined" && b instanceof FormData;
|
|
93
|
+
}
|
|
94
|
+
__name(isFormData, "isFormData");
|
|
95
|
+
function createFetcher(opts = {}) {
|
|
96
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
97
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
98
|
+
async function request(method, path, ro = {}) {
|
|
99
|
+
if (!fetchImpl) {
|
|
100
|
+
throw new Error("No fetch implementation: pass opts.fetch or set globalThis.fetch");
|
|
101
|
+
}
|
|
102
|
+
const url = buildUrl(path, ro, baseUrl);
|
|
103
|
+
const headers = {
|
|
104
|
+
...opts.headers?.()
|
|
105
|
+
};
|
|
106
|
+
let body = void 0;
|
|
107
|
+
if (ro.body !== void 0) {
|
|
108
|
+
if (isFormData(ro.body)) {
|
|
109
|
+
body = ro.body;
|
|
110
|
+
} else {
|
|
111
|
+
body = JSON.stringify(ro.body);
|
|
112
|
+
headers["content-type"] = "application/json";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!headers.accept) {
|
|
116
|
+
headers.accept = "application/json";
|
|
117
|
+
}
|
|
118
|
+
const res = await fetchImpl(url, {
|
|
119
|
+
method,
|
|
120
|
+
headers,
|
|
121
|
+
...body !== void 0 ? {
|
|
122
|
+
body
|
|
123
|
+
} : {}
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const err = await ApiHttpError.fromResponse(res);
|
|
127
|
+
opts.onError?.(err);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
if (res.status === 204) return void 0;
|
|
131
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
132
|
+
if (ct.includes("application/json")) return await res.json();
|
|
133
|
+
return await res.text();
|
|
134
|
+
}
|
|
135
|
+
__name(request, "request");
|
|
136
|
+
return {
|
|
137
|
+
get: /* @__PURE__ */ __name((p, ro) => request("GET", p, ro), "get"),
|
|
138
|
+
post: /* @__PURE__ */ __name((p, ro) => request("POST", p, ro), "post"),
|
|
139
|
+
put: /* @__PURE__ */ __name((p, ro) => request("PUT", p, ro), "put"),
|
|
140
|
+
patch: /* @__PURE__ */ __name((p, ro) => request("PATCH", p, ro), "patch"),
|
|
141
|
+
delete: /* @__PURE__ */ __name((p, ro) => request("DELETE", p, ro), "delete")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
__name(createFetcher, "createFetcher");
|
|
145
|
+
|
|
146
|
+
// src/contract/contract.ts
|
|
147
|
+
function defineContract(def) {
|
|
148
|
+
return def;
|
|
149
|
+
}
|
|
150
|
+
__name(defineContract, "defineContract");
|
|
151
|
+
|
|
152
|
+
// src/contract/apply-contract.decorator.ts
|
|
153
|
+
import { SetMetadata, UsePipes, applyDecorators } from "@nestjs/common";
|
|
154
|
+
|
|
155
|
+
// src/contract/contract-validation.pipe.ts
|
|
156
|
+
import { BadRequestException, Injectable } from "@nestjs/common";
|
|
157
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
158
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
159
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
160
|
+
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;
|
|
161
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
162
|
+
}
|
|
163
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
164
|
+
function _ts_metadata(k, v) {
|
|
165
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
166
|
+
}
|
|
167
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
168
|
+
var ContractValidationPipe = class {
|
|
169
|
+
static {
|
|
170
|
+
__name(this, "ContractValidationPipe");
|
|
171
|
+
}
|
|
172
|
+
contract;
|
|
173
|
+
constructor(contract) {
|
|
174
|
+
this.contract = contract;
|
|
175
|
+
}
|
|
176
|
+
transform(value, metadata) {
|
|
177
|
+
let schema;
|
|
178
|
+
if (metadata.type === "body" && this.contract.body) {
|
|
179
|
+
schema = this.contract.body;
|
|
180
|
+
} else if (metadata.type === "query" && this.contract.query) {
|
|
181
|
+
schema = this.contract.query;
|
|
182
|
+
} else {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
const parsed = schema.safeParse(value);
|
|
186
|
+
if (!parsed.success) {
|
|
187
|
+
throw new BadRequestException({
|
|
188
|
+
message: "Contract validation failed",
|
|
189
|
+
issues: parsed.error.issues
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return parsed.data;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
ContractValidationPipe = _ts_decorate([
|
|
196
|
+
Injectable(),
|
|
197
|
+
_ts_metadata("design:type", Function),
|
|
198
|
+
_ts_metadata("design:paramtypes", [
|
|
199
|
+
typeof C === "undefined" ? Object : C
|
|
200
|
+
])
|
|
201
|
+
], ContractValidationPipe);
|
|
202
|
+
|
|
203
|
+
// src/contract/metadata.ts
|
|
204
|
+
var CONTRACT_METADATA = /* @__PURE__ */ Symbol.for("nestjs-inertia:contract");
|
|
205
|
+
function getContract(target) {
|
|
206
|
+
if (typeof target !== "function") return void 0;
|
|
207
|
+
return Reflect.getMetadata(CONTRACT_METADATA, target) ?? void 0;
|
|
208
|
+
}
|
|
209
|
+
__name(getContract, "getContract");
|
|
210
|
+
|
|
211
|
+
// src/contract/apply-contract.decorator.ts
|
|
212
|
+
function ApplyContract(c, opts = {}) {
|
|
213
|
+
const decorators = [
|
|
214
|
+
SetMetadata(CONTRACT_METADATA, c)
|
|
215
|
+
];
|
|
216
|
+
if (opts.validate) {
|
|
217
|
+
decorators.push(UsePipes(new ContractValidationPipe(c)));
|
|
218
|
+
}
|
|
219
|
+
return applyDecorators(...decorators);
|
|
220
|
+
}
|
|
221
|
+
__name(ApplyContract, "ApplyContract");
|
|
222
|
+
|
|
223
|
+
// src/contract/as.decorator.ts
|
|
224
|
+
import { SetMetadata as SetMetadata2 } from "@nestjs/common";
|
|
225
|
+
var ROUTE_NAME_METADATA = /* @__PURE__ */ Symbol.for("nestjs-inertia:route-name");
|
|
226
|
+
var As = /* @__PURE__ */ __name((name) => SetMetadata2(ROUTE_NAME_METADATA, name), "As");
|
|
227
|
+
|
|
228
|
+
// src/invalidate.ts
|
|
229
|
+
function invalidate(qc, name, queryArgs) {
|
|
230
|
+
const queryKey = queryArgs === void 0 ? [
|
|
231
|
+
name
|
|
232
|
+
] : [
|
|
233
|
+
name,
|
|
234
|
+
queryArgs
|
|
235
|
+
];
|
|
236
|
+
return qc.invalidateQueries({
|
|
237
|
+
queryKey
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
__name(invalidate, "invalidate");
|
|
241
|
+
|
|
242
|
+
// src/index.ts
|
|
243
|
+
var VERSION = "1.0.0";
|
|
244
|
+
export {
|
|
245
|
+
ApiHttpError,
|
|
246
|
+
ApplyContract,
|
|
247
|
+
As,
|
|
248
|
+
CONTRACT_METADATA,
|
|
249
|
+
ContractValidationPipe,
|
|
250
|
+
ROUTE_NAME_METADATA,
|
|
251
|
+
VERSION,
|
|
252
|
+
buildUrl,
|
|
253
|
+
createFetcher,
|
|
254
|
+
defineContract,
|
|
255
|
+
getContract,
|
|
256
|
+
invalidate
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=index.js.map
|