yaxa-svelte 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -14
- package/dist/server/endpoint/endpoint.test.d.ts +1 -0
- package/dist/server/endpoint/endpoint.test.js +364 -0
- package/dist/server/endpoint/guards.d.ts +57 -0
- package/dist/server/endpoint/guards.js +153 -0
- package/dist/server/endpoint/index.d.ts +14 -0
- package/dist/server/endpoint/index.js +242 -0
- package/dist/server/endpoint/problem.d.ts +26 -0
- package/dist/server/endpoint/problem.js +62 -0
- package/dist/server/endpoint/types.d.ts +151 -0
- package/dist/server/endpoint/types.js +1 -0
- package/dist/server/endpoint/validate.d.ts +21 -0
- package/dist/server/endpoint/validate.js +128 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/openapi/generator.d.ts +5 -0
- package/dist/server/openapi/generator.js +317 -0
- package/dist/server/openapi/handler.d.ts +7 -0
- package/dist/server/openapi/handler.js +117 -0
- package/dist/server/openapi/index.d.ts +5 -0
- package/dist/server/openapi/index.js +4 -0
- package/dist/server/openapi/openapi.test.d.ts +1 -0
- package/dist/server/openapi/openapi.test.js +219 -0
- package/dist/server/openapi/registry.d.ts +35 -0
- package/dist/server/openapi/registry.js +51 -0
- package/dist/server/openapi/schema.d.ts +4 -0
- package/dist/server/openapi/schema.js +59 -0
- package/dist/server/openapi/types.d.ts +127 -0
- package/dist/server/openapi/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import type { YaxaEndpointConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps SvelteKit +server.ts request handlers into an opinionated,
|
|
5
|
+
* declarative, type-safe execution pipeline with Guards, Multi-Engine
|
|
6
|
+
* Validation, and RFC 7807 Problem Details.
|
|
7
|
+
*/
|
|
8
|
+
export declare function yaxaEndpoint<TBodySchema = any, TQuerySchema = any, TParamsSchema = any, THeadersSchema = any, TResult = any>(config: YaxaEndpointConfig<TBodySchema, TQuerySchema, TParamsSchema, THeadersSchema, TResult>): RequestHandler & {
|
|
9
|
+
_yaxaConfig: typeof config;
|
|
10
|
+
};
|
|
11
|
+
export * from './types';
|
|
12
|
+
export * from './problem';
|
|
13
|
+
export * from './guards';
|
|
14
|
+
export * from './validate';
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { isProblemDetails, problemResponse } from './problem';
|
|
2
|
+
import { validateData } from './validate';
|
|
3
|
+
import { globalApiRegistry } from '../openapi/registry';
|
|
4
|
+
/**
|
|
5
|
+
* Parses URLSearchParams into a plain key-value object.
|
|
6
|
+
* Duplicate keys are parsed into arrays.
|
|
7
|
+
*/
|
|
8
|
+
function parseSearchParamsToObject(searchParams) {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const key of searchParams.keys()) {
|
|
11
|
+
const allValues = searchParams.getAll(key);
|
|
12
|
+
if (allValues.length > 1) {
|
|
13
|
+
result[key] = allValues;
|
|
14
|
+
}
|
|
15
|
+
else if (allValues.length === 1) {
|
|
16
|
+
result[key] = allValues[0];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Safely parses request body based on Content-Type.
|
|
23
|
+
*/
|
|
24
|
+
async function parseRequestBody(request) {
|
|
25
|
+
const contentType = request.headers.get('content-type') || '';
|
|
26
|
+
if (contentType.includes('application/json')) {
|
|
27
|
+
try {
|
|
28
|
+
const text = await request.text();
|
|
29
|
+
if (!text.trim()) {
|
|
30
|
+
return { ok: true, body: undefined };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, body: JSON.parse(text) };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
response: problemResponse({
|
|
38
|
+
status: 400,
|
|
39
|
+
title: 'Bad Request',
|
|
40
|
+
detail: 'Malformed JSON payload in request body.',
|
|
41
|
+
type: 'https://yaxa.dev/errors/malformed-json'
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (contentType.includes('application/x-www-form-urlencoded') ||
|
|
47
|
+
contentType.includes('multipart/form-data')) {
|
|
48
|
+
try {
|
|
49
|
+
const formData = await request.formData();
|
|
50
|
+
const formObj = {};
|
|
51
|
+
formData.forEach((val, key) => {
|
|
52
|
+
if (key in formObj) {
|
|
53
|
+
if (Array.isArray(formObj[key])) {
|
|
54
|
+
formObj[key].push(val);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
formObj[key] = [formObj[key], val];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
formObj[key] = val;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return { ok: true, body: formObj };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
response: problemResponse({
|
|
70
|
+
status: 400,
|
|
71
|
+
title: 'Bad Request',
|
|
72
|
+
detail: 'Malformed form-data in request body.'
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Plain text or other payloads
|
|
78
|
+
try {
|
|
79
|
+
const text = await request.text();
|
|
80
|
+
return { ok: true, body: text ? text : undefined };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return { ok: true, body: undefined };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Wraps SvelteKit +server.ts request handlers into an opinionated,
|
|
88
|
+
* declarative, type-safe execution pipeline with Guards, Multi-Engine
|
|
89
|
+
* Validation, and RFC 7807 Problem Details.
|
|
90
|
+
*/
|
|
91
|
+
export function yaxaEndpoint(config) {
|
|
92
|
+
const handler = async (event) => {
|
|
93
|
+
const { request, url, locals, params } = event;
|
|
94
|
+
// 1. Execute Declarative Guards in Series
|
|
95
|
+
if (config.guards && config.guards.length > 0) {
|
|
96
|
+
for (const guard of config.guards) {
|
|
97
|
+
const result = await guard(event);
|
|
98
|
+
if (result instanceof Response) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
if (isProblemDetails(result)) {
|
|
102
|
+
return problemResponse(result);
|
|
103
|
+
}
|
|
104
|
+
if (result === false) {
|
|
105
|
+
return problemResponse({
|
|
106
|
+
status: 403,
|
|
107
|
+
title: 'Forbidden',
|
|
108
|
+
detail: 'Access denied by endpoint guard.'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 2. Parse Raw Inputs
|
|
114
|
+
const rawParams = params || {};
|
|
115
|
+
const rawQuery = parseSearchParamsToObject(url.searchParams);
|
|
116
|
+
const rawHeaders = {};
|
|
117
|
+
request.headers.forEach((val, key) => {
|
|
118
|
+
rawHeaders[key.toLowerCase()] = val;
|
|
119
|
+
});
|
|
120
|
+
let rawBody;
|
|
121
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
122
|
+
const parsed = await parseRequestBody(request);
|
|
123
|
+
if (!parsed.ok) {
|
|
124
|
+
return parsed.response;
|
|
125
|
+
}
|
|
126
|
+
rawBody = parsed.body;
|
|
127
|
+
}
|
|
128
|
+
// 3. Multi-Engine Schema Validation (Standard Schema, Zod, etc.)
|
|
129
|
+
const allInvalidParams = [];
|
|
130
|
+
let validatedBody = rawBody;
|
|
131
|
+
let validatedQuery = rawQuery;
|
|
132
|
+
let validatedParams = rawParams;
|
|
133
|
+
let validatedHeaders = rawHeaders;
|
|
134
|
+
if (config.schema) {
|
|
135
|
+
if (config.schema.params) {
|
|
136
|
+
const res = await validateData(config.schema.params, rawParams, 'params');
|
|
137
|
+
if (res.success)
|
|
138
|
+
validatedParams = res.data;
|
|
139
|
+
else
|
|
140
|
+
allInvalidParams.push(...res.invalidParams);
|
|
141
|
+
}
|
|
142
|
+
if (config.schema.query) {
|
|
143
|
+
const res = await validateData(config.schema.query, rawQuery, 'query');
|
|
144
|
+
if (res.success)
|
|
145
|
+
validatedQuery = res.data;
|
|
146
|
+
else
|
|
147
|
+
allInvalidParams.push(...res.invalidParams);
|
|
148
|
+
}
|
|
149
|
+
if (config.schema.headers) {
|
|
150
|
+
const res = await validateData(config.schema.headers, rawHeaders, 'headers');
|
|
151
|
+
if (res.success)
|
|
152
|
+
validatedHeaders = res.data;
|
|
153
|
+
else
|
|
154
|
+
allInvalidParams.push(...res.invalidParams);
|
|
155
|
+
}
|
|
156
|
+
if (config.schema.body && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
157
|
+
const res = await validateData(config.schema.body, rawBody, 'body');
|
|
158
|
+
if (res.success)
|
|
159
|
+
validatedBody = res.data;
|
|
160
|
+
else
|
|
161
|
+
allInvalidParams.push(...res.invalidParams);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (allInvalidParams.length > 0) {
|
|
165
|
+
return problemResponse({
|
|
166
|
+
status: 422,
|
|
167
|
+
title: 'Unprocessable Entity',
|
|
168
|
+
detail: `Validation failed for ${allInvalidParams.length} parameter(s).`,
|
|
169
|
+
type: 'https://yaxa.dev/errors/validation-failed',
|
|
170
|
+
invalidParams: allInvalidParams
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// 4. Construct Rich Endpoint Context
|
|
174
|
+
const context = {
|
|
175
|
+
event,
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
locals,
|
|
179
|
+
body: validatedBody,
|
|
180
|
+
query: validatedQuery,
|
|
181
|
+
params: validatedParams,
|
|
182
|
+
headers: validatedHeaders,
|
|
183
|
+
user: locals?.user || null,
|
|
184
|
+
session: locals?.session || null,
|
|
185
|
+
orgId: locals?.orgId || null
|
|
186
|
+
};
|
|
187
|
+
// 5. Execute Handler wrapped in Interceptors & Error Boundary
|
|
188
|
+
const runHandler = async () => {
|
|
189
|
+
const defaultStatus = config.status || 200;
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await config.handle(context);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
// Allow SvelteKit redirects and HTTP errors to bubble up
|
|
196
|
+
if (err && typeof err === 'object' && ('status' in err || 'location' in err)) {
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
if (err instanceof Response) {
|
|
200
|
+
return err;
|
|
201
|
+
}
|
|
202
|
+
console.error('[Yaxa Endpoint Error]', err);
|
|
203
|
+
return problemResponse({
|
|
204
|
+
status: 500,
|
|
205
|
+
title: 'Internal Server Error',
|
|
206
|
+
detail: 'An unexpected internal error occurred.',
|
|
207
|
+
type: 'https://yaxa.dev/errors/internal-server-error'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// 6. Response Auto-Serialization
|
|
211
|
+
if (result instanceof Response) {
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
if (result === undefined || result === null) {
|
|
215
|
+
return new Response(null, { status: 204 });
|
|
216
|
+
}
|
|
217
|
+
if (typeof result === 'string') {
|
|
218
|
+
return new Response(result, {
|
|
219
|
+
status: defaultStatus,
|
|
220
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return Response.json(result, { status: defaultStatus });
|
|
224
|
+
};
|
|
225
|
+
// 7. Apply Interceptors (if any)
|
|
226
|
+
if (config.interceptors && config.interceptors.length > 0) {
|
|
227
|
+
const pipeline = config.interceptors.reduceRight((next, interceptor) => () => interceptor(context, next), runHandler);
|
|
228
|
+
return pipeline();
|
|
229
|
+
}
|
|
230
|
+
return runHandler();
|
|
231
|
+
};
|
|
232
|
+
// Attach configuration metadata for runtime reflection and OpenAPI generation (Milestone 3)
|
|
233
|
+
handler._yaxaConfig = config;
|
|
234
|
+
if (config.path && config.method) {
|
|
235
|
+
globalApiRegistry.register(config.path, { [config.method]: handler });
|
|
236
|
+
}
|
|
237
|
+
return handler;
|
|
238
|
+
}
|
|
239
|
+
export * from './types';
|
|
240
|
+
export * from './problem';
|
|
241
|
+
export * from './guards';
|
|
242
|
+
export * from './validate';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ProblemDetails } from './types';
|
|
2
|
+
export interface CreateProblemOptions {
|
|
3
|
+
status: number;
|
|
4
|
+
title?: string;
|
|
5
|
+
detail?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
instance?: string;
|
|
8
|
+
invalidParams?: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
}>;
|
|
12
|
+
headers?: HeadersInit;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates an RFC 7807 Problem Details object.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createProblemDetails(options: CreateProblemOptions): ProblemDetails;
|
|
19
|
+
/**
|
|
20
|
+
* Creates a standard SvelteKit Response formatted with application/problem+json.
|
|
21
|
+
*/
|
|
22
|
+
export declare function problemResponse(options: CreateProblemOptions): Response;
|
|
23
|
+
/**
|
|
24
|
+
* Type-guard checking if an object conforms to ProblemDetails.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isProblemDetails(value: unknown): value is ProblemDetails;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard HTTP error status code to human-readable RFC 7807 titles.
|
|
3
|
+
*/
|
|
4
|
+
const STATUS_TITLES = {
|
|
5
|
+
400: 'Bad Request',
|
|
6
|
+
401: 'Unauthorized',
|
|
7
|
+
403: 'Forbidden',
|
|
8
|
+
404: 'Not Found',
|
|
9
|
+
405: 'Method Not Allowed',
|
|
10
|
+
409: 'Conflict',
|
|
11
|
+
422: 'Unprocessable Entity',
|
|
12
|
+
429: 'Too Many Requests',
|
|
13
|
+
500: 'Internal Server Error',
|
|
14
|
+
502: 'Bad Gateway',
|
|
15
|
+
503: 'Service Unavailable',
|
|
16
|
+
504: 'Gateway Timeout'
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Creates an RFC 7807 Problem Details object.
|
|
20
|
+
*/
|
|
21
|
+
export function createProblemDetails(options) {
|
|
22
|
+
const status = options.status || 500;
|
|
23
|
+
const title = options.title || STATUS_TITLES[status] || 'Internal Error';
|
|
24
|
+
const type = options.type || `https://httpstatuses.com/${status}`;
|
|
25
|
+
const problem = {
|
|
26
|
+
type,
|
|
27
|
+
title,
|
|
28
|
+
status
|
|
29
|
+
};
|
|
30
|
+
for (const [key, value] of Object.entries(options)) {
|
|
31
|
+
if (key !== 'headers' && key !== 'status' && key !== 'title' && key !== 'type') {
|
|
32
|
+
problem[key] = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return problem;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a standard SvelteKit Response formatted with application/problem+json.
|
|
39
|
+
*/
|
|
40
|
+
export function problemResponse(options) {
|
|
41
|
+
const status = options.status || 500;
|
|
42
|
+
const problem = createProblemDetails(options);
|
|
43
|
+
const customHeaders = new Headers(options.headers || {});
|
|
44
|
+
if (!customHeaders.has('content-type')) {
|
|
45
|
+
customHeaders.set('content-type', 'application/problem+json; charset=utf-8');
|
|
46
|
+
}
|
|
47
|
+
return new Response(JSON.stringify(problem), {
|
|
48
|
+
status,
|
|
49
|
+
headers: customHeaders
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type-guard checking if an object conforms to ProblemDetails.
|
|
54
|
+
*/
|
|
55
|
+
export function isProblemDetails(value) {
|
|
56
|
+
return (typeof value === 'object' &&
|
|
57
|
+
value !== null &&
|
|
58
|
+
'status' in value &&
|
|
59
|
+
typeof value.status === 'number' &&
|
|
60
|
+
'title' in value &&
|
|
61
|
+
'type' in value);
|
|
62
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Standard RFC 7807 Problem Details representation.
|
|
4
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7807
|
|
5
|
+
*/
|
|
6
|
+
export interface ProblemDetails {
|
|
7
|
+
type: string;
|
|
8
|
+
title: string;
|
|
9
|
+
status: number;
|
|
10
|
+
detail?: string;
|
|
11
|
+
instance?: string;
|
|
12
|
+
invalidParams?: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}>;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generic guard return value.
|
|
20
|
+
* - Return `Response` or `ProblemDetails`: Immediately stops pipeline and sends HTTP response.
|
|
21
|
+
* - Return `void` or `undefined` or `true`: Guard passed; continues to next step.
|
|
22
|
+
*/
|
|
23
|
+
export type GuardResult = Response | ProblemDetails | void | undefined | boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Guard function signature.
|
|
26
|
+
*/
|
|
27
|
+
export type Guard = (event: RequestEvent) => Promise<GuardResult> | GuardResult;
|
|
28
|
+
/**
|
|
29
|
+
* Standard Schema or validator representation.
|
|
30
|
+
* Supports:
|
|
31
|
+
* 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
|
|
32
|
+
* 2. `.safeParse()` (Zod, Yup, etc.)
|
|
33
|
+
* 3. Functional validator: `(data: unknown) => { success: boolean; data?: T; error?: any }`
|
|
34
|
+
*/
|
|
35
|
+
export type AnyValidator<T = any> = {
|
|
36
|
+
'~standard': {
|
|
37
|
+
validate: (value: unknown) => Promise<{
|
|
38
|
+
value?: T;
|
|
39
|
+
issues?: ReadonlyArray<any>;
|
|
40
|
+
}> | {
|
|
41
|
+
value?: T;
|
|
42
|
+
issues?: ReadonlyArray<any>;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
} | {
|
|
46
|
+
safeParse: (value: unknown) => {
|
|
47
|
+
success: true;
|
|
48
|
+
data: T;
|
|
49
|
+
} | {
|
|
50
|
+
success: false;
|
|
51
|
+
error: any;
|
|
52
|
+
};
|
|
53
|
+
} | {
|
|
54
|
+
parse: (value: unknown) => T;
|
|
55
|
+
} | ((value: unknown) => {
|
|
56
|
+
success: boolean;
|
|
57
|
+
data?: T;
|
|
58
|
+
error?: any;
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Infer output type from a validator.
|
|
62
|
+
*/
|
|
63
|
+
export type InferValidatorOutput<V> = V extends {
|
|
64
|
+
'~standard': {
|
|
65
|
+
validate: (...args: any[]) => Promise<{
|
|
66
|
+
value?: infer T;
|
|
67
|
+
}> | {
|
|
68
|
+
value?: infer T;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
} ? T : V extends {
|
|
72
|
+
safeParse: (...args: any[]) => {
|
|
73
|
+
success: true;
|
|
74
|
+
data: infer T;
|
|
75
|
+
} | {
|
|
76
|
+
success: false;
|
|
77
|
+
error: any;
|
|
78
|
+
};
|
|
79
|
+
} ? T : V extends {
|
|
80
|
+
parse: (...args: any[]) => infer T;
|
|
81
|
+
} ? T : V extends (value: unknown) => {
|
|
82
|
+
success: true;
|
|
83
|
+
data: infer T;
|
|
84
|
+
} ? T : any;
|
|
85
|
+
export interface YaxaEndpointSchemaConfig<TBody = any, TQuery = any, TParams = any, THeaders = any> {
|
|
86
|
+
body?: TBody;
|
|
87
|
+
query?: TQuery;
|
|
88
|
+
params?: TParams;
|
|
89
|
+
headers?: THeaders;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Rich context passed to business handler in yaxaEndpoint.
|
|
93
|
+
*/
|
|
94
|
+
export interface YaxaEndpointContext<TBody = any, TQuery = any, TParams = any, THeaders = any> {
|
|
95
|
+
event: RequestEvent;
|
|
96
|
+
request: Request;
|
|
97
|
+
url: URL;
|
|
98
|
+
locals: App.Locals;
|
|
99
|
+
body: TBody;
|
|
100
|
+
query: TQuery;
|
|
101
|
+
params: TParams;
|
|
102
|
+
headers: THeaders;
|
|
103
|
+
user: any;
|
|
104
|
+
session: any;
|
|
105
|
+
orgId: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Interceptor function wrapping handler execution.
|
|
109
|
+
*/
|
|
110
|
+
export type YaxaInterceptor = (context: YaxaEndpointContext, next: () => Promise<Response>) => Promise<Response>;
|
|
111
|
+
/**
|
|
112
|
+
* Endpoint configuration passed to yaxaEndpoint().
|
|
113
|
+
*/
|
|
114
|
+
export interface YaxaEndpointConfig<TBodySchema = any, TQuerySchema = any, TParamsSchema = any, THeadersSchema = any, TResult = any> {
|
|
115
|
+
/**
|
|
116
|
+
* Sequence of declarative guards executed before handler body.
|
|
117
|
+
* (e.g. requireAuth(), requireRole(['admin']), rateLimit({ points: 10 }))
|
|
118
|
+
*/
|
|
119
|
+
guards?: Guard[];
|
|
120
|
+
/**
|
|
121
|
+
* Schemas for input payload validation (body, query string, path params, headers).
|
|
122
|
+
*/
|
|
123
|
+
schema?: YaxaEndpointSchemaConfig<TBodySchema, TQuerySchema, TParamsSchema, THeadersSchema>;
|
|
124
|
+
/**
|
|
125
|
+
* Optional status code to return for successful non-Response return values.
|
|
126
|
+
* Defaults to 200 (or 201 for POST when returning created object if desired).
|
|
127
|
+
*/
|
|
128
|
+
status?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Optional response interceptors (e.g. audit logging, timing headers).
|
|
131
|
+
*/
|
|
132
|
+
interceptors?: YaxaInterceptor[];
|
|
133
|
+
/**
|
|
134
|
+
* OpenAPI metadata annotations (used by Milestone 3 auto-docs).
|
|
135
|
+
*/
|
|
136
|
+
summary?: string;
|
|
137
|
+
description?: string;
|
|
138
|
+
tags?: string[];
|
|
139
|
+
deprecated?: boolean;
|
|
140
|
+
operationId?: string;
|
|
141
|
+
path?: string;
|
|
142
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string;
|
|
143
|
+
responses?: Record<number, {
|
|
144
|
+
description: string;
|
|
145
|
+
schema?: any;
|
|
146
|
+
}>;
|
|
147
|
+
/**
|
|
148
|
+
* Core business logic handler.
|
|
149
|
+
*/
|
|
150
|
+
handle: (context: YaxaEndpointContext<InferValidatorOutput<TBodySchema>, InferValidatorOutput<TQuerySchema>, InferValidatorOutput<TParamsSchema>, InferValidatorOutput<THeadersSchema>>) => Promise<TResult> | TResult;
|
|
151
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ValidationSuccess<T> {
|
|
2
|
+
success: true;
|
|
3
|
+
data: T;
|
|
4
|
+
}
|
|
5
|
+
export interface ValidationFailure {
|
|
6
|
+
success: false;
|
|
7
|
+
invalidParams: Array<{
|
|
8
|
+
name: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}>;
|
|
11
|
+
rawError?: any;
|
|
12
|
+
}
|
|
13
|
+
export type ValidationOutcome<T> = ValidationSuccess<T> | ValidationFailure;
|
|
14
|
+
/**
|
|
15
|
+
* Validates arbitrary input data against any supported validator:
|
|
16
|
+
* 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
|
|
17
|
+
* 2. `.safeParse()` (Zod, Yup, etc.)
|
|
18
|
+
* 3. `.parse()`
|
|
19
|
+
* 4. Custom validation function: `(data) => { success, data, error }`
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateData<T = any>(validator: any, input: unknown, scopePrefix?: string): Promise<ValidationOutcome<T>>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates arbitrary input data against any supported validator:
|
|
3
|
+
* 1. Standard Schema v1 (`~standard` property, e.g. Zod v3.24+, Zod v4, Valibot, ArkType)
|
|
4
|
+
* 2. `.safeParse()` (Zod, Yup, etc.)
|
|
5
|
+
* 3. `.parse()`
|
|
6
|
+
* 4. Custom validation function: `(data) => { success, data, error }`
|
|
7
|
+
*/
|
|
8
|
+
export async function validateData(validator, input, scopePrefix = '') {
|
|
9
|
+
if (!validator) {
|
|
10
|
+
return { success: true, data: input };
|
|
11
|
+
}
|
|
12
|
+
// 1. Standard Schema v1 (~standard specification)
|
|
13
|
+
if (typeof validator === 'object' && validator !== null && '~standard' in validator) {
|
|
14
|
+
try {
|
|
15
|
+
const std = validator['~standard'];
|
|
16
|
+
const result = await std.validate(input);
|
|
17
|
+
if (result &&
|
|
18
|
+
'issues' in result &&
|
|
19
|
+
Array.isArray(result.issues) &&
|
|
20
|
+
result.issues.length > 0) {
|
|
21
|
+
const invalidParams = result.issues.map((issue) => {
|
|
22
|
+
let pathStr = '';
|
|
23
|
+
if (Array.isArray(issue.path)) {
|
|
24
|
+
pathStr = issue.path
|
|
25
|
+
.map((seg) => typeof seg === 'object' && seg !== null && 'key' in seg ? seg.key : String(seg))
|
|
26
|
+
.join('.');
|
|
27
|
+
}
|
|
28
|
+
const name = scopePrefix
|
|
29
|
+
? pathStr
|
|
30
|
+
? `${scopePrefix}.${pathStr}`
|
|
31
|
+
: scopePrefix
|
|
32
|
+
: pathStr || 'value';
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
reason: issue.message || 'Invalid value'
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return { success: false, invalidParams, rawError: result.issues };
|
|
39
|
+
}
|
|
40
|
+
return { success: true, data: result.value };
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
invalidParams: [
|
|
46
|
+
{ name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }
|
|
47
|
+
],
|
|
48
|
+
rawError: err
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 2. Standard .safeParse() (Zod, etc.)
|
|
53
|
+
if (typeof validator === 'object' &&
|
|
54
|
+
validator !== null &&
|
|
55
|
+
typeof validator.safeParse === 'function') {
|
|
56
|
+
const parsed = validator.safeParse(input);
|
|
57
|
+
if (parsed.success) {
|
|
58
|
+
return { success: true, data: parsed.data };
|
|
59
|
+
}
|
|
60
|
+
// ZodError or similar
|
|
61
|
+
const issues = parsed.error?.issues || parsed.error?.errors || [];
|
|
62
|
+
const invalidParams = issues.length > 0
|
|
63
|
+
? issues.map((issue) => {
|
|
64
|
+
const pathStr = Array.isArray(issue.path) ? issue.path.join('.') : '';
|
|
65
|
+
const name = scopePrefix
|
|
66
|
+
? pathStr
|
|
67
|
+
? `${scopePrefix}.${pathStr}`
|
|
68
|
+
: scopePrefix
|
|
69
|
+
: pathStr || 'value';
|
|
70
|
+
return {
|
|
71
|
+
name,
|
|
72
|
+
reason: issue.message || 'Validation failed'
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
: [{ name: scopePrefix || 'value', reason: parsed.error?.message || 'Validation failed' }];
|
|
76
|
+
return { success: false, invalidParams, rawError: parsed.error };
|
|
77
|
+
}
|
|
78
|
+
// 3. Functional Validator
|
|
79
|
+
if (typeof validator === 'function') {
|
|
80
|
+
try {
|
|
81
|
+
const res = validator(input);
|
|
82
|
+
if (res && typeof res === 'object' && 'success' in res) {
|
|
83
|
+
if (res.success) {
|
|
84
|
+
return { success: true, data: (res.data !== undefined ? res.data : input) };
|
|
85
|
+
}
|
|
86
|
+
const reason = res.error?.message || (typeof res.error === 'string' ? res.error : 'Validation failed');
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
invalidParams: [{ name: scopePrefix || 'value', reason }],
|
|
90
|
+
rawError: res.error
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// If function returned parsed data without error
|
|
94
|
+
return { success: true, data: res };
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
invalidParams: [
|
|
100
|
+
{ name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }
|
|
101
|
+
],
|
|
102
|
+
rawError: err
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 4. Standard .parse() throwing validator
|
|
107
|
+
if (typeof validator === 'object' &&
|
|
108
|
+
validator !== null &&
|
|
109
|
+
typeof validator.parse === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
const data = validator.parse(input);
|
|
112
|
+
return { success: true, data: data };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const issues = err?.issues || err?.errors || [];
|
|
116
|
+
const invalidParams = issues.length > 0
|
|
117
|
+
? issues.map((issue) => ({
|
|
118
|
+
name: scopePrefix
|
|
119
|
+
? `${scopePrefix}.${issue.path?.join('.')}`
|
|
120
|
+
: issue.path?.join('.') || 'value',
|
|
121
|
+
reason: issue.message || 'Validation failed'
|
|
122
|
+
}))
|
|
123
|
+
: [{ name: scopePrefix || 'value', reason: err?.message || 'Validation failed' }];
|
|
124
|
+
return { success: false, invalidParams, rawError: err };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { success: true, data: input };
|
|
128
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -22,3 +22,7 @@ export { getResendClient, sendMagicLinkEmail, sendWelcomeEmail } from './email/i
|
|
|
22
22
|
export type { ResendEmailConfig, SendMagicLinkOptions, SendWelcomeEmailOptions } from './email/index';
|
|
23
23
|
export { getS3Client, createPresignedUploadUrl, createS3UploadHandler } from './storage/index';
|
|
24
24
|
export type { S3StorageConfig, PresignedUploadOptions, PresignedUploadResult, S3UploadHandlerOptions } from './storage/index';
|
|
25
|
+
export { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails, requireAuth, requireRole, rateLimit, composeGuards, validateData } from './endpoint/index';
|
|
26
|
+
export type { YaxaEndpointConfig, YaxaEndpointContext, YaxaEndpointSchemaConfig, YaxaInterceptor, Guard, GuardResult, ProblemDetails, CreateProblemOptions, RequireAuthOptions, RequireRoleOptions, RateLimitOptions, AnyValidator, InferValidatorOutput, ValidationOutcome, ValidationSuccess, ValidationFailure } from './endpoint/index';
|
|
27
|
+
export { generateOpenApiSpec, createApiDocsHandler, createApiRegistry, globalApiRegistry, convertToJsonSchema } from './openapi/index';
|
|
28
|
+
export type { OpenApiSpec31, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, GenerateOpenApiOptions, ApiDocsOptions, ApiDocsTheme, EndpointLike, EndpointMap } from './openapi/index';
|
package/dist/server/index.js
CHANGED
|
@@ -18,3 +18,7 @@ export { getPolarClient, createPolarCheckout, createPolarCustomerPortalSession,
|
|
|
18
18
|
export { getResendClient, sendMagicLinkEmail, sendWelcomeEmail } from './email/index';
|
|
19
19
|
// Cloud Storage (AWS S3 & Cloudflare R2)
|
|
20
20
|
export { getS3Client, createPresignedUploadUrl, createS3UploadHandler } from './storage/index';
|
|
21
|
+
// Endpoint Pipeline, Guards & RFC 7807 Validation (Milestone 1)
|
|
22
|
+
export { yaxaEndpoint, createProblemDetails, problemResponse, isProblemDetails, requireAuth, requireRole, rateLimit, composeGuards, validateData } from './endpoint/index';
|
|
23
|
+
// Auto-Generated OpenAPI 3.1 & Interactive Docs (Milestone 3)
|
|
24
|
+
export { generateOpenApiSpec, createApiDocsHandler, createApiRegistry, globalApiRegistry, convertToJsonSchema } from './openapi/index';
|