yaxa-svelte 2.2.0 → 2.4.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 +85 -14
- package/dist/server/db/schema-pg.d.ts +262 -0
- package/dist/server/db/schema-pg.js +20 -0
- package/dist/server/db/schema-sqlite.d.ts +276 -0
- package/dist/server/db/schema-sqlite.js +20 -0
- package/dist/server/db/types.d.ts +18 -0
- package/dist/server/endpoint/guards.d.ts +5 -0
- package/dist/server/endpoint/guards.js +19 -3
- package/dist/server/endpoint/index.js +4 -0
- package/dist/server/endpoint/types.d.ts +7 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/jobs/events.d.ts +29 -0
- package/dist/server/jobs/events.js +74 -0
- package/dist/server/jobs/index.d.ts +4 -0
- package/dist/server/jobs/index.js +3 -0
- package/dist/server/jobs/jobs.test.d.ts +1 -0
- package/dist/server/jobs/jobs.test.js +189 -0
- package/dist/server/jobs/queue.d.ts +14 -0
- package/dist/server/jobs/queue.js +90 -0
- package/dist/server/jobs/runner.d.ts +12 -0
- package/dist/server/jobs/runner.js +176 -0
- package/dist/server/jobs/types.d.ts +136 -0
- package/dist/server/jobs/types.js +1 -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,117 @@
|
|
|
1
|
+
import { generateOpenApiSpec } from './generator';
|
|
2
|
+
function renderScalarHtml(specJson, title) {
|
|
3
|
+
return `<!doctype html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<title>${title}</title>
|
|
9
|
+
<style>
|
|
10
|
+
body { margin: 0; background-color: #0f172a; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<script id="api-reference" data-configuration='{"darkMode":true,"layout":"modern"}' type="application/json">
|
|
15
|
+
${specJson}
|
|
16
|
+
</script>
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
18
|
+
</body>
|
|
19
|
+
</html>`;
|
|
20
|
+
}
|
|
21
|
+
function renderSwaggerHtml(specJson, title) {
|
|
22
|
+
return `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
27
|
+
<title>${title}</title>
|
|
28
|
+
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
29
|
+
<style>
|
|
30
|
+
body { margin: 0; background: #fafafa; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div id="swagger-ui"></div>
|
|
35
|
+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
36
|
+
<script>
|
|
37
|
+
window.ui = SwaggerUIBundle({
|
|
38
|
+
spec: ${specJson},
|
|
39
|
+
dom_id: '#swagger-ui',
|
|
40
|
+
deepLinking: true,
|
|
41
|
+
presets: [
|
|
42
|
+
SwaggerUIBundle.presets.apis,
|
|
43
|
+
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
44
|
+
],
|
|
45
|
+
layout: "BaseLayout"
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>`;
|
|
50
|
+
}
|
|
51
|
+
function renderRedocHtml(specJson, title) {
|
|
52
|
+
return `<!DOCTYPE html>
|
|
53
|
+
<html lang="en">
|
|
54
|
+
<head>
|
|
55
|
+
<meta charset="utf-8" />
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
57
|
+
<title>${title}</title>
|
|
58
|
+
<style>
|
|
59
|
+
body { margin: 0; padding: 0; }
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<div id="redoc-container"></div>
|
|
64
|
+
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
|
65
|
+
<script>
|
|
66
|
+
Redoc.init(${specJson}, {
|
|
67
|
+
scrollYOffset: 50,
|
|
68
|
+
theme: {
|
|
69
|
+
colors: { primary: { main: '#3b82f6' } }
|
|
70
|
+
}
|
|
71
|
+
}, document.getElementById('redoc-container'));
|
|
72
|
+
</script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Creates a turnkey SvelteKit RequestHandler that generates an interactive OpenAPI 3.1 documentation
|
|
78
|
+
* page (Scalar, Swagger UI, or Redoc) or returns raw OpenAPI 3.1 JSON when ?format=json is requested.
|
|
79
|
+
*/
|
|
80
|
+
export function createApiDocsHandler(options = {}) {
|
|
81
|
+
const theme = options.theme || 'scalar';
|
|
82
|
+
return async (event) => {
|
|
83
|
+
const { url, request } = event;
|
|
84
|
+
const format = url.searchParams.get('format');
|
|
85
|
+
const acceptHeader = request.headers.get('accept') || '';
|
|
86
|
+
const spec = generateOpenApiSpec(options);
|
|
87
|
+
// If caller requests JSON via query param, accept header, or path ending in .json
|
|
88
|
+
if (format === 'json' ||
|
|
89
|
+
acceptHeader.includes('application/json') ||
|
|
90
|
+
url.pathname.endsWith('/openapi.json')) {
|
|
91
|
+
return Response.json(spec, {
|
|
92
|
+
headers: {
|
|
93
|
+
'content-type': 'application/json; charset=utf-8',
|
|
94
|
+
'cache-control': 'public, max-age=60'
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Render Interactive HTML Docs
|
|
99
|
+
const specJson = JSON.stringify(spec);
|
|
100
|
+
let html;
|
|
101
|
+
if (theme === 'swagger') {
|
|
102
|
+
html = renderSwaggerHtml(specJson, spec.info.title);
|
|
103
|
+
}
|
|
104
|
+
else if (theme === 'redoc') {
|
|
105
|
+
html = renderRedocHtml(specJson, spec.info.title);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
html = renderScalarHtml(specJson, spec.info.title);
|
|
109
|
+
}
|
|
110
|
+
return new Response(html, {
|
|
111
|
+
headers: {
|
|
112
|
+
'content-type': 'text/html; charset=utf-8',
|
|
113
|
+
'cache-control': 'public, max-age=300'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { generateOpenApiSpec } from './generator';
|
|
2
|
+
export { createApiDocsHandler } from './handler';
|
|
3
|
+
export { ApiRegistry, createApiRegistry, globalApiRegistry } from './registry';
|
|
4
|
+
export { convertToJsonSchema } from './schema';
|
|
5
|
+
export type { OpenApiSpec31, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, GenerateOpenApiOptions, ApiDocsOptions, ApiDocsTheme, EndpointLike, EndpointMap } from './types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createMockRequestEvent } from '../../testing/index.js';
|
|
4
|
+
import { generateOpenApiSpec, createApiDocsHandler, createApiRegistry, globalApiRegistry, convertToJsonSchema } from './index.js';
|
|
5
|
+
import { yaxaEndpoint } from '../endpoint/index.js';
|
|
6
|
+
import { requireAuth, requireRole, rateLimit } from '../endpoint/guards.js';
|
|
7
|
+
describe('OpenAPI 3.1 Schema Engine', () => {
|
|
8
|
+
it('converts Zod schema into compliant JSON Schema', () => {
|
|
9
|
+
const schema = z.object({
|
|
10
|
+
title: z.string().min(3),
|
|
11
|
+
count: z.number().optional(),
|
|
12
|
+
role: z.enum(['admin', 'member'])
|
|
13
|
+
});
|
|
14
|
+
const jsonSchema = convertToJsonSchema(schema);
|
|
15
|
+
expect(jsonSchema.type).toBe('object');
|
|
16
|
+
expect(jsonSchema.properties.title.type).toBe('string');
|
|
17
|
+
expect(jsonSchema.properties.count.type).toBe('number');
|
|
18
|
+
expect(jsonSchema.required).toContain('title');
|
|
19
|
+
expect(jsonSchema.required).toContain('role');
|
|
20
|
+
expect(jsonSchema.required).not.toContain('count');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe('API Route Registry', () => {
|
|
24
|
+
let registry;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
registry = createApiRegistry();
|
|
27
|
+
globalApiRegistry.clear();
|
|
28
|
+
});
|
|
29
|
+
it('normalizes SvelteKit bracket path params to OpenAPI curly braces', () => {
|
|
30
|
+
expect(registry.normalizePath('/api/items/[id]')).toBe('/api/items/{id}');
|
|
31
|
+
expect(registry.normalizePath('/api/orgs/[orgSlug]/users/[userId]')).toBe('/api/orgs/{orgSlug}/users/{userId}');
|
|
32
|
+
expect(registry.normalizePath('/api/files/[...rest]')).toBe('/api/files/{rest}');
|
|
33
|
+
});
|
|
34
|
+
it('registers endpoints and retrieves them in normalized form', () => {
|
|
35
|
+
const getHandler = yaxaEndpoint({ handle: () => [] });
|
|
36
|
+
registry.register('/api/users/[id]', { GET: getHandler });
|
|
37
|
+
const endpoints = registry.getEndpoints();
|
|
38
|
+
expect(endpoints['/api/users/{id}']).toBeDefined();
|
|
39
|
+
expect(endpoints['/api/users/{id}']['GET']).toBe(getHandler);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('generateOpenApiSpec Engine', () => {
|
|
43
|
+
const ItemParamsSchema = z.object({
|
|
44
|
+
id: z.string()
|
|
45
|
+
});
|
|
46
|
+
const ItemQuerySchema = z.object({
|
|
47
|
+
filter: z.string().optional(),
|
|
48
|
+
limit: z.number().optional()
|
|
49
|
+
});
|
|
50
|
+
const CreateItemBodySchema = z.object({
|
|
51
|
+
name: z.string().min(2),
|
|
52
|
+
price: z.number().positive()
|
|
53
|
+
});
|
|
54
|
+
it('generates complete OpenAPI 3.1 document with parameters, body and tags', () => {
|
|
55
|
+
const registry = createApiRegistry();
|
|
56
|
+
const getEndpoint = yaxaEndpoint({
|
|
57
|
+
summary: 'Get Item By ID',
|
|
58
|
+
description: 'Retrieves an item record by its unique ID',
|
|
59
|
+
tags: ['Items'],
|
|
60
|
+
schema: {
|
|
61
|
+
params: ItemParamsSchema,
|
|
62
|
+
query: ItemQuerySchema
|
|
63
|
+
},
|
|
64
|
+
handle: ({ params }) => ({ id: params.id })
|
|
65
|
+
});
|
|
66
|
+
const postEndpoint = yaxaEndpoint({
|
|
67
|
+
summary: 'Create Item',
|
|
68
|
+
tags: ['Items'],
|
|
69
|
+
schema: {
|
|
70
|
+
body: CreateItemBodySchema
|
|
71
|
+
},
|
|
72
|
+
handle: ({ body }) => body
|
|
73
|
+
});
|
|
74
|
+
registry.register('/api/items/[id]', { GET: getEndpoint });
|
|
75
|
+
registry.register('/api/items', { POST: postEndpoint });
|
|
76
|
+
const spec = generateOpenApiSpec({
|
|
77
|
+
title: 'Inventory API',
|
|
78
|
+
version: '2.0.0',
|
|
79
|
+
description: 'Test Inventory System',
|
|
80
|
+
registry
|
|
81
|
+
});
|
|
82
|
+
expect(spec.openapi).toBe('3.1.0');
|
|
83
|
+
expect(spec.info.title).toBe('Inventory API');
|
|
84
|
+
expect(spec.info.version).toBe('2.0.0');
|
|
85
|
+
// Verify GET /api/items/{id}
|
|
86
|
+
const getOp = spec.paths['/api/items/{id}']['get'];
|
|
87
|
+
expect(getOp.summary).toBe('Get Item By ID');
|
|
88
|
+
expect(getOp.tags).toEqual(['Items']);
|
|
89
|
+
expect(getOp.parameters).toHaveLength(3); // id (path), filter (query), limit (query)
|
|
90
|
+
const idParam = getOp.parameters?.find((p) => p.name === 'id');
|
|
91
|
+
expect(idParam?.in).toBe('path');
|
|
92
|
+
expect(idParam?.required).toBe(true);
|
|
93
|
+
// Verify POST /api/items
|
|
94
|
+
const postOp = spec.paths['/api/items']['post'];
|
|
95
|
+
expect(postOp.summary).toBe('Create Item');
|
|
96
|
+
expect(postOp.requestBody).toBeDefined();
|
|
97
|
+
expect(postOp.requestBody?.content['application/json'].schema?.type).toBe('object');
|
|
98
|
+
expect(postOp.responses['201']).toBeDefined();
|
|
99
|
+
expect(postOp.responses['422']).toBeDefined(); // Schema validation failed
|
|
100
|
+
});
|
|
101
|
+
it('deduces security schemes and RFC 7807 responses from guards automatically', () => {
|
|
102
|
+
const registry = createApiRegistry();
|
|
103
|
+
const secureEndpoint = yaxaEndpoint({
|
|
104
|
+
guards: [
|
|
105
|
+
requireAuth(),
|
|
106
|
+
requireRole(['admin', 'billing_manager']),
|
|
107
|
+
rateLimit({ points: 15, windowSeconds: 60 })
|
|
108
|
+
],
|
|
109
|
+
schema: {
|
|
110
|
+
body: CreateItemBodySchema
|
|
111
|
+
},
|
|
112
|
+
handle: () => ({ ok: true })
|
|
113
|
+
});
|
|
114
|
+
registry.register('/api/secure-action', { POST: secureEndpoint });
|
|
115
|
+
const spec = generateOpenApiSpec({ registry });
|
|
116
|
+
const op = spec.paths['/api/secure-action']['post'];
|
|
117
|
+
// Security schemes attached
|
|
118
|
+
expect(spec.components?.securitySchemes?.BearerAuth).toBeDefined();
|
|
119
|
+
expect(spec.components?.securitySchemes?.CookieAuth).toBeDefined();
|
|
120
|
+
expect(op.security).toBeDefined();
|
|
121
|
+
// Guard error responses generated
|
|
122
|
+
expect(op.responses['401']).toBeDefined();
|
|
123
|
+
expect(op.responses['403']).toBeDefined();
|
|
124
|
+
expect(op.responses['403'].description).toContain('admin, billing_manager');
|
|
125
|
+
expect(op.responses['422']).toBeDefined();
|
|
126
|
+
expect(op.responses['429']).toBeDefined();
|
|
127
|
+
expect(op.responses['429'].headers?.['Retry-After']).toBeDefined();
|
|
128
|
+
expect(op.responses['500']).toBeDefined();
|
|
129
|
+
// Components schema ProblemDetails registered
|
|
130
|
+
expect(spec.components?.schemas?.ProblemDetails).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
it('supports self-registration via yaxaEndpoint path and method metadata', () => {
|
|
133
|
+
globalApiRegistry.clear();
|
|
134
|
+
yaxaEndpoint({
|
|
135
|
+
path: '/api/v1/auto-registered',
|
|
136
|
+
method: 'GET',
|
|
137
|
+
summary: 'Auto Registered Route',
|
|
138
|
+
handle: () => 'ok'
|
|
139
|
+
});
|
|
140
|
+
const spec = generateOpenApiSpec();
|
|
141
|
+
expect(spec.paths['/api/v1/auto-registered']?.['get']?.summary).toBe('Auto Registered Route');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('createApiDocsHandler Endpoint', () => {
|
|
145
|
+
it('serves OpenAPI 3.1 JSON when format=json query parameter is present', async () => {
|
|
146
|
+
const handler = createApiDocsHandler({
|
|
147
|
+
title: 'Store API',
|
|
148
|
+
version: '1.2.3'
|
|
149
|
+
});
|
|
150
|
+
const event = createMockRequestEvent({
|
|
151
|
+
url: 'http://localhost:5173/api/docs?format=json'
|
|
152
|
+
});
|
|
153
|
+
const response = await handler(event);
|
|
154
|
+
expect(response.status).toBe(200);
|
|
155
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
156
|
+
const json = await response.json();
|
|
157
|
+
expect(json.openapi).toBe('3.1.0');
|
|
158
|
+
expect(json.info.title).toBe('Store API');
|
|
159
|
+
expect(json.info.version).toBe('1.2.3');
|
|
160
|
+
});
|
|
161
|
+
it('serves OpenAPI 3.1 JSON when Accept header requests application/json', async () => {
|
|
162
|
+
const handler = createApiDocsHandler();
|
|
163
|
+
const event = createMockRequestEvent({
|
|
164
|
+
url: 'http://localhost:5173/api/docs',
|
|
165
|
+
headers: { accept: 'application/json' }
|
|
166
|
+
});
|
|
167
|
+
const response = await handler(event);
|
|
168
|
+
expect(response.status).toBe(200);
|
|
169
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
170
|
+
});
|
|
171
|
+
it('renders Scalar HTML documentation by default', async () => {
|
|
172
|
+
const handler = createApiDocsHandler({
|
|
173
|
+
title: 'Scalar Docs Test',
|
|
174
|
+
theme: 'scalar'
|
|
175
|
+
});
|
|
176
|
+
const event = createMockRequestEvent({
|
|
177
|
+
url: 'http://localhost:5173/api/docs'
|
|
178
|
+
});
|
|
179
|
+
const response = await handler(event);
|
|
180
|
+
expect(response.status).toBe(200);
|
|
181
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
182
|
+
const html = await response.text();
|
|
183
|
+
expect(html).toContain('<title>Scalar Docs Test</title>');
|
|
184
|
+
expect(html).toContain('@scalar/api-reference');
|
|
185
|
+
expect(html).toContain('id="api-reference"');
|
|
186
|
+
});
|
|
187
|
+
it('renders Swagger UI HTML when theme is swagger', async () => {
|
|
188
|
+
const handler = createApiDocsHandler({
|
|
189
|
+
title: 'Swagger Docs Test',
|
|
190
|
+
theme: 'swagger'
|
|
191
|
+
});
|
|
192
|
+
const event = createMockRequestEvent({
|
|
193
|
+
url: 'http://localhost:5173/api/docs'
|
|
194
|
+
});
|
|
195
|
+
const response = await handler(event);
|
|
196
|
+
expect(response.status).toBe(200);
|
|
197
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
198
|
+
const html = await response.text();
|
|
199
|
+
expect(html).toContain('<title>Swagger Docs Test</title>');
|
|
200
|
+
expect(html).toContain('swagger-ui-dist');
|
|
201
|
+
expect(html).toContain('SwaggerUIBundle');
|
|
202
|
+
});
|
|
203
|
+
it('renders Redoc HTML when theme is redoc', async () => {
|
|
204
|
+
const handler = createApiDocsHandler({
|
|
205
|
+
title: 'Redoc Docs Test',
|
|
206
|
+
theme: 'redoc'
|
|
207
|
+
});
|
|
208
|
+
const event = createMockRequestEvent({
|
|
209
|
+
url: 'http://localhost:5173/api/docs'
|
|
210
|
+
});
|
|
211
|
+
const response = await handler(event);
|
|
212
|
+
expect(response.status).toBe(200);
|
|
213
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
214
|
+
const html = await response.text();
|
|
215
|
+
expect(html).toContain('<title>Redoc Docs Test</title>');
|
|
216
|
+
expect(html).toContain('redoc.standalone.js');
|
|
217
|
+
expect(html).toContain('redoc-container');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EndpointLike, EndpointMap } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Route & Endpoint Registry for OpenAPI discovery and introspection.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ApiRegistry {
|
|
6
|
+
private endpoints;
|
|
7
|
+
/**
|
|
8
|
+
* Registers endpoints for a given path.
|
|
9
|
+
* @param path SvelteKit route path (e.g. '/api/projects' or '/api/projects/[id]')
|
|
10
|
+
* @param methods Map of HTTP methods to handlers or yaxaEndpoint configs
|
|
11
|
+
*/
|
|
12
|
+
register(path: string, methods: Record<string, EndpointLike>): this;
|
|
13
|
+
/**
|
|
14
|
+
* Retrieves all registered endpoints.
|
|
15
|
+
*/
|
|
16
|
+
getEndpoints(): EndpointMap;
|
|
17
|
+
/**
|
|
18
|
+
* Clears registered endpoints (useful in testing).
|
|
19
|
+
*/
|
|
20
|
+
clear(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Normalizes SvelteKit path parameters to OpenAPI format:
|
|
23
|
+
* '/api/projects/[id]' -> '/api/projects/{id}'
|
|
24
|
+
* '/api/teams/[orgSlug]/members/[userId]' -> '/api/teams/{orgSlug}/members/{userId}'
|
|
25
|
+
*/
|
|
26
|
+
normalizePath(path: string): string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Global singleton API registry for zero-config route registration.
|
|
30
|
+
*/
|
|
31
|
+
export declare const globalApiRegistry: ApiRegistry;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new isolated API registry instance.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createApiRegistry(): ApiRegistry;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route & Endpoint Registry for OpenAPI discovery and introspection.
|
|
3
|
+
*/
|
|
4
|
+
export class ApiRegistry {
|
|
5
|
+
endpoints = {};
|
|
6
|
+
/**
|
|
7
|
+
* Registers endpoints for a given path.
|
|
8
|
+
* @param path SvelteKit route path (e.g. '/api/projects' or '/api/projects/[id]')
|
|
9
|
+
* @param methods Map of HTTP methods to handlers or yaxaEndpoint configs
|
|
10
|
+
*/
|
|
11
|
+
register(path, methods) {
|
|
12
|
+
const normalizedPath = this.normalizePath(path);
|
|
13
|
+
if (!this.endpoints[normalizedPath]) {
|
|
14
|
+
this.endpoints[normalizedPath] = {};
|
|
15
|
+
}
|
|
16
|
+
for (const [method, handler] of Object.entries(methods)) {
|
|
17
|
+
this.endpoints[normalizedPath][method.toUpperCase()] = handler;
|
|
18
|
+
}
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Retrieves all registered endpoints.
|
|
23
|
+
*/
|
|
24
|
+
getEndpoints() {
|
|
25
|
+
return this.endpoints;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Clears registered endpoints (useful in testing).
|
|
29
|
+
*/
|
|
30
|
+
clear() {
|
|
31
|
+
this.endpoints = {};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Normalizes SvelteKit path parameters to OpenAPI format:
|
|
35
|
+
* '/api/projects/[id]' -> '/api/projects/{id}'
|
|
36
|
+
* '/api/teams/[orgSlug]/members/[userId]' -> '/api/teams/{orgSlug}/members/{userId}'
|
|
37
|
+
*/
|
|
38
|
+
normalizePath(path) {
|
|
39
|
+
return path.replace(/\[\.\.\.(\w+)\]/g, '{$1}').replace(/\[(\w+)\]/g, '{$1}');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Global singleton API registry for zero-config route registration.
|
|
44
|
+
*/
|
|
45
|
+
export const globalApiRegistry = new ApiRegistry();
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new isolated API registry instance.
|
|
48
|
+
*/
|
|
49
|
+
export function createApiRegistry() {
|
|
50
|
+
return new ApiRegistry();
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a Zod or Standard Schema validator into a compliant OpenAPI 3.1 JSON Schema.
|
|
4
|
+
*/
|
|
5
|
+
export function convertToJsonSchema(validator) {
|
|
6
|
+
if (!validator) {
|
|
7
|
+
return { type: 'object' };
|
|
8
|
+
}
|
|
9
|
+
// 1. Zod native JSON schema converter
|
|
10
|
+
if (typeof z?.toJSONSchema === 'function') {
|
|
11
|
+
try {
|
|
12
|
+
const jsonSchema = z.toJSONSchema(validator);
|
|
13
|
+
if (jsonSchema && typeof jsonSchema === 'object') {
|
|
14
|
+
const cleanSchema = { ...jsonSchema };
|
|
15
|
+
delete cleanSchema.$schema;
|
|
16
|
+
return cleanSchema;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fall through to other strategies
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 2. Check if validator has built-in jsonSchema method/property
|
|
24
|
+
if (typeof validator.jsonSchema === 'function') {
|
|
25
|
+
try {
|
|
26
|
+
const res = validator.jsonSchema();
|
|
27
|
+
const cleanSchema = { ...res };
|
|
28
|
+
delete cleanSchema.$schema;
|
|
29
|
+
return cleanSchema;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Fall through
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (typeof validator.jsonSchema === 'object' && validator.jsonSchema !== null) {
|
|
36
|
+
const cleanSchema = { ...validator.jsonSchema };
|
|
37
|
+
delete cleanSchema.$schema;
|
|
38
|
+
return cleanSchema;
|
|
39
|
+
}
|
|
40
|
+
// 3. Heuristic for common object schema shapes
|
|
41
|
+
if (typeof validator === 'object' && validator !== null) {
|
|
42
|
+
if ('shape' in validator && typeof validator.shape === 'object') {
|
|
43
|
+
const properties = {};
|
|
44
|
+
const required = [];
|
|
45
|
+
for (const [key, val] of Object.entries(validator.shape)) {
|
|
46
|
+
properties[key] = convertToJsonSchema(val);
|
|
47
|
+
if (!val?.isOptional?.()) {
|
|
48
|
+
required.push(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties,
|
|
54
|
+
...(required.length > 0 ? { required } : {})
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { type: 'string' };
|
|
59
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import type { YaxaEndpointConfig } from '../endpoint/types';
|
|
3
|
+
/**
|
|
4
|
+
* OpenAPI 3.1 Parameter Object
|
|
5
|
+
*/
|
|
6
|
+
export interface OpenApiParameter {
|
|
7
|
+
name: string;
|
|
8
|
+
in: 'query' | 'header' | 'path' | 'cookie';
|
|
9
|
+
description?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
deprecated?: boolean;
|
|
12
|
+
schema?: Record<string, any>;
|
|
13
|
+
example?: any;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* OpenAPI 3.1 Request Body Object
|
|
17
|
+
*/
|
|
18
|
+
export interface OpenApiRequestBody {
|
|
19
|
+
description?: string;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
content: {
|
|
22
|
+
[mediaType: string]: {
|
|
23
|
+
schema?: Record<string, any>;
|
|
24
|
+
example?: any;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* OpenAPI 3.1 Response Object
|
|
30
|
+
*/
|
|
31
|
+
export interface OpenApiResponse {
|
|
32
|
+
description: string;
|
|
33
|
+
headers?: Record<string, any>;
|
|
34
|
+
content?: {
|
|
35
|
+
[mediaType: string]: {
|
|
36
|
+
schema?: Record<string, any>;
|
|
37
|
+
example?: any;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* OpenAPI 3.1 Operation Object
|
|
43
|
+
*/
|
|
44
|
+
export interface OpenApiOperation {
|
|
45
|
+
summary?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
operationId?: string;
|
|
48
|
+
tags?: string[];
|
|
49
|
+
parameters?: OpenApiParameter[];
|
|
50
|
+
requestBody?: OpenApiRequestBody;
|
|
51
|
+
responses: Record<string, OpenApiResponse>;
|
|
52
|
+
deprecated?: boolean;
|
|
53
|
+
security?: Array<Record<string, string[]>>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OpenAPI 3.1 Specification Object
|
|
57
|
+
*/
|
|
58
|
+
export interface OpenApiSpec31 {
|
|
59
|
+
openapi: '3.1.0';
|
|
60
|
+
info: {
|
|
61
|
+
title: string;
|
|
62
|
+
version: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
termsOfService?: string;
|
|
65
|
+
contact?: {
|
|
66
|
+
name?: string;
|
|
67
|
+
url?: string;
|
|
68
|
+
email?: string;
|
|
69
|
+
};
|
|
70
|
+
license?: {
|
|
71
|
+
name: string;
|
|
72
|
+
url?: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
servers?: Array<{
|
|
76
|
+
url: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
}>;
|
|
79
|
+
paths: {
|
|
80
|
+
[path: string]: {
|
|
81
|
+
[method: string]: OpenApiOperation;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
components?: {
|
|
85
|
+
schemas?: Record<string, any>;
|
|
86
|
+
securitySchemes?: Record<string, any>;
|
|
87
|
+
responses?: Record<string, OpenApiResponse>;
|
|
88
|
+
parameters?: Record<string, OpenApiParameter>;
|
|
89
|
+
};
|
|
90
|
+
tags?: Array<{
|
|
91
|
+
name: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
export type EndpointLike = (RequestHandler & {
|
|
96
|
+
_yaxaConfig?: YaxaEndpointConfig;
|
|
97
|
+
}) | YaxaEndpointConfig;
|
|
98
|
+
export type EndpointMap = Record<string, Record<string, EndpointLike>>;
|
|
99
|
+
export interface GenerateOpenApiOptions {
|
|
100
|
+
title?: string;
|
|
101
|
+
version?: string;
|
|
102
|
+
description?: string;
|
|
103
|
+
servers?: Array<{
|
|
104
|
+
url: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
}>;
|
|
107
|
+
endpoints?: EndpointMap;
|
|
108
|
+
registry?: any;
|
|
109
|
+
}
|
|
110
|
+
export type ApiDocsTheme = 'scalar' | 'swagger' | 'redoc';
|
|
111
|
+
export interface ApiDocsOptions extends GenerateOpenApiOptions {
|
|
112
|
+
/**
|
|
113
|
+
* Interactive UI theme:
|
|
114
|
+
* - 'scalar' (default): Modern, dark-mode native, high contrast developer reference.
|
|
115
|
+
* - 'swagger': Classic Swagger UI with interactive Try-It-Out console.
|
|
116
|
+
* - 'redoc': Clean 3-panel technical documentation format.
|
|
117
|
+
*/
|
|
118
|
+
theme?: ApiDocsTheme;
|
|
119
|
+
/**
|
|
120
|
+
* Mount path of documentation endpoint (default: '/api/docs').
|
|
121
|
+
*/
|
|
122
|
+
docsPath?: string;
|
|
123
|
+
/**
|
|
124
|
+
* CDN version/URL overrides if needed.
|
|
125
|
+
*/
|
|
126
|
+
cdnUrl?: string;
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaxa-svelte",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "The Intuitive Svelte UI Library — Nuxt UI v4 equivalent for SvelteKit 2 & 3 & Svelte 5 with automated SEO, dynamic OpenGraph cards, sitemaps, robots.txt, and Tailwind CSS v4.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://yaxa.vercel.app",
|