@velajs/vela 0.9.0 → 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 +33 -0
- package/dist/application.d.ts +12 -0
- package/dist/application.js +22 -0
- package/dist/constants.d.ts +29 -26
- package/dist/constants.js +26 -29
- package/dist/http/middleware-consumer.d.ts +11 -10
- package/dist/http/middleware-consumer.js +10 -11
- package/dist/http/route.manager.js +1 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- 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/pipeline/component.manager.d.ts +1 -0
- package/dist/pipeline/component.manager.js +10 -34
- package/dist/registry/metadata.registry.d.ts +2 -2
- package/dist/registry/metadata.registry.js +13 -6
- package/dist/schedule/cron-matcher.d.ts +2 -0
- package/dist/schedule/cron-matcher.js +62 -0
- package/dist/schedule/index.d.ts +4 -3
- package/dist/schedule/index.js +2 -2
- package/dist/schedule/schedule.module.d.ts +2 -6
- package/dist/schedule/schedule.module.js +4 -42
- package/dist/schedule/schedule.tokens.d.ts +0 -3
- package/dist/schedule/schedule.tokens.js +0 -2
- package/dist/schedule/schedule.types.d.ts +0 -3
- package/dist/schedule-node/index.d.ts +2 -0
- package/dist/schedule-node/index.js +2 -0
- package/dist/schedule-node/schedule-node.module.d.ts +4 -0
- package/dist/schedule-node/schedule-node.module.js +18 -0
- package/dist/{schedule → schedule-node}/schedule.executor.d.ts +2 -5
- package/dist/schedule-node/schedule.executor.js +91 -0
- package/dist/services/logger.d.ts +9 -8
- package/dist/services/logger.js +14 -15
- package/package.json +5 -1
- package/dist/schedule/schedule.executor.js +0 -169
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-04-28)
|
|
4
|
+
|
|
5
|
+
Edge-runtime audit and AI-drift cleanup.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- **Schedule module split.** `ScheduleExecutor`, `SCHEDULE_MODULE_OPTIONS`, and `ScheduleModuleOptions` are no longer exported from `@velajs/vela`. The `setInterval`-based timer executor moved to a new opt-in sub-export at `@velajs/vela/schedule-node`. Consumers on Node or Bun should now do:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { ScheduleNodeModule } from '@velajs/vela/schedule-node';
|
|
13
|
+
|
|
14
|
+
@Module({ imports: [ScheduleNodeModule.forRoot()], providers: [JobsService] })
|
|
15
|
+
class AppModule {}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`ScheduleModule.forRoot()` is now metadata-only — `enableTimers` is no longer accepted (drop the option entirely). `ScheduleModule.forRootAsync()` was removed (no options to async-resolve).
|
|
19
|
+
|
|
20
|
+
Edge runtimes without `setInterval` (Cloudflare Workers, etc.) should continue to use platform cron triggers — `@velajs/cloudflare` ≥ 0.2.0 dispatches core `@Cron` jobs via its `scheduled()` handler.
|
|
21
|
+
|
|
22
|
+
- **TypeScript enums replaced with `as const` objects** for `HttpMethod`, `ParamType`, `Scope`, `RequestMethod`, and `LogLevel`. Value access (`HttpMethod.GET`) keeps working; type-position usages (`: HttpMethod`) keep working via same-name type aliases. Code that imported the enum *type* with structural assumptions about enum runtime shape may need adjustment.
|
|
23
|
+
|
|
24
|
+
### Fixes
|
|
25
|
+
|
|
26
|
+
- `@Head()` is now HEAD-only. Previously it registered as a plain GET handler, so `GET` requests would also hit `@Head()`-decorated methods.
|
|
27
|
+
- Handler-level guards / pipes / interceptors / filters now key off the controller constructor instead of `${className}:${method}`, fixing a metadata collision when two controllers shared a class name (across feature modules or after minification).
|
|
28
|
+
- Removed an obsolete narration comment in `route.manager.ts`.
|
|
29
|
+
|
|
30
|
+
### New
|
|
31
|
+
|
|
32
|
+
- `@velajs/vela/schedule-node` — opt-in entry for Node/Bun cron and interval execution.
|
|
33
|
+
- `parseCron(expression)` and `CronMatcher` exported from the core for use by platform cron adapters.
|
|
34
|
+
- Edge-runtime audit test (`src/__tests__/edge-runtime-audit.test.ts`) — fails CI if any file under `src/` (excluding `schedule-node/`) references forbidden APIs (`node:*`, `Buffer`, `process.*`, `__dirname/__filename`, `fs/path/os/child_process`, `setInterval`, `Bun.serve`).
|
|
35
|
+
|
|
3
36
|
## 0.1.0 (2026-02-19)
|
|
4
37
|
|
|
5
38
|
Initial release.
|
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/constants.d.ts
CHANGED
|
@@ -13,29 +13,32 @@ export declare const METADATA_KEYS: {
|
|
|
13
13
|
readonly CATCH: "vela:catch";
|
|
14
14
|
readonly CRUD: "vela:crud";
|
|
15
15
|
};
|
|
16
|
-
export declare
|
|
17
|
-
GET
|
|
18
|
-
POST
|
|
19
|
-
PUT
|
|
20
|
-
PATCH
|
|
21
|
-
DELETE
|
|
22
|
-
OPTIONS
|
|
23
|
-
HEAD
|
|
24
|
-
ALL
|
|
25
|
-
}
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
export declare const HttpMethod: {
|
|
17
|
+
readonly GET: "get";
|
|
18
|
+
readonly POST: "post";
|
|
19
|
+
readonly PUT: "put";
|
|
20
|
+
readonly PATCH: "patch";
|
|
21
|
+
readonly DELETE: "delete";
|
|
22
|
+
readonly OPTIONS: "options";
|
|
23
|
+
readonly HEAD: "head";
|
|
24
|
+
readonly ALL: "all";
|
|
25
|
+
};
|
|
26
|
+
export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];
|
|
27
|
+
export declare const ParamType: {
|
|
28
|
+
readonly BODY: "body";
|
|
29
|
+
readonly QUERY: "query";
|
|
30
|
+
readonly PARAM: "param";
|
|
31
|
+
readonly HEADERS: "headers";
|
|
32
|
+
readonly REQUEST: "request";
|
|
33
|
+
readonly RESPONSE: "response";
|
|
34
|
+
readonly IP: "ip";
|
|
35
|
+
readonly COOKIE: "cookie";
|
|
36
|
+
readonly RAW_BODY: "raw_body";
|
|
37
|
+
};
|
|
38
|
+
export type ParamType = (typeof ParamType)[keyof typeof ParamType];
|
|
39
|
+
export declare const Scope: {
|
|
40
|
+
readonly SINGLETON: "singleton";
|
|
41
|
+
readonly TRANSIENT: "transient";
|
|
42
|
+
readonly REQUEST: "request";
|
|
43
|
+
};
|
|
44
|
+
export type Scope = (typeof Scope)[keyof typeof Scope];
|
package/dist/constants.js
CHANGED
|
@@ -18,32 +18,29 @@ export const METADATA_KEYS = {
|
|
|
18
18
|
// CRUD
|
|
19
19
|
CRUD: 'vela:crud'
|
|
20
20
|
};
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Scope["REQUEST"] = "request";
|
|
48
|
-
return Scope;
|
|
49
|
-
}({});
|
|
21
|
+
export const HttpMethod = {
|
|
22
|
+
GET: 'get',
|
|
23
|
+
POST: 'post',
|
|
24
|
+
PUT: 'put',
|
|
25
|
+
PATCH: 'patch',
|
|
26
|
+
DELETE: 'delete',
|
|
27
|
+
OPTIONS: 'options',
|
|
28
|
+
HEAD: 'head',
|
|
29
|
+
ALL: 'all'
|
|
30
|
+
};
|
|
31
|
+
export const ParamType = {
|
|
32
|
+
BODY: 'body',
|
|
33
|
+
QUERY: 'query',
|
|
34
|
+
PARAM: 'param',
|
|
35
|
+
HEADERS: 'headers',
|
|
36
|
+
REQUEST: 'request',
|
|
37
|
+
RESPONSE: 'response',
|
|
38
|
+
IP: 'ip',
|
|
39
|
+
COOKIE: 'cookie',
|
|
40
|
+
RAW_BODY: 'raw_body'
|
|
41
|
+
};
|
|
42
|
+
export const Scope = {
|
|
43
|
+
SINGLETON: 'singleton',
|
|
44
|
+
TRANSIENT: 'transient',
|
|
45
|
+
REQUEST: 'request'
|
|
46
|
+
};
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { NestMiddleware } from '../pipeline/types';
|
|
2
2
|
import type { Type } from '../registry/types';
|
|
3
|
-
export declare
|
|
4
|
-
GET
|
|
5
|
-
POST
|
|
6
|
-
PUT
|
|
7
|
-
DELETE
|
|
8
|
-
PATCH
|
|
9
|
-
OPTIONS
|
|
10
|
-
HEAD
|
|
11
|
-
ALL
|
|
12
|
-
}
|
|
3
|
+
export declare const RequestMethod: {
|
|
4
|
+
readonly GET: "GET";
|
|
5
|
+
readonly POST: "POST";
|
|
6
|
+
readonly PUT: "PUT";
|
|
7
|
+
readonly DELETE: "DELETE";
|
|
8
|
+
readonly PATCH: "PATCH";
|
|
9
|
+
readonly OPTIONS: "OPTIONS";
|
|
10
|
+
readonly HEAD: "HEAD";
|
|
11
|
+
readonly ALL: "ALL";
|
|
12
|
+
};
|
|
13
|
+
export type RequestMethod = (typeof RequestMethod)[keyof typeof RequestMethod];
|
|
13
14
|
export interface RouteInfo {
|
|
14
15
|
path: string;
|
|
15
16
|
method?: RequestMethod;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}({});
|
|
2
|
+
export const RequestMethod = {
|
|
3
|
+
GET: 'GET',
|
|
4
|
+
POST: 'POST',
|
|
5
|
+
PUT: 'PUT',
|
|
6
|
+
DELETE: 'DELETE',
|
|
7
|
+
PATCH: 'PATCH',
|
|
8
|
+
OPTIONS: 'OPTIONS',
|
|
9
|
+
HEAD: 'HEAD',
|
|
10
|
+
ALL: 'ALL'
|
|
11
|
+
};
|
|
13
12
|
export class MiddlewareBuilder {
|
|
14
13
|
definitions = [];
|
|
15
14
|
apply(...middleware) {
|
|
@@ -50,7 +50,7 @@ export class RouteManager {
|
|
|
50
50
|
],
|
|
51
51
|
[
|
|
52
52
|
HttpMethod.HEAD,
|
|
53
|
-
(app, p, h)=>app.get(p, h)
|
|
53
|
+
(app, p, h)=>app.get(p, (c, next)=>c.req.method === 'HEAD' ? h(c) : next())
|
|
54
54
|
],
|
|
55
55
|
[
|
|
56
56
|
HttpMethod.ALL,
|
|
@@ -175,11 +175,6 @@ export class RouteManager {
|
|
|
175
175
|
instantiateMany(items, container) {
|
|
176
176
|
return items.map((item)=>this.instantiate(item, container));
|
|
177
177
|
}
|
|
178
|
-
// Resolve middleware priority at build time. Supports three sources:
|
|
179
|
-
// 1. static priority on the class constructor
|
|
180
|
-
// 2. priority property on the instance
|
|
181
|
-
// 3. for container tokens, resolve and inspect the instance (+ its ctor)
|
|
182
|
-
// Falls back to 0 on any error or when no priority is set.
|
|
183
178
|
getMiddlewarePriority(entry) {
|
|
184
179
|
if (entry == null) return 0;
|
|
185
180
|
if (typeof entry === 'function') {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ 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';
|
|
@@ -20,8 +20,8 @@ export { CacheModule, CacheService, CacheInterceptor, MemoryCacheStore, CacheKey
|
|
|
20
20
|
export type { CacheModuleOptions, CacheStore, CacheEntry } from './cache/index';
|
|
21
21
|
export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_EVENT_METADATA, } from './event-emitter/index';
|
|
22
22
|
export type { EventHandler, OnEventMetadata } from './event-emitter/index';
|
|
23
|
-
export { ScheduleModule, ScheduleRegistry,
|
|
24
|
-
export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata,
|
|
23
|
+
export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA, } from './schedule/index';
|
|
24
|
+
export type { RegisteredCronJob, RegisteredIntervalJob, CronMetadata, IntervalMetadata, CronMatcher, } from './schedule/index';
|
|
25
25
|
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator, } from './health/index';
|
|
26
26
|
export type { HealthCheckResult, HealthCheckStatus, HealthIndicatorResult, HealthIndicatorFunction, ResponseCheckCallback, HttpPingOptions, } from './health/index';
|
|
27
27
|
export { ThrottlerModule, ThrottlerGuard, ThrottlerStorage, Throttle, SkipThrottle, THROTTLER_OPTIONS, THROTTLER_STORAGE, THROTTLE_METADATA, SKIP_THROTTLE_METADATA, } from './throttler/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
|
|
@@ -24,7 +24,7 @@ export { CacheModule, CacheService, CacheInterceptor, MemoryCacheStore, CacheKey
|
|
|
24
24
|
// Event Emitter
|
|
25
25
|
export { EventEmitterModule, EventEmitter, EventEmitterSubscriber, OnEvent, ON_EVENT_METADATA } from "./event-emitter/index.js";
|
|
26
26
|
// Schedule
|
|
27
|
-
export { ScheduleModule, ScheduleRegistry,
|
|
27
|
+
export { ScheduleModule, ScheduleRegistry, Cron, Interval, parseCron, CRON_METADATA, INTERVAL_METADATA } from "./schedule/index.js";
|
|
28
28
|
// Health
|
|
29
29
|
export { HealthModule, HealthCheckService, HealthIndicatorService, HttpHealthIndicator } from "./health/index.js";
|
|
30
30
|
// Throttler
|
|
@@ -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
|
+
}
|