@velajs/vela 0.8.0 → 0.10.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/dist/application.d.ts +12 -0
- package/dist/application.js +22 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/openapi/decorators.d.ts +15 -1
- package/dist/openapi/decorators.js +24 -0
- package/dist/openapi/document.js +99 -18
- package/dist/openapi/index.d.ts +3 -2
- package/dist/openapi/index.js +2 -1
- package/dist/openapi/scalar-ui.d.ts +1 -0
- package/dist/openapi/scalar-ui.js +18 -0
- package/dist/openapi/types.d.ts +18 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/logger.d.ts +28 -0
- package/dist/services/logger.js +97 -32
- package/package.json +1 -1
package/dist/application.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Hono } from 'hono';
|
|
|
2
2
|
import type { Container } from './container/container';
|
|
3
3
|
import type { Token } from './container/types';
|
|
4
4
|
import type { RouteManager } from './http/route.manager';
|
|
5
|
+
import type { MountOpenApiOptions } from './openapi/types';
|
|
5
6
|
import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
|
|
6
7
|
export declare class VelaApplication {
|
|
7
8
|
private readonly container;
|
|
@@ -22,6 +23,17 @@ export declare class VelaApplication {
|
|
|
22
23
|
useGlobalGuards(...guards: GuardType[]): this;
|
|
23
24
|
useGlobalInterceptors(...interceptors: InterceptorType[]): this;
|
|
24
25
|
useGlobalFilters(...filters: FilterType[]): this;
|
|
26
|
+
/**
|
|
27
|
+
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on
|
|
28
|
+
* the underlying Hono app. Edge-safe: the UI HTML loads Scalar from a
|
|
29
|
+
* CDN at runtime so nothing is bundled server-side.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const doc = createOpenApiDocument(AppModule);
|
|
33
|
+
* app.mountOpenApi({ document: doc, ui: 'scalar' });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
mountOpenApi(options: MountOpenApiOptions): this;
|
|
25
37
|
callOnModuleInit(): Promise<void>;
|
|
26
38
|
callOnApplicationBootstrap(): Promise<void>;
|
|
27
39
|
close(signal?: string): Promise<void>;
|
package/dist/application.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasBeforeApplicationShutdown, hasOnApplicationBootstrap, hasOnApplicationShutdown, hasOnModuleDestroy, hasOnModuleInit } from "./lifecycle/index.js";
|
|
2
|
+
import { renderScalarUi } from "./openapi/scalar-ui.js";
|
|
2
3
|
export class VelaApplication {
|
|
3
4
|
container;
|
|
4
5
|
routeManager;
|
|
@@ -52,6 +53,27 @@ export class VelaApplication {
|
|
|
52
53
|
this.routeManager.useGlobalFilters(...filters);
|
|
53
54
|
return this;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Serve a pre-built OpenAPI document (and optionally a Scalar UI) on
|
|
58
|
+
* the underlying Hono app. Edge-safe: the UI HTML loads Scalar from a
|
|
59
|
+
* CDN at runtime so nothing is bundled server-side.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const doc = createOpenApiDocument(AppModule);
|
|
63
|
+
* app.mountOpenApi({ document: doc, ui: 'scalar' });
|
|
64
|
+
* ```
|
|
65
|
+
*/ mountOpenApi(options) {
|
|
66
|
+
const app = this.getApp();
|
|
67
|
+
const jsonPath = options.path ?? '/docs.json';
|
|
68
|
+
const doc = options.document;
|
|
69
|
+
app.get(jsonPath, (c)=>c.json(doc));
|
|
70
|
+
if (options.ui === 'scalar') {
|
|
71
|
+
const uiPath = options.uiPath ?? '/docs';
|
|
72
|
+
const html = renderScalarUi(jsonPath);
|
|
73
|
+
app.get(uiPath, (c)=>c.html(html));
|
|
74
|
+
}
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
55
77
|
// Lifecycle hooks
|
|
56
78
|
async callOnModuleInit() {
|
|
57
79
|
for (const instance of this.instances){
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ import './metadata';
|
|
|
2
2
|
export { defineMetadata, getMetadata } from './metadata';
|
|
3
3
|
export { VelaFactory, createApplication } from './factory';
|
|
4
4
|
export { VelaApplication } from './application';
|
|
5
|
-
export { createOpenApiDocument, ApiDoc, ApiTags, zodToJsonSchema } from './openapi/index';
|
|
6
|
-
export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
|
|
5
|
+
export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema, } from './openapi/index';
|
|
6
|
+
export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, } from './openapi/index';
|
|
7
7
|
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from './container/index';
|
|
8
8
|
export type { Type, Token, InjectableOptions, ProviderOptions, } from './container/index';
|
|
9
9
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
|
|
10
10
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, } from './http/index';
|
|
11
11
|
export { Logger, LogLevel } from './services/index';
|
|
12
|
-
export type { LoggerService } from './services/index';
|
|
12
|
+
export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
|
|
13
13
|
export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
|
|
14
14
|
export type { ConfigModuleOptions } from './config/index';
|
|
15
15
|
export { HttpModule, HttpService, HTTP_MODULE_OPTIONS, HttpRequestException } from './fetch/index';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { defineMetadata, getMetadata } from "./metadata.js";
|
|
|
4
4
|
export { VelaFactory, createApplication } from "./factory.js";
|
|
5
5
|
export { VelaApplication } from "./application.js";
|
|
6
6
|
// OpenAPI
|
|
7
|
-
export { createOpenApiDocument, ApiDoc, ApiTags, zodToJsonSchema } from "./openapi/index.js";
|
|
7
|
+
export { createOpenApiDocument, ApiDoc, ApiTags, ApiResponse, zodToJsonSchema } from "./openapi/index.js";
|
|
8
8
|
// DI Container
|
|
9
9
|
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, mixin } from "./container/index.js";
|
|
10
10
|
// Constants
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ApiDocMetadata } from './types';
|
|
1
|
+
import type { ApiDocMetadata, ApiResponseEntry, ApiResponseOptions } from './types';
|
|
2
2
|
export declare const API_DOC_METADATA = "vela:openapi:doc";
|
|
3
3
|
export declare const API_TAGS_METADATA = "vela:openapi:tags";
|
|
4
|
+
export declare const API_RESPONSES_METADATA = "vela:openapi:responses";
|
|
4
5
|
/**
|
|
5
6
|
* Attach OpenAPI documentation to a route handler (or controller).
|
|
6
7
|
*
|
|
@@ -18,3 +19,16 @@ export declare function ApiDoc(metadata: ApiDocMetadata): MethodDecorator & Clas
|
|
|
18
19
|
export declare function ApiTags(...tags: string[]): MethodDecorator & ClassDecorator;
|
|
19
20
|
export declare function getApiDoc(target: object, propertyKey?: string | symbol): ApiDocMetadata | undefined;
|
|
20
21
|
export declare function getApiTags(target: object, propertyKey?: string | symbol): string[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Document a response for a given status code. Stackable: apply multiple
|
|
24
|
+
* times on the same handler to declare different statuses.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* @Get('/:id')
|
|
28
|
+
* @ApiResponse(200, { description: 'Found', schema: UserDto })
|
|
29
|
+
* @ApiResponse(404, { description: 'Not found', schema: ErrorDto })
|
|
30
|
+
* findOne() { ... }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function ApiResponse(status: number | string, options: ApiResponseOptions): MethodDecorator;
|
|
34
|
+
export declare function getApiResponses(target: object, propertyKey: string | symbol): ApiResponseEntry[] | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const API_DOC_METADATA = 'vela:openapi:doc';
|
|
2
2
|
export const API_TAGS_METADATA = 'vela:openapi:tags';
|
|
3
|
+
export const API_RESPONSES_METADATA = 'vela:openapi:responses';
|
|
3
4
|
/**
|
|
4
5
|
* Attach OpenAPI documentation to a route handler (or controller).
|
|
5
6
|
*
|
|
@@ -35,3 +36,26 @@ export function getApiDoc(target, propertyKey) {
|
|
|
35
36
|
export function getApiTags(target, propertyKey) {
|
|
36
37
|
return propertyKey !== undefined ? Reflect.getMetadata(API_TAGS_METADATA, target, propertyKey) : Reflect.getMetadata(API_TAGS_METADATA, target);
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Document a response for a given status code. Stackable: apply multiple
|
|
41
|
+
* times on the same handler to declare different statuses.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* @Get('/:id')
|
|
45
|
+
* @ApiResponse(200, { description: 'Found', schema: UserDto })
|
|
46
|
+
* @ApiResponse(404, { description: 'Not found', schema: ErrorDto })
|
|
47
|
+
* findOne() { ... }
|
|
48
|
+
* ```
|
|
49
|
+
*/ export function ApiResponse(status, options) {
|
|
50
|
+
return (target, propertyKey)=>{
|
|
51
|
+
const existing = Reflect.getMetadata(API_RESPONSES_METADATA, target, propertyKey) ?? [];
|
|
52
|
+
existing.push({
|
|
53
|
+
status,
|
|
54
|
+
...options
|
|
55
|
+
});
|
|
56
|
+
Reflect.defineMetadata(API_RESPONSES_METADATA, existing, target, propertyKey);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function getApiResponses(target, propertyKey) {
|
|
60
|
+
return Reflect.getMetadata(API_RESPONSES_METADATA, target, propertyKey);
|
|
61
|
+
}
|
package/dist/openapi/document.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getModuleMetadata } from "../module/decorators.js";
|
|
|
2
2
|
import { ForwardRef } from "../container/types.js";
|
|
3
3
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
4
4
|
import { ParamType } from "../constants.js";
|
|
5
|
-
import { getApiDoc, getApiTags } from "./decorators.js";
|
|
5
|
+
import { getApiDoc, getApiResponses, getApiTags } from "./decorators.js";
|
|
6
6
|
import { isOptional, zodToJsonSchema } from "./zod-to-json-schema.js";
|
|
7
7
|
function isDynamicModuleLike(v) {
|
|
8
8
|
return !!v && typeof v === 'object' && 'module' in v && typeof v.module === 'function';
|
|
@@ -31,7 +31,6 @@ function collectControllers(rootModule) {
|
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
function normalizePath(path) {
|
|
34
|
-
// Hono/Nest style `:id` → OpenAPI `{id}`
|
|
35
34
|
return path.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');
|
|
36
35
|
}
|
|
37
36
|
function joinPath(a, b) {
|
|
@@ -40,10 +39,47 @@ function joinPath(a, b) {
|
|
|
40
39
|
const joined = `${left}${right}`;
|
|
41
40
|
return joined || '/';
|
|
42
41
|
}
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Tracks DTO classes referenced during document generation and registers
|
|
44
|
+
* each under `components.schemas`. Handles name collisions by suffixing.
|
|
45
|
+
*/ let ComponentsRegistry = class ComponentsRegistry {
|
|
46
|
+
schemas = new Map();
|
|
47
|
+
classToKey = new WeakMap();
|
|
48
|
+
ref(dtoClass) {
|
|
49
|
+
const existing = this.classToKey.get(dtoClass);
|
|
50
|
+
if (existing) return {
|
|
51
|
+
$ref: `#/components/schemas/${existing}`
|
|
52
|
+
};
|
|
53
|
+
const baseName = dtoClass.name && dtoClass.name !== '' ? dtoClass.name : 'Schema';
|
|
54
|
+
let key = baseName;
|
|
55
|
+
let counter = 2;
|
|
56
|
+
while(this.schemas.has(key)){
|
|
57
|
+
key = `${baseName}${counter++}`;
|
|
58
|
+
}
|
|
59
|
+
this.classToKey.set(dtoClass, key);
|
|
60
|
+
const staticSchema = dtoClass.schema;
|
|
61
|
+
this.schemas.set(key, zodToJsonSchema(staticSchema));
|
|
62
|
+
return {
|
|
63
|
+
$ref: `#/components/schemas/${key}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
build() {
|
|
67
|
+
if (this.schemas.size === 0) return undefined;
|
|
68
|
+
return Object.fromEntries(this.schemas);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function isDtoClass(value) {
|
|
72
|
+
return typeof value === 'function' && value.schema != null;
|
|
73
|
+
}
|
|
74
|
+
function getParamSchema(param, paramtypes, registry) {
|
|
44
75
|
const metatype = paramtypes?.[param.index];
|
|
45
|
-
if (metatype
|
|
46
|
-
|
|
76
|
+
if (!metatype) return undefined;
|
|
77
|
+
if (isDtoClass(metatype)) {
|
|
78
|
+
return registry.ref(metatype);
|
|
79
|
+
}
|
|
80
|
+
const maybeSchema = metatype.schema;
|
|
81
|
+
if (maybeSchema) {
|
|
82
|
+
return zodToJsonSchema(maybeSchema);
|
|
47
83
|
}
|
|
48
84
|
return undefined;
|
|
49
85
|
}
|
|
@@ -54,13 +90,33 @@ function isParamOptional(param, paramtypes) {
|
|
|
54
90
|
}
|
|
55
91
|
return true;
|
|
56
92
|
}
|
|
57
|
-
function
|
|
93
|
+
function isLikelyJsonSchema(value) {
|
|
94
|
+
if (!value || typeof value !== 'object') return false;
|
|
95
|
+
const v = value;
|
|
96
|
+
if (typeof v.toJSONSchema === 'function') return false;
|
|
97
|
+
return 'type' in v || '$ref' in v || 'oneOf' in v || 'anyOf' in v || 'allOf' in v || 'enum' in v || 'const' in v;
|
|
98
|
+
}
|
|
99
|
+
function resolveResponseSchema(input, registry) {
|
|
100
|
+
if (input === undefined || input === null) return undefined;
|
|
101
|
+
if (isDtoClass(input)) {
|
|
102
|
+
return registry.ref(input);
|
|
103
|
+
}
|
|
104
|
+
if (isLikelyJsonSchema(input)) {
|
|
105
|
+
return {
|
|
106
|
+
...input
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (typeof input === 'object' && typeof input.toJSONSchema === 'function') {
|
|
110
|
+
return zodToJsonSchema(input);
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
function buildOperation(controller, route, pathString, registry) {
|
|
58
115
|
const handlerName = route.handlerName;
|
|
59
116
|
const paramMetadata = MetadataRegistry.getParameters(controller).get(handlerName) ?? [];
|
|
60
117
|
const paramtypes = Reflect.getMetadata('design:paramtypes', controller.prototype, handlerName);
|
|
61
118
|
const parameters = [];
|
|
62
119
|
let requestBody;
|
|
63
|
-
// Path params declared in the URL but not listed as @Param() get a string default.
|
|
64
120
|
const declaredPathParams = new Set();
|
|
65
121
|
const pathParamNames = [
|
|
66
122
|
...pathString.matchAll(/\{([^}]+)\}/g)
|
|
@@ -72,7 +128,7 @@ function buildOperation(controller, route, pathString) {
|
|
|
72
128
|
name: param.name,
|
|
73
129
|
in: 'path',
|
|
74
130
|
required: true,
|
|
75
|
-
schema: getParamSchema(param, paramtypes) ?? {
|
|
131
|
+
schema: getParamSchema(param, paramtypes, registry) ?? {
|
|
76
132
|
type: 'string'
|
|
77
133
|
}
|
|
78
134
|
});
|
|
@@ -81,7 +137,7 @@ function buildOperation(controller, route, pathString) {
|
|
|
81
137
|
name: param.name,
|
|
82
138
|
in: 'query',
|
|
83
139
|
required: !isParamOptional(param, paramtypes),
|
|
84
|
-
schema: getParamSchema(param, paramtypes) ?? {
|
|
140
|
+
schema: getParamSchema(param, paramtypes, registry) ?? {
|
|
85
141
|
type: 'string'
|
|
86
142
|
}
|
|
87
143
|
});
|
|
@@ -90,12 +146,12 @@ function buildOperation(controller, route, pathString) {
|
|
|
90
146
|
name: param.name,
|
|
91
147
|
in: 'header',
|
|
92
148
|
required: !isParamOptional(param, paramtypes),
|
|
93
|
-
schema: getParamSchema(param, paramtypes) ?? {
|
|
149
|
+
schema: getParamSchema(param, paramtypes, registry) ?? {
|
|
94
150
|
type: 'string'
|
|
95
151
|
}
|
|
96
152
|
});
|
|
97
153
|
} else if (param.type === ParamType.BODY) {
|
|
98
|
-
const schema = getParamSchema(param, paramtypes);
|
|
154
|
+
const schema = getParamSchema(param, paramtypes, registry);
|
|
99
155
|
if (schema) {
|
|
100
156
|
requestBody = {
|
|
101
157
|
required: !isParamOptional(param, paramtypes),
|
|
@@ -131,12 +187,29 @@ function buildOperation(controller, route, pathString) {
|
|
|
131
187
|
...docTags
|
|
132
188
|
])
|
|
133
189
|
];
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
'
|
|
137
|
-
|
|
138
|
-
|
|
190
|
+
const responses = {
|
|
191
|
+
'200': {
|
|
192
|
+
description: 'OK'
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const apiResponses = getApiResponses(controller.prototype, handlerName) ?? [];
|
|
196
|
+
for (const entry of apiResponses){
|
|
197
|
+
const key = String(entry.status);
|
|
198
|
+
const resolved = {
|
|
199
|
+
description: entry.description
|
|
200
|
+
};
|
|
201
|
+
const schema = resolveResponseSchema(entry.schema, registry);
|
|
202
|
+
if (schema) {
|
|
203
|
+
resolved.content = {
|
|
204
|
+
'application/json': {
|
|
205
|
+
schema
|
|
206
|
+
}
|
|
207
|
+
};
|
|
139
208
|
}
|
|
209
|
+
responses[key] = resolved;
|
|
210
|
+
}
|
|
211
|
+
const operation = {
|
|
212
|
+
responses
|
|
140
213
|
};
|
|
141
214
|
if (parameters.length > 0) operation.parameters = parameters;
|
|
142
215
|
if (requestBody) operation.requestBody = requestBody;
|
|
@@ -159,6 +232,7 @@ const VERB_WHITELIST = new Set([
|
|
|
159
232
|
export function createOpenApiDocument(rootModule, options = {}) {
|
|
160
233
|
const paths = {};
|
|
161
234
|
const globalPrefix = options.globalPrefix ?? '';
|
|
235
|
+
const registry = new ComponentsRegistry();
|
|
162
236
|
for (const controller of collectControllers(rootModule)){
|
|
163
237
|
const controllerPath = MetadataRegistry.getControllerPath(controller);
|
|
164
238
|
const routes = MetadataRegistry.getRoutes(controller);
|
|
@@ -167,13 +241,13 @@ export function createOpenApiDocument(rootModule, options = {}) {
|
|
|
167
241
|
if (!VERB_WHITELIST.has(method)) continue;
|
|
168
242
|
const rawPath = joinPath(joinPath(globalPrefix, controllerPath), route.path);
|
|
169
243
|
const pathString = normalizePath(rawPath);
|
|
170
|
-
const operation = buildOperation(controller, route, pathString);
|
|
244
|
+
const operation = buildOperation(controller, route, pathString, registry);
|
|
171
245
|
const pathItem = paths[pathString] ?? {};
|
|
172
246
|
pathItem[method] = operation;
|
|
173
247
|
paths[pathString] = pathItem;
|
|
174
248
|
}
|
|
175
249
|
}
|
|
176
|
-
|
|
250
|
+
const document = {
|
|
177
251
|
openapi: '3.1.0',
|
|
178
252
|
info: {
|
|
179
253
|
title: options.info?.title ?? 'Vela API',
|
|
@@ -184,4 +258,11 @@ export function createOpenApiDocument(rootModule, options = {}) {
|
|
|
184
258
|
},
|
|
185
259
|
paths
|
|
186
260
|
};
|
|
261
|
+
const schemas = registry.build();
|
|
262
|
+
if (schemas) {
|
|
263
|
+
document.components = {
|
|
264
|
+
schemas
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return document;
|
|
187
268
|
}
|
package/dist/openapi/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createOpenApiDocument } from './document';
|
|
2
|
-
export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from './decorators';
|
|
2
|
+
export { ApiDoc, ApiTags, ApiResponse, API_DOC_METADATA, API_TAGS_METADATA, API_RESPONSES_METADATA, } from './decorators';
|
|
3
3
|
export { zodToJsonSchema } from './zod-to-json-schema';
|
|
4
|
-
export
|
|
4
|
+
export { renderScalarUi } from './scalar-ui';
|
|
5
|
+
export type { ApiDocMetadata, ApiResponseEntry, ApiResponseOptions, CreateOpenApiDocumentOptions, HttpVerb, JsonSchema, MountOpenApiOptions, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiRequestBody, OpenApiResponse, } from './types';
|
package/dist/openapi/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createOpenApiDocument } from "./document.js";
|
|
2
|
-
export { ApiDoc, ApiTags, API_DOC_METADATA, API_TAGS_METADATA } from "./decorators.js";
|
|
2
|
+
export { ApiDoc, ApiTags, ApiResponse, API_DOC_METADATA, API_TAGS_METADATA, API_RESPONSES_METADATA } from "./decorators.js";
|
|
3
3
|
export { zodToJsonSchema } from "./zod-to-json-schema.js";
|
|
4
|
+
export { renderScalarUi } from "./scalar-ui.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderScalarUi(jsonUrl: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Minimal HTML shell that bootstraps Scalar's hosted API-reference UI.
|
|
2
|
+
// Served as a static string so it works on every edge runtime with no
|
|
3
|
+
// Node or bundler deps. Scalar itself is loaded from a CDN at runtime.
|
|
4
|
+
export function renderScalarUi(jsonUrl) {
|
|
5
|
+
const safeUrl = jsonUrl.replace(/"/g, '"');
|
|
6
|
+
return `<!doctype html>
|
|
7
|
+
<html>
|
|
8
|
+
<head>
|
|
9
|
+
<title>API Reference</title>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<script id="api-reference" data-url="${safeUrl}"></script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>`;
|
|
18
|
+
}
|
package/dist/openapi/types.d.ts
CHANGED
|
@@ -79,7 +79,25 @@ export interface ApiDocMetadata {
|
|
|
79
79
|
deprecated?: boolean;
|
|
80
80
|
tags?: string[];
|
|
81
81
|
}
|
|
82
|
+
export interface ApiResponseOptions {
|
|
83
|
+
description: string;
|
|
84
|
+
/** Zod schema, DTO class (from createZodDto), or raw JSON Schema. */
|
|
85
|
+
schema?: unknown;
|
|
86
|
+
}
|
|
87
|
+
export interface ApiResponseEntry extends ApiResponseOptions {
|
|
88
|
+
status: number | string;
|
|
89
|
+
}
|
|
82
90
|
export interface CreateOpenApiDocumentOptions {
|
|
83
91
|
info?: Partial<OpenApiInfo>;
|
|
84
92
|
globalPrefix?: string;
|
|
85
93
|
}
|
|
94
|
+
export interface MountOpenApiOptions {
|
|
95
|
+
/** Pre-built OpenAPI document to serve. */
|
|
96
|
+
document: OpenApiDocument;
|
|
97
|
+
/** Path for the JSON endpoint. Default `/docs.json`. */
|
|
98
|
+
path?: string;
|
|
99
|
+
/** Opt-in UI renderer. Only `scalar` is bundled today. */
|
|
100
|
+
ui?: 'scalar';
|
|
101
|
+
/** Path the UI is served at when `ui` is set. Default `/docs`. */
|
|
102
|
+
uiPath?: string;
|
|
103
|
+
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Logger, LogLevel } from './logger';
|
|
2
|
-
export type { LoggerService } from './logger';
|
|
2
|
+
export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './logger';
|
|
@@ -13,18 +13,46 @@ export interface LoggerService {
|
|
|
13
13
|
debug?(message: unknown, ...optionalParams: unknown[]): void;
|
|
14
14
|
verbose?(message: unknown, ...optionalParams: unknown[]): void;
|
|
15
15
|
}
|
|
16
|
+
export type ContextProvider = () => Record<string, unknown>;
|
|
17
|
+
export type LoggerLevelName = 'LOG' | 'ERROR' | 'WARN' | 'DEBUG' | 'VERBOSE';
|
|
18
|
+
export type Writer = (level: LoggerLevelName, line: string, ...rest: unknown[]) => void;
|
|
16
19
|
export declare class Logger implements LoggerService {
|
|
17
20
|
static level: LogLevel;
|
|
18
21
|
private static globalLogger;
|
|
22
|
+
private static globalContextProviders;
|
|
23
|
+
private static globalWriter;
|
|
19
24
|
private context?;
|
|
25
|
+
private instanceContextProviders;
|
|
26
|
+
private instanceWriter?;
|
|
20
27
|
static setLogLevel(level: LogLevel): void;
|
|
21
28
|
static overrideLogger(logger: LoggerService | false): void;
|
|
29
|
+
/** Register a context provider run on every log line from every logger. */
|
|
30
|
+
static addContextProvider(provider: ContextProvider): void;
|
|
31
|
+
/** Remove all registered global context providers. */
|
|
32
|
+
static clearContextProviders(): void;
|
|
33
|
+
/** Replace the default console writer. Useful on edge runtimes that have
|
|
34
|
+
* a different output sink (e.g. `env.LOGS.writeDataPoint` on Workers). */
|
|
35
|
+
static setWriter(writer: Writer): void;
|
|
36
|
+
/** Restore the default console-based writer. */
|
|
37
|
+
static resetWriter(): void;
|
|
22
38
|
constructor(context?: string);
|
|
23
39
|
setContext(context: string): void;
|
|
40
|
+
/** Add a context provider that runs only for THIS logger instance.
|
|
41
|
+
* Children created via extend() inherit these providers by copy. */
|
|
42
|
+
addContextProvider(provider: ContextProvider): this;
|
|
43
|
+
/** Override the writer for this logger (and any extend() children). */
|
|
44
|
+
setWriter(writer: Writer): this;
|
|
45
|
+
/** Create a child logger with a nested namespace. Context becomes
|
|
46
|
+
* `parent:child`; instance-level providers and writers are inherited
|
|
47
|
+
* by copy so later mutations on either logger stay isolated. */
|
|
48
|
+
extend(namespace: string): Logger;
|
|
24
49
|
log(message: unknown, ...optionalParams: unknown[]): void;
|
|
25
50
|
error(message: unknown, ...optionalParams: unknown[]): void;
|
|
26
51
|
warn(message: unknown, ...optionalParams: unknown[]): void;
|
|
27
52
|
debug(message: unknown, ...optionalParams: unknown[]): void;
|
|
28
53
|
verbose(message: unknown, ...optionalParams: unknown[]): void;
|
|
54
|
+
private emit;
|
|
29
55
|
private formatMessage;
|
|
56
|
+
private collectContext;
|
|
57
|
+
private formatContextObj;
|
|
30
58
|
}
|
package/dist/services/logger.js
CHANGED
|
@@ -17,16 +17,50 @@ export var LogLevel = /*#__PURE__*/ function(LogLevel) {
|
|
|
17
17
|
LogLevel[LogLevel["SILENT"] = 5] = "SILENT";
|
|
18
18
|
return LogLevel;
|
|
19
19
|
}({});
|
|
20
|
+
const defaultWriter = (level, line, ...rest)=>{
|
|
21
|
+
if (level === 'ERROR') return console.error(line, ...rest);
|
|
22
|
+
if (level === 'WARN') return console.warn(line, ...rest);
|
|
23
|
+
if (level === 'DEBUG' || level === 'VERBOSE') return console.debug(line, ...rest);
|
|
24
|
+
return console.log(line, ...rest);
|
|
25
|
+
};
|
|
26
|
+
function formatValue(v) {
|
|
27
|
+
if (typeof v === 'string') {
|
|
28
|
+
return /[\s="\\]/.test(v) ? JSON.stringify(v) : v;
|
|
29
|
+
}
|
|
30
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
|
|
31
|
+
try {
|
|
32
|
+
return JSON.stringify(v);
|
|
33
|
+
} catch {
|
|
34
|
+
return String(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
20
37
|
export class Logger {
|
|
21
38
|
static level = 2;
|
|
22
39
|
static globalLogger;
|
|
40
|
+
static globalContextProviders = [];
|
|
41
|
+
static globalWriter = defaultWriter;
|
|
23
42
|
context;
|
|
43
|
+
instanceContextProviders = [];
|
|
44
|
+
instanceWriter;
|
|
24
45
|
static setLogLevel(level) {
|
|
25
46
|
Logger.level = level;
|
|
26
47
|
}
|
|
27
48
|
static overrideLogger(logger) {
|
|
28
49
|
Logger.globalLogger = logger;
|
|
29
50
|
}
|
|
51
|
+
/** Register a context provider run on every log line from every logger. */ static addContextProvider(provider) {
|
|
52
|
+
Logger.globalContextProviders.push(provider);
|
|
53
|
+
}
|
|
54
|
+
/** Remove all registered global context providers. */ static clearContextProviders() {
|
|
55
|
+
Logger.globalContextProviders = [];
|
|
56
|
+
}
|
|
57
|
+
/** Replace the default console writer. Useful on edge runtimes that have
|
|
58
|
+
* a different output sink (e.g. `env.LOGS.writeDataPoint` on Workers). */ static setWriter(writer) {
|
|
59
|
+
Logger.globalWriter = writer;
|
|
60
|
+
}
|
|
61
|
+
/** Restore the default console-based writer. */ static resetWriter() {
|
|
62
|
+
Logger.globalWriter = defaultWriter;
|
|
63
|
+
}
|
|
30
64
|
constructor(context){
|
|
31
65
|
if (context) {
|
|
32
66
|
this.context = context;
|
|
@@ -35,55 +69,86 @@ export class Logger {
|
|
|
35
69
|
setContext(context) {
|
|
36
70
|
this.context = context;
|
|
37
71
|
}
|
|
72
|
+
/** Add a context provider that runs only for THIS logger instance.
|
|
73
|
+
* Children created via extend() inherit these providers by copy. */ addContextProvider(provider) {
|
|
74
|
+
this.instanceContextProviders.push(provider);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/** Override the writer for this logger (and any extend() children). */ setWriter(writer) {
|
|
78
|
+
this.instanceWriter = writer;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/** Create a child logger with a nested namespace. Context becomes
|
|
82
|
+
* `parent:child`; instance-level providers and writers are inherited
|
|
83
|
+
* by copy so later mutations on either logger stay isolated. */ extend(namespace) {
|
|
84
|
+
const newContext = this.context ? `${this.context}:${namespace}` : namespace;
|
|
85
|
+
const child = new Logger(newContext);
|
|
86
|
+
child.instanceContextProviders = [
|
|
87
|
+
...this.instanceContextProviders
|
|
88
|
+
];
|
|
89
|
+
if (this.instanceWriter) child.instanceWriter = this.instanceWriter;
|
|
90
|
+
return child;
|
|
91
|
+
}
|
|
38
92
|
log(message, ...optionalParams) {
|
|
39
|
-
|
|
40
|
-
if (Logger.globalLogger === false) return;
|
|
41
|
-
if (Logger.globalLogger) {
|
|
42
|
-
Logger.globalLogger.log(message, ...optionalParams);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
console.log(this.formatMessage('LOG', message), ...optionalParams);
|
|
93
|
+
this.emit('LOG', 2, message, optionalParams);
|
|
46
94
|
}
|
|
47
95
|
error(message, ...optionalParams) {
|
|
48
|
-
|
|
49
|
-
if (Logger.globalLogger === false) return;
|
|
50
|
-
if (Logger.globalLogger) {
|
|
51
|
-
Logger.globalLogger.error(message, ...optionalParams);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
console.error(this.formatMessage('ERROR', message), ...optionalParams);
|
|
96
|
+
this.emit('ERROR', 4, message, optionalParams);
|
|
55
97
|
}
|
|
56
98
|
warn(message, ...optionalParams) {
|
|
57
|
-
|
|
58
|
-
if (Logger.globalLogger === false) return;
|
|
59
|
-
if (Logger.globalLogger) {
|
|
60
|
-
Logger.globalLogger.warn(message, ...optionalParams);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
console.warn(this.formatMessage('WARN', message), ...optionalParams);
|
|
99
|
+
this.emit('WARN', 3, message, optionalParams);
|
|
64
100
|
}
|
|
65
101
|
debug(message, ...optionalParams) {
|
|
66
|
-
|
|
67
|
-
if (Logger.globalLogger === false) return;
|
|
68
|
-
if (Logger.globalLogger) {
|
|
69
|
-
Logger.globalLogger.debug?.(message, ...optionalParams);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
console.debug(this.formatMessage('DEBUG', message), ...optionalParams);
|
|
102
|
+
this.emit('DEBUG', 1, message, optionalParams);
|
|
73
103
|
}
|
|
74
104
|
verbose(message, ...optionalParams) {
|
|
75
|
-
|
|
105
|
+
this.emit('VERBOSE', 0, message, optionalParams);
|
|
106
|
+
}
|
|
107
|
+
emit(levelName, levelValue, message, rest) {
|
|
108
|
+
if (Logger.level > levelValue) return;
|
|
76
109
|
if (Logger.globalLogger === false) return;
|
|
77
110
|
if (Logger.globalLogger) {
|
|
78
|
-
|
|
111
|
+
const method = levelName.toLowerCase();
|
|
112
|
+
const fn = Logger.globalLogger[method];
|
|
113
|
+
if (fn) fn.call(Logger.globalLogger, message, ...rest);
|
|
79
114
|
return;
|
|
80
115
|
}
|
|
81
|
-
|
|
116
|
+
const line = this.formatMessage(levelName, message);
|
|
117
|
+
const writer = this.instanceWriter ?? Logger.globalWriter;
|
|
118
|
+
writer(levelName, line, ...rest);
|
|
82
119
|
}
|
|
83
120
|
formatMessage(level, message) {
|
|
84
121
|
const timestamp = new Date().toISOString();
|
|
85
122
|
const ctx = this.context ? ` [${this.context}]` : '';
|
|
86
|
-
|
|
123
|
+
const contextStr = this.formatContextObj(this.collectContext());
|
|
124
|
+
return `[Vela] ${timestamp} ${level}${ctx} ${message}${contextStr}`;
|
|
125
|
+
}
|
|
126
|
+
collectContext() {
|
|
127
|
+
const result = {};
|
|
128
|
+
for (const provider of Logger.globalContextProviders){
|
|
129
|
+
try {
|
|
130
|
+
Object.assign(result, provider());
|
|
131
|
+
} catch {
|
|
132
|
+
// Provider threw; drop its contribution and keep going.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const provider of this.instanceContextProviders){
|
|
136
|
+
try {
|
|
137
|
+
Object.assign(result, provider());
|
|
138
|
+
} catch {
|
|
139
|
+
// Same — never let a provider break logging.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
formatContextObj(obj) {
|
|
145
|
+
const parts = [];
|
|
146
|
+
for (const key of Object.keys(obj)){
|
|
147
|
+
const value = obj[key];
|
|
148
|
+
if (value === undefined || value === null) continue;
|
|
149
|
+
parts.push(`${key}=${formatValue(value)}`);
|
|
150
|
+
}
|
|
151
|
+
return parts.length > 0 ? ` ${parts.join(' ')}` : '';
|
|
87
152
|
}
|
|
88
153
|
}
|
|
89
154
|
Logger = _ts_decorate([
|