@enzopita/adonisjs-openapi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +64 -2
- package/build/commands/generate_openapi.d.ts +8 -0
- package/build/commands/generate_openapi.js +49 -0
- package/build/commands/main.d.ts +4 -0
- package/build/commands/main.js +7 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +8 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +4 -0
- package/build/src/collect_routes.d.ts +7 -0
- package/build/src/collect_routes.js +60 -0
- package/build/src/define_config.d.ts +2 -0
- package/build/src/define_config.js +14 -0
- package/build/src/generate.d.ts +8 -0
- package/build/src/generate.js +148 -0
- package/build/src/route_types.d.ts +25 -0
- package/build/src/route_types.js +80 -0
- package/build/src/type_to_schema.d.ts +17 -0
- package/build/src/type_to_schema.js +135 -0
- package/build/src/types.d.ts +114 -0
- package/build/src/types.js +1 -0
- package/build/stubs/config/openapi.stub +15 -0
- package/build/stubs/main.d.ts +1 -0
- package/build/stubs/main.js +1 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enzo Pita <enzo@enzopita.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @enzopita/adonisjs-openapi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Generate an OpenAPI 3.1 document from an AdonisJS 7 app, with no annotations. The generator reads the same sources as [Tuyau](https://github.com/Julien-R44/tuyau):
|
|
4
|
+
|
|
5
|
+
| OpenAPI | Source |
|
|
6
|
+
| ----------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
7
|
+
| Paths and operations | Named routes from the router |
|
|
8
|
+
| Request body, query and path params | The VineJS validator passed to `request.validateUsing()` (formats, bounds and enums included) |
|
|
9
|
+
| Responses | The return type of the controller method, such as `serialize(UserTransformer.transform(user))` |
|
|
10
|
+
| `422` response | Added to every route that validates its input |
|
|
11
|
+
| `security` | Named middleware on the route, mapped in the config |
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
node ace add @enzopita/adonisjs-openapi
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The command installs the package, registers its ace command in `adonisrc.ts`, and creates `config/openapi.ts`. The package needs `@adonisjs/assembler`, which every AdonisJS app has as a dev dependency.
|
|
20
|
+
|
|
21
|
+
## Generate the document
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
node ace openapi:generate
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The command boots the app the way `node ace codegen` does, so no database connection or queue worker starts. It writes `openapi.json` at the app root. To write it somewhere else, pass `--output=public/openapi.json` or set `output` in the config.
|
|
28
|
+
|
|
29
|
+
## Configure
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// config/openapi.ts
|
|
33
|
+
import { defineConfig } from '@enzopita/adonisjs-openapi'
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
output: 'openapi.json',
|
|
37
|
+
info: { title: 'Acme API', version: '1.0.0' },
|
|
38
|
+
servers: [{ url: 'https://api.acme.test' }],
|
|
39
|
+
routes: { except: ['admin.'] },
|
|
40
|
+
securitySchemes: {
|
|
41
|
+
accessToken: { type: 'http', scheme: 'bearer' },
|
|
42
|
+
},
|
|
43
|
+
middlewareSecurity: {
|
|
44
|
+
auth: 'accessToken',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `routes` takes `only` or `except`, with the same matchers as Tuyau's `generateRegistry()`: a substring, a regular expression, or a function of the route name.
|
|
50
|
+
- `middlewareSecurity` maps a named middleware, such as `middleware.auth()`, to a key of `securitySchemes`.
|
|
51
|
+
|
|
52
|
+
## How types become schemas
|
|
53
|
+
|
|
54
|
+
- A route must have a name and a controller handler. Closure routes are skipped, as they are in Tuyau.
|
|
55
|
+
- On `GET` routes, the validator fields become query parameters. On other methods they become the body, and the validator's `query` and `params` objects become query and path parameters.
|
|
56
|
+
- A body with a `vine.file()` field is sent as `multipart/form-data`.
|
|
57
|
+
- When VineJS cannot describe a field (a file, a custom rule), the generator uses the field's TypeScript type.
|
|
58
|
+
- `Date` and Luxon `DateTime` values become `{ type: 'string', format: 'date-time' }`.
|
|
59
|
+
- A handler that returns a model or an untyped object produces a `200` response with no schema and a warning. Return `serialize(...)` to type it.
|
|
60
|
+
|
|
61
|
+
## Limitations
|
|
62
|
+
|
|
63
|
+
- Schemas are inlined. Transformers do not become named components yet.
|
|
64
|
+
- Operations have no `summary` or `description` yet.
|
|
65
|
+
- Status codes other than `200` and `422` come only from return types that carry a `__status`, which no AdonisJS 7.6 API produces yet.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { BaseCommand, flags } from '@adonisjs/core/ace';
|
|
11
|
+
import { defineConfig } from "../src/define_config.js";
|
|
12
|
+
export default class GenerateOpenApi extends BaseCommand {
|
|
13
|
+
static commandName = 'openapi:generate';
|
|
14
|
+
static description = 'Generate an OpenAPI document from the routes, validators and transformers';
|
|
15
|
+
/**
|
|
16
|
+
* Same warm-up as `node ace codegen`: the app is assembled like the web
|
|
17
|
+
* environment, but never becomes ready, so no long-running side effect starts.
|
|
18
|
+
*/
|
|
19
|
+
async #warmUpApp() {
|
|
20
|
+
this.app.setEnvironment('web');
|
|
21
|
+
this.app.setMode('warmup');
|
|
22
|
+
await this.app.boot();
|
|
23
|
+
await this.app.warmUp();
|
|
24
|
+
const router = await this.app.container.make('router');
|
|
25
|
+
router.commit();
|
|
26
|
+
return router;
|
|
27
|
+
}
|
|
28
|
+
async run() {
|
|
29
|
+
const router = await this.#warmUpApp();
|
|
30
|
+
const config = this.app.config.get('openapi') ?? defineConfig();
|
|
31
|
+
const startTime = process.hrtime();
|
|
32
|
+
// Loaded on run: it pulls in the TypeScript compiler, which every other ace command would pay for.
|
|
33
|
+
const { generateDocument } = await import("../src/generate.js");
|
|
34
|
+
const { document, warnings } = await generateDocument(this.app, router, config);
|
|
35
|
+
for (const warning of warnings)
|
|
36
|
+
this.logger.warning(warning);
|
|
37
|
+
const output = join(fileURLToPath(this.app.appRoot), this.output ?? config.output);
|
|
38
|
+
await mkdir(dirname(output), { recursive: true });
|
|
39
|
+
await writeFile(output, `${JSON.stringify(document, null, 2)}\n`);
|
|
40
|
+
this.logger.success(`OpenAPI document written to ${this.app.relativePath(output)}`, {
|
|
41
|
+
startTime,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
__decorate([
|
|
46
|
+
flags.string({
|
|
47
|
+
description: 'Output file, relative to the app root. Defaults to config/openapi.ts "output"',
|
|
48
|
+
})
|
|
49
|
+
], GenerateOpenApi.prototype, "output", void 0);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CommandMetaData } from '@adonisjs/core/types/ace';
|
|
2
|
+
import GenerateOpenApi from './generate_openapi.ts';
|
|
3
|
+
export declare function getMetaData(): Promise<CommandMetaData[]>;
|
|
4
|
+
export declare function getCommand(metaData: CommandMetaData): Promise<typeof GenerateOpenApi | null>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import GenerateOpenApi from "./generate_openapi.js";
|
|
2
|
+
export async function getMetaData() {
|
|
3
|
+
return [{ ...GenerateOpenApi.serialize(), filePath: 'generate_openapi.js' }];
|
|
4
|
+
}
|
|
5
|
+
export async function getCommand(metaData) {
|
|
6
|
+
return metaData.commandName === GenerateOpenApi.commandName ? GenerateOpenApi : null;
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { stubsRoot } from "./stubs/main.js";
|
|
2
|
+
export async function configure(command) {
|
|
3
|
+
const codemods = await command.createCodemods();
|
|
4
|
+
await codemods.makeUsingStub(stubsRoot, 'config/openapi.stub', {});
|
|
5
|
+
await codemods.updateRcFile(rcFile => {
|
|
6
|
+
rcFile.addCommand('@enzopita/adonisjs-openapi/commands');
|
|
7
|
+
});
|
|
8
|
+
}
|
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Router } from '@adonisjs/core/http';
|
|
2
|
+
import type { OpenApiConfig, RouteOperation } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Scans the committed router with the same routes scanner the dev server feeds
|
|
5
|
+
* Tuyau, so both see the same request and response types.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectRoutes(appRoot: string, router: Router, config: OpenApiConfig): Promise<RouteOperation[]>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RoutesScanner } from '@adonisjs/assembler/routes_scanner';
|
|
2
|
+
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options']);
|
|
3
|
+
function matches(routeName, matcher) {
|
|
4
|
+
if (matcher instanceof RegExp)
|
|
5
|
+
return matcher.test(routeName);
|
|
6
|
+
if (matcher instanceof Function)
|
|
7
|
+
return matcher(routeName);
|
|
8
|
+
return routeName.includes(matcher);
|
|
9
|
+
}
|
|
10
|
+
function isIncluded(routeName, filters) {
|
|
11
|
+
if (filters.only)
|
|
12
|
+
return filters.only.some(matcher => matches(routeName, matcher));
|
|
13
|
+
if (filters.except)
|
|
14
|
+
return !filters.except.some(matcher => matches(routeName, matcher));
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `/users/:id/:slug?` becomes `/users/{id}/{slug}`, the only form OpenAPI paths accept.
|
|
19
|
+
*/
|
|
20
|
+
function toOpenApiPath(pattern) {
|
|
21
|
+
return pattern.replace(/:([^/?]+)\??/g, '{$1}').replace(/\*/g, '{wildcard}');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Scans the committed router with the same routes scanner the dev server feeds
|
|
25
|
+
* Tuyau, so both see the same request and response types.
|
|
26
|
+
*/
|
|
27
|
+
export async function collectRoutes(appRoot, router, config) {
|
|
28
|
+
const live = router.toJSON();
|
|
29
|
+
const middlewareByRoute = new Map();
|
|
30
|
+
for (const route of Object.values(live).flat()) {
|
|
31
|
+
const names = [...route.middleware.all()].flatMap(entry => 'name' in entry && entry.name ? [entry.name] : []);
|
|
32
|
+
middlewareByRoute.set(`${route.domain}:${route.pattern}:${route.methods.join(',')}`, names);
|
|
33
|
+
}
|
|
34
|
+
// Round-tripped through JSON like the dev server and `node ace codegen` do, which drops closure handlers.
|
|
35
|
+
const serialized = JSON.parse(JSON.stringify(live));
|
|
36
|
+
const scanner = new RoutesScanner(appRoot, []);
|
|
37
|
+
scanner.filter(route => isIncluded(route.name ?? '', config.routes));
|
|
38
|
+
for (const routes of Object.values(serialized))
|
|
39
|
+
await scanner.scan(routes);
|
|
40
|
+
return scanner.getScannedRoutes().map(route => {
|
|
41
|
+
const pattern = toOpenApiPath(route.pattern);
|
|
42
|
+
const [validator] = route.validators ?? [];
|
|
43
|
+
return {
|
|
44
|
+
name: route.name,
|
|
45
|
+
methods: route.methods
|
|
46
|
+
.map(method => method.toLowerCase())
|
|
47
|
+
.filter((method) => METHODS.has(method)),
|
|
48
|
+
path: pattern,
|
|
49
|
+
pathParams: [...pattern.matchAll(/\{([^}]+)\}/g)].map(match => match[1]),
|
|
50
|
+
middleware: middlewareByRoute.get(`${route.domain}:${route.pattern}:${route.methods.join(',')}`) ?? [],
|
|
51
|
+
requestType: route.request?.type,
|
|
52
|
+
responseType: route.response?.type,
|
|
53
|
+
imports: [...(route.request?.imports ?? []), ...(route.response?.imports ?? [])],
|
|
54
|
+
validator: validator && {
|
|
55
|
+
specifier: validator.import.specifier,
|
|
56
|
+
exportName: validator.import.value,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function defineConfig(config = {}) {
|
|
2
|
+
if (config.routes?.only && config.routes.except) {
|
|
3
|
+
throw new Error('openapi: use either "routes.only" or "routes.except", not both');
|
|
4
|
+
}
|
|
5
|
+
return {
|
|
6
|
+
output: 'openapi.json',
|
|
7
|
+
info: { title: 'API', version: '1.0.0' },
|
|
8
|
+
servers: [],
|
|
9
|
+
routes: {},
|
|
10
|
+
securitySchemes: {},
|
|
11
|
+
middlewareSecurity: {},
|
|
12
|
+
...config,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
import type { Router } from '@adonisjs/core/http';
|
|
3
|
+
import type { OpenApiConfig, OpenApiDocument } from './types.ts';
|
|
4
|
+
export interface GeneratedDocument {
|
|
5
|
+
document: OpenApiDocument;
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function generateDocument(app: ApplicationService, router: Router, config: OpenApiConfig): Promise<GeneratedDocument>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { STATUS_CODES } from 'node:http';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { collectRoutes } from "./collect_routes.js";
|
|
5
|
+
import { RouteTypes } from "./route_types.js";
|
|
6
|
+
const NOT_IN_BODY = new Set(['query', 'params', 'headers', 'cookies']);
|
|
7
|
+
/**
|
|
8
|
+
* Vine knows the constraints (formats, bounds, enums) but has no JSON Schema for
|
|
9
|
+
* files or custom rules and emits `{}` there: the TypeScript shape fills those gaps.
|
|
10
|
+
*/
|
|
11
|
+
function mergeSchemas(vine, typed) {
|
|
12
|
+
if (!vine || !Object.keys(vine).length)
|
|
13
|
+
return typed;
|
|
14
|
+
if (!vine.properties || !typed.properties)
|
|
15
|
+
return vine;
|
|
16
|
+
const typedProperties = typed.properties;
|
|
17
|
+
const properties = Object.fromEntries(Object.entries(vine.properties).map(([key, schema]) => [
|
|
18
|
+
key,
|
|
19
|
+
mergeSchemas(schema, typedProperties[key] ?? {}),
|
|
20
|
+
]));
|
|
21
|
+
return { ...vine, properties };
|
|
22
|
+
}
|
|
23
|
+
function pick(schema, keep) {
|
|
24
|
+
const properties = Object.entries(schema.properties ?? {}).filter(([key]) => keep(key));
|
|
25
|
+
if (!properties.length)
|
|
26
|
+
return undefined;
|
|
27
|
+
const { required = [], ...rest } = schema;
|
|
28
|
+
const kept = required.filter(keep);
|
|
29
|
+
return kept.length
|
|
30
|
+
? { ...rest, properties: Object.fromEntries(properties), required: kept }
|
|
31
|
+
: { ...rest, properties: Object.fromEntries(properties) };
|
|
32
|
+
}
|
|
33
|
+
async function validatorSchemas(app, routes, warnings) {
|
|
34
|
+
const schemas = new Map();
|
|
35
|
+
for (const route of routes) {
|
|
36
|
+
if (!route.validator)
|
|
37
|
+
continue;
|
|
38
|
+
const { specifier, exportName } = route.validator;
|
|
39
|
+
try {
|
|
40
|
+
// SAFETY: the routes scanner only reports values passed to `request.validateUsing()`, which are Vine validators.
|
|
41
|
+
const module = (await app.import(specifier));
|
|
42
|
+
const validator = module[exportName];
|
|
43
|
+
if (validator)
|
|
44
|
+
schemas.set(route.name, validator.toJSONSchema());
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
48
|
+
warnings.push(`${route.name}: cannot read the JSON Schema of ${specifier}#${exportName} (${reason})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return schemas;
|
|
52
|
+
}
|
|
53
|
+
function responsesFor(route, types, response, warnings) {
|
|
54
|
+
const responses = {};
|
|
55
|
+
const describe = (code, schema) => {
|
|
56
|
+
responses[code] = {
|
|
57
|
+
description: STATUS_CODES[Number(code)] ?? 'Response',
|
|
58
|
+
content: { 'application/json': { schema } },
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const members = response?.isUnion() ? response.types : response ? [response] : [];
|
|
62
|
+
for (const member of members) {
|
|
63
|
+
const status = types.property(member, '__status');
|
|
64
|
+
const payload = types.property(member, '__response');
|
|
65
|
+
if (status && payload) {
|
|
66
|
+
describe(status.isNumberLiteral() ? String(status.value) : 'default', types.schemas.convert(payload));
|
|
67
|
+
}
|
|
68
|
+
else if (!(member.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))) {
|
|
69
|
+
describe('200', types.schemas.convert(member));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const validationError = route.validator && types.validationError();
|
|
73
|
+
if (validationError)
|
|
74
|
+
describe('422', types.schemas.convert(validationError));
|
|
75
|
+
if (!Object.keys(responses).some(code => code.startsWith('2') || code === 'default')) {
|
|
76
|
+
warnings.push(`${route.name}: the handler's return type is unknown, returning serialize(...) makes it typed`);
|
|
77
|
+
responses['200'] = { description: 'OK' };
|
|
78
|
+
}
|
|
79
|
+
return responses;
|
|
80
|
+
}
|
|
81
|
+
function parametersFor(route, query, params) {
|
|
82
|
+
const path = route.pathParams.map((name) => ({
|
|
83
|
+
name,
|
|
84
|
+
in: 'path',
|
|
85
|
+
required: true,
|
|
86
|
+
schema: params?.properties?.[name] ?? { type: 'string' },
|
|
87
|
+
}));
|
|
88
|
+
const search = Object.entries(query?.properties ?? {}).map(([name, schema]) => ({
|
|
89
|
+
name,
|
|
90
|
+
in: 'query',
|
|
91
|
+
required: query?.required?.includes(name) ?? false,
|
|
92
|
+
schema,
|
|
93
|
+
}));
|
|
94
|
+
return [...path, ...search];
|
|
95
|
+
}
|
|
96
|
+
export async function generateDocument(app, router, config) {
|
|
97
|
+
const warnings = [];
|
|
98
|
+
const appRoot = fileURLToPath(app.appRoot);
|
|
99
|
+
const routes = await collectRoutes(appRoot, router, config);
|
|
100
|
+
const types = new RouteTypes(appRoot, routes);
|
|
101
|
+
const vine = await validatorSchemas(app, routes, warnings);
|
|
102
|
+
const paths = {};
|
|
103
|
+
for (const route of routes) {
|
|
104
|
+
const { request, response } = types.get(route.name);
|
|
105
|
+
const input = request && mergeSchemas(vine.get(route.name), types.schemas.convert(request));
|
|
106
|
+
const readsQueryOnly = route.methods.every(method => method === 'get' || method === 'options');
|
|
107
|
+
const fields = input && pick(input, key => !NOT_IN_BODY.has(key));
|
|
108
|
+
const body = readsQueryOnly ? undefined : fields;
|
|
109
|
+
const query = readsQueryOnly ? fields : input?.properties?.query;
|
|
110
|
+
const parameters = parametersFor(route, query, input?.properties?.params);
|
|
111
|
+
const security = route.middleware.flatMap(name => {
|
|
112
|
+
const scheme = config.middlewareSecurity[name];
|
|
113
|
+
return scheme ? [{ [scheme]: [] }] : [];
|
|
114
|
+
});
|
|
115
|
+
const operation = {
|
|
116
|
+
tags: [route.name.split('.')[0] ?? route.name],
|
|
117
|
+
responses: responsesFor(route, types, response, warnings),
|
|
118
|
+
};
|
|
119
|
+
if (parameters.length)
|
|
120
|
+
operation.parameters = parameters;
|
|
121
|
+
if (security.length)
|
|
122
|
+
operation.security = security;
|
|
123
|
+
if (body) {
|
|
124
|
+
const mediaType = JSON.stringify(body).includes('"format":"binary"')
|
|
125
|
+
? 'multipart/form-data'
|
|
126
|
+
: 'application/json';
|
|
127
|
+
operation.requestBody = { required: true, content: { [mediaType]: { schema: body } } };
|
|
128
|
+
}
|
|
129
|
+
const methods = paths[route.path] ?? {};
|
|
130
|
+
paths[route.path] = methods;
|
|
131
|
+
for (const method of route.methods) {
|
|
132
|
+
const operationId = route.methods.length > 1 ? `${route.name}.${method}` : route.name;
|
|
133
|
+
methods[method] = { operationId, ...operation };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const type of types.schemas.unsupported) {
|
|
137
|
+
warnings.push(`no JSON Schema for the type ${type}, emitted {}`);
|
|
138
|
+
}
|
|
139
|
+
const document = {
|
|
140
|
+
openapi: '3.1.0',
|
|
141
|
+
info: config.info,
|
|
142
|
+
paths,
|
|
143
|
+
components: { schemas: types.schemas.components, securitySchemes: config.securitySchemes },
|
|
144
|
+
};
|
|
145
|
+
if (config.servers.length)
|
|
146
|
+
document.servers = config.servers;
|
|
147
|
+
return { document, warnings };
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { TypeToSchema } from './type_to_schema.ts';
|
|
3
|
+
import type { RouteOperation } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the request and response types of every route with the app's own
|
|
6
|
+
* tsconfig, through one in-memory module that names them all.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RouteTypes {
|
|
9
|
+
#private;
|
|
10
|
+
readonly checker: ts.TypeChecker;
|
|
11
|
+
readonly schemas: TypeToSchema;
|
|
12
|
+
constructor(appRoot: string, routes: RouteOperation[]);
|
|
13
|
+
/**
|
|
14
|
+
* The types of one route's `request` and `response` members.
|
|
15
|
+
*/
|
|
16
|
+
get(routeName: string): {
|
|
17
|
+
request?: undefined;
|
|
18
|
+
response?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
request: ts.Type | undefined;
|
|
21
|
+
response: ts.Type | undefined;
|
|
22
|
+
};
|
|
23
|
+
validationError(): ts.Type | undefined;
|
|
24
|
+
property(type: ts.Type, name: string): ts.Type | undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { TypeToSchema } from "./type_to_schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the request and response types of every route with the app's own
|
|
6
|
+
* tsconfig, through one in-memory module that names them all.
|
|
7
|
+
*/
|
|
8
|
+
export class RouteTypes {
|
|
9
|
+
checker;
|
|
10
|
+
schemas;
|
|
11
|
+
#routes;
|
|
12
|
+
#validationError;
|
|
13
|
+
constructor(appRoot, routes) {
|
|
14
|
+
const entryFile = join(appRoot, '.adonisjs/openapi/routes.ts');
|
|
15
|
+
const config = ts.getParsedCommandLineOfConfigFile(join(appRoot, 'tsconfig.json'), {}, {
|
|
16
|
+
...ts.sys,
|
|
17
|
+
onUnRecoverableConfigFileDiagnostic: diagnostic => {
|
|
18
|
+
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!config)
|
|
22
|
+
throw new Error(`openapi: cannot read ${join(appRoot, 'tsconfig.json')}`);
|
|
23
|
+
const source = renderEntry(entryFile, appRoot, routes);
|
|
24
|
+
const host = ts.createCompilerHost(config.options);
|
|
25
|
+
const getSourceFile = host.getSourceFile.bind(host);
|
|
26
|
+
host.getSourceFile = (fileName, languageVersion, ...rest) => fileName === entryFile
|
|
27
|
+
? ts.createSourceFile(fileName, source, languageVersion)
|
|
28
|
+
: getSourceFile(fileName, languageVersion, ...rest);
|
|
29
|
+
const fileExists = host.fileExists.bind(host);
|
|
30
|
+
host.fileExists = fileName => fileName === entryFile || fileExists(fileName);
|
|
31
|
+
// The app's own files carry the module augmentations (container bindings, auth guards) the types depend on.
|
|
32
|
+
const program = ts.createProgram({
|
|
33
|
+
rootNames: [...config.fileNames, entryFile],
|
|
34
|
+
options: { ...config.options, noEmit: true },
|
|
35
|
+
host,
|
|
36
|
+
});
|
|
37
|
+
this.checker = program.getTypeChecker();
|
|
38
|
+
this.schemas = new TypeToSchema(this.checker);
|
|
39
|
+
const entry = program.getSourceFile(entryFile);
|
|
40
|
+
this.#routes = entry.statements.find(ts.isInterfaceDeclaration);
|
|
41
|
+
this.#validationError = entry.statements.find(ts.isTypeAliasDeclaration);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The types of one route's `request` and `response` members.
|
|
45
|
+
*/
|
|
46
|
+
get(routeName) {
|
|
47
|
+
const member = this.#routes.members.find((candidate) => ts.isPropertySignature(candidate) &&
|
|
48
|
+
ts.isStringLiteral(candidate.name) &&
|
|
49
|
+
candidate.name.text === routeName);
|
|
50
|
+
if (!member)
|
|
51
|
+
return {};
|
|
52
|
+
const type = this.checker.getTypeAtLocation(member);
|
|
53
|
+
return { request: this.property(type, 'request'), response: this.property(type, 'response') };
|
|
54
|
+
}
|
|
55
|
+
validationError() {
|
|
56
|
+
return this.#validationError && this.checker.getTypeAtLocation(this.#validationError);
|
|
57
|
+
}
|
|
58
|
+
property(type, name) {
|
|
59
|
+
const symbol = this.checker.getPropertyOfType(type, name);
|
|
60
|
+
return symbol && this.checker.getTypeOfSymbol(symbol);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function renderEntry(entryFile, appRoot, routes) {
|
|
64
|
+
const toRelative = (expression) => expression.replace(/import\('(app\/[^']+?)(\.ts)?'\)/g, (_, path) => {
|
|
65
|
+
const target = relative(join(entryFile, '..'), join(appRoot, path));
|
|
66
|
+
return `import('${target}.js')`;
|
|
67
|
+
});
|
|
68
|
+
const imports = new Set(routes.flatMap(route => route.imports));
|
|
69
|
+
const members = routes.map(route => {
|
|
70
|
+
const request = route.requestType ? `request: ${toRelative(route.requestType)}; ` : '';
|
|
71
|
+
const response = route.responseType
|
|
72
|
+
? `response: Awaited<${toRelative(route.responseType)}>`
|
|
73
|
+
: '';
|
|
74
|
+
return ` ${JSON.stringify(route.name)}: { ${request}${response} }`;
|
|
75
|
+
});
|
|
76
|
+
const validationError = routes.some(route => route.validator)
|
|
77
|
+
? `import type { SimpleError } from '@vinejs/vine/types'\nexport type ValidationError = { errors: SimpleError[] }`
|
|
78
|
+
: '';
|
|
79
|
+
return [...imports, validationError, `export interface Routes {\n${members.join('\n')}\n}`].join('\n');
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { JsonSchema } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Converts a type the checker resolved into JSON Schema (draft 2020-12, as OpenAPI 3.1 uses).
|
|
5
|
+
* Serialized output types are plain data, so methods are dropped and dates become strings.
|
|
6
|
+
*/
|
|
7
|
+
export declare class TypeToSchema {
|
|
8
|
+
#private;
|
|
9
|
+
readonly components: Record<string, JsonSchema>;
|
|
10
|
+
readonly unsupported: Set<string>;
|
|
11
|
+
constructor(checker: ts.TypeChecker);
|
|
12
|
+
convert(type: ts.Type, stack?: ts.Type[]): JsonSchema;
|
|
13
|
+
/**
|
|
14
|
+
* Whether `undefined` is one of the members, which makes an object property optional.
|
|
15
|
+
*/
|
|
16
|
+
acceptsUndefined(type: ts.Type): boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
const OMITTED = ts.TypeFlags.Undefined | ts.TypeFlags.Void;
|
|
3
|
+
// `vine.file()` infers `MultipartFile` on the server and `File | Blob` for clients.
|
|
4
|
+
const BINARY = new Set(['MultipartFile', 'File', 'Blob']);
|
|
5
|
+
/**
|
|
6
|
+
* Converts a type the checker resolved into JSON Schema (draft 2020-12, as OpenAPI 3.1 uses).
|
|
7
|
+
* Serialized output types are plain data, so methods are dropped and dates become strings.
|
|
8
|
+
*/
|
|
9
|
+
export class TypeToSchema {
|
|
10
|
+
#checker;
|
|
11
|
+
#recursive = new Map();
|
|
12
|
+
components = {};
|
|
13
|
+
unsupported = new Set();
|
|
14
|
+
constructor(checker) {
|
|
15
|
+
this.#checker = checker;
|
|
16
|
+
}
|
|
17
|
+
convert(type, stack = []) {
|
|
18
|
+
const flags = type.flags;
|
|
19
|
+
if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
|
20
|
+
return {};
|
|
21
|
+
if (flags & ts.TypeFlags.Never)
|
|
22
|
+
return { not: {} };
|
|
23
|
+
if (type.isStringLiteral())
|
|
24
|
+
return { type: 'string', const: type.value };
|
|
25
|
+
if (type.isNumberLiteral())
|
|
26
|
+
return { type: 'number', const: type.value };
|
|
27
|
+
if (flags & ts.TypeFlags.BooleanLiteral)
|
|
28
|
+
return { type: 'boolean', const: this.#checker.typeToString(type) === 'true' };
|
|
29
|
+
if (flags & (ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral))
|
|
30
|
+
return { type: 'string' };
|
|
31
|
+
if (flags & ts.TypeFlags.Number)
|
|
32
|
+
return { type: 'number' };
|
|
33
|
+
if (flags & ts.TypeFlags.BigInt)
|
|
34
|
+
return { type: 'integer' };
|
|
35
|
+
if (flags & ts.TypeFlags.Boolean)
|
|
36
|
+
return { type: 'boolean' };
|
|
37
|
+
if (flags & ts.TypeFlags.Null)
|
|
38
|
+
return { type: 'null' };
|
|
39
|
+
if (flags & OMITTED)
|
|
40
|
+
return { not: {} };
|
|
41
|
+
if (type.isUnion())
|
|
42
|
+
return this.#union(type, stack);
|
|
43
|
+
const name = (type.aliasSymbol ?? type.getSymbol())?.getName();
|
|
44
|
+
if (name === 'Date' || name === 'DateTime')
|
|
45
|
+
return { type: 'string', format: 'date-time' };
|
|
46
|
+
if (name && BINARY.has(name))
|
|
47
|
+
return { type: 'string', format: 'binary' };
|
|
48
|
+
if (this.#checker.isArrayType(type)) {
|
|
49
|
+
// SAFETY: isArrayType only holds for TypeReference instances.
|
|
50
|
+
const [item] = this.#checker.getTypeArguments(type);
|
|
51
|
+
return { type: 'array', items: item ? this.convert(item, stack) : {} };
|
|
52
|
+
}
|
|
53
|
+
if (this.#checker.isTupleType(type)) {
|
|
54
|
+
// SAFETY: isTupleType only holds for TypeReference instances.
|
|
55
|
+
const items = this.#checker
|
|
56
|
+
.getTypeArguments(type)
|
|
57
|
+
.map(item => this.convert(item, stack));
|
|
58
|
+
return { type: 'array', prefixItems: items, minItems: items.length, maxItems: items.length };
|
|
59
|
+
}
|
|
60
|
+
if (flags & ts.TypeFlags.Object || type.isIntersection())
|
|
61
|
+
return this.#object(type, name, stack);
|
|
62
|
+
this.unsupported.add(this.#checker.typeToString(type));
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether `undefined` is one of the members, which makes an object property optional.
|
|
67
|
+
*/
|
|
68
|
+
acceptsUndefined(type) {
|
|
69
|
+
return type.isUnion()
|
|
70
|
+
? type.types.some(member => member.flags & OMITTED)
|
|
71
|
+
: Boolean(type.flags & OMITTED);
|
|
72
|
+
}
|
|
73
|
+
#union(type, stack) {
|
|
74
|
+
const members = type.types.filter(member => !(member.flags & OMITTED));
|
|
75
|
+
const booleans = members.filter(member => member.flags & ts.TypeFlags.BooleanLiteral);
|
|
76
|
+
const converted = members
|
|
77
|
+
.filter(member => !(member.flags & ts.TypeFlags.BooleanLiteral))
|
|
78
|
+
.map(member => this.convert(member, stack));
|
|
79
|
+
if (booleans.length === 2)
|
|
80
|
+
converted.push({ type: 'boolean' });
|
|
81
|
+
else
|
|
82
|
+
converted.push(...booleans.map(member => this.convert(member, stack)));
|
|
83
|
+
const schemas = [...new Map(converted.map(schema => [JSON.stringify(schema), schema])).values()];
|
|
84
|
+
if (schemas.length === 1)
|
|
85
|
+
return schemas[0];
|
|
86
|
+
if (schemas.every(schema => 'const' in schema || schema.type === 'null')) {
|
|
87
|
+
const types = [...new Set(schemas.flatMap(schema => schema.type ?? []))];
|
|
88
|
+
return {
|
|
89
|
+
type: types.length === 1 ? types[0] : types,
|
|
90
|
+
enum: schemas.map(schema => schema.const ?? null),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { anyOf: schemas };
|
|
94
|
+
}
|
|
95
|
+
#object(type, name, stack) {
|
|
96
|
+
if (stack.includes(type))
|
|
97
|
+
return { $ref: `#/components/schemas/${this.#recursiveName(type, name)}` };
|
|
98
|
+
const next = [...stack, type];
|
|
99
|
+
const properties = {};
|
|
100
|
+
const required = [];
|
|
101
|
+
for (const property of this.#checker.getPropertiesOfType(type)) {
|
|
102
|
+
const propertyType = this.#checker.getTypeOfSymbol(property);
|
|
103
|
+
if (propertyType.getCallSignatures().length)
|
|
104
|
+
continue;
|
|
105
|
+
properties[property.getName()] = this.convert(propertyType, next);
|
|
106
|
+
const optional = property.flags & ts.SymbolFlags.Optional || this.acceptsUndefined(propertyType);
|
|
107
|
+
if (!optional)
|
|
108
|
+
required.push(property.getName());
|
|
109
|
+
}
|
|
110
|
+
const [index] = this.#checker.getIndexInfosOfType(type);
|
|
111
|
+
const schema = {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties,
|
|
114
|
+
additionalProperties: index ? this.convert(index.type, next) : false,
|
|
115
|
+
};
|
|
116
|
+
if (required.length)
|
|
117
|
+
schema.required = required;
|
|
118
|
+
const ref = this.#recursive.get(type);
|
|
119
|
+
if (ref) {
|
|
120
|
+
this.components[ref] = schema;
|
|
121
|
+
return { $ref: `#/components/schemas/${ref}` };
|
|
122
|
+
}
|
|
123
|
+
return schema;
|
|
124
|
+
}
|
|
125
|
+
#recursiveName(type, name) {
|
|
126
|
+
const existing = this.#recursive.get(type);
|
|
127
|
+
if (existing)
|
|
128
|
+
return existing;
|
|
129
|
+
const base = name && !name.startsWith('__') ? name : 'Recursive';
|
|
130
|
+
const ref = this.components[base] ? `${base}${this.#recursive.size}` : base;
|
|
131
|
+
this.#recursive.set(type, ref);
|
|
132
|
+
this.components[ref] = {};
|
|
133
|
+
return ref;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type JsonValue = string | number | boolean | null;
|
|
2
|
+
/**
|
|
3
|
+
* The JSON Schema keywords the generator reads or writes. Keywords Vine adds
|
|
4
|
+
* (`minLength`, `pattern`...) pass through untouched.
|
|
5
|
+
*/
|
|
6
|
+
export interface JsonSchema {
|
|
7
|
+
$ref?: string;
|
|
8
|
+
type?: string | string[];
|
|
9
|
+
format?: string;
|
|
10
|
+
const?: JsonValue;
|
|
11
|
+
enum?: JsonValue[];
|
|
12
|
+
properties?: Record<string, JsonSchema>;
|
|
13
|
+
required?: string[];
|
|
14
|
+
additionalProperties?: boolean | JsonSchema;
|
|
15
|
+
items?: JsonSchema;
|
|
16
|
+
prefixItems?: JsonSchema[];
|
|
17
|
+
minItems?: number;
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
anyOf?: JsonSchema[];
|
|
20
|
+
not?: JsonSchema;
|
|
21
|
+
}
|
|
22
|
+
export type RouteMatcher = string | RegExp | ((routeName: string) => boolean);
|
|
23
|
+
export type SecurityScheme = {
|
|
24
|
+
type: 'http';
|
|
25
|
+
scheme: 'bearer' | 'basic';
|
|
26
|
+
bearerFormat?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'apiKey';
|
|
30
|
+
name: string;
|
|
31
|
+
in: 'header' | 'query' | 'cookie';
|
|
32
|
+
description?: string;
|
|
33
|
+
};
|
|
34
|
+
export interface OpenApiConfig {
|
|
35
|
+
/**
|
|
36
|
+
* Where the document is written, relative to the app root.
|
|
37
|
+
*/
|
|
38
|
+
output: string;
|
|
39
|
+
info: {
|
|
40
|
+
title: string;
|
|
41
|
+
version: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
};
|
|
44
|
+
servers: {
|
|
45
|
+
url: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
}[];
|
|
48
|
+
/**
|
|
49
|
+
* Same filters as Tuyau's `generateRegistry({ routes })`. Only one of them can be set.
|
|
50
|
+
*/
|
|
51
|
+
routes: {
|
|
52
|
+
only?: RouteMatcher[];
|
|
53
|
+
except?: RouteMatcher[];
|
|
54
|
+
};
|
|
55
|
+
securitySchemes: Record<string, SecurityScheme>;
|
|
56
|
+
/**
|
|
57
|
+
* Maps a named middleware (as in `middleware.auth()`) to a key of `securitySchemes`.
|
|
58
|
+
*/
|
|
59
|
+
middlewareSecurity: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options';
|
|
62
|
+
/**
|
|
63
|
+
* One named route as the generator sees it: where it lives, and the TypeScript
|
|
64
|
+
* expressions the routes scanner extracted for its input and output.
|
|
65
|
+
*/
|
|
66
|
+
export interface RouteOperation {
|
|
67
|
+
name: string;
|
|
68
|
+
methods: HttpMethod[];
|
|
69
|
+
path: string;
|
|
70
|
+
pathParams: string[];
|
|
71
|
+
middleware: string[];
|
|
72
|
+
requestType?: string;
|
|
73
|
+
responseType?: string;
|
|
74
|
+
imports: string[];
|
|
75
|
+
validator?: {
|
|
76
|
+
specifier: string;
|
|
77
|
+
exportName: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export interface OpenApiParameter {
|
|
81
|
+
name: string;
|
|
82
|
+
in: 'path' | 'query';
|
|
83
|
+
required: boolean;
|
|
84
|
+
schema: JsonSchema;
|
|
85
|
+
}
|
|
86
|
+
export interface OpenApiResponse {
|
|
87
|
+
description: string;
|
|
88
|
+
content?: Record<string, {
|
|
89
|
+
schema: JsonSchema;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
export interface OpenApiOperation {
|
|
93
|
+
operationId: string;
|
|
94
|
+
tags: string[];
|
|
95
|
+
parameters?: OpenApiParameter[];
|
|
96
|
+
requestBody?: {
|
|
97
|
+
required: true;
|
|
98
|
+
content: Record<string, {
|
|
99
|
+
schema: JsonSchema;
|
|
100
|
+
}>;
|
|
101
|
+
};
|
|
102
|
+
responses: Record<string, OpenApiResponse>;
|
|
103
|
+
security?: Record<string, string[]>[];
|
|
104
|
+
}
|
|
105
|
+
export interface OpenApiDocument {
|
|
106
|
+
openapi: '3.1.0';
|
|
107
|
+
info: OpenApiConfig['info'];
|
|
108
|
+
servers?: OpenApiConfig['servers'];
|
|
109
|
+
paths: Record<string, Partial<Record<HttpMethod, OpenApiOperation>>>;
|
|
110
|
+
components: {
|
|
111
|
+
schemas: Record<string, JsonSchema>;
|
|
112
|
+
securitySchemes: OpenApiConfig['securitySchemes'];
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.configPath('openapi.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
import { defineConfig } from '@enzopita/adonisjs-openapi'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
output: 'openapi.json',
|
|
8
|
+
info: { title: 'API', version: '1.0.0' },
|
|
9
|
+
securitySchemes: {
|
|
10
|
+
accessToken: { type: 'http', scheme: 'bearer' },
|
|
11
|
+
},
|
|
12
|
+
middlewareSecurity: {
|
|
13
|
+
auth: 'accessToken',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const stubsRoot: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const stubsRoot = import.meta.dirname;
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enzopita/adonisjs-openapi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate an OpenAPI document from the routes, validators and transformers of an AdonisJS app",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adonisjs",
|
|
7
|
+
"openapi",
|
|
8
|
+
"swagger",
|
|
9
|
+
"tuyau",
|
|
10
|
+
"vinejs"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Enzo Pita <enzo@enzopita.com>",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/enzopita/adonisjs-packages.git",
|
|
17
|
+
"directory": "packages/openapi"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"build",
|
|
21
|
+
"!build/bin",
|
|
22
|
+
"!build/tests"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./build/index.js",
|
|
27
|
+
"./commands": "./build/commands/main.js",
|
|
28
|
+
"./types": "./build/src/types.js"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"typescript": "~6.0.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@adonisjs/assembler": "^8.5.2",
|
|
38
|
+
"@adonisjs/core": "^7.6.0",
|
|
39
|
+
"@adonisjs/tsconfig": "^2.0.0",
|
|
40
|
+
"@japa/assert": "^4.2.0",
|
|
41
|
+
"@japa/runner": "^5.3.0",
|
|
42
|
+
"@poppinss/ts-exec": "^1.4.4",
|
|
43
|
+
"@types/node": "~26.6.5",
|
|
44
|
+
"@vinejs/vine": "^4.4.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@adonisjs/assembler": "^8.5.0",
|
|
48
|
+
"@adonisjs/core": "^7.6.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=24.0.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rm -rf build && tsc -p tsconfig.build.json && cp -r stubs/config build/stubs/",
|
|
55
|
+
"test": "pnpm run build && node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
}
|
|
6
58
|
}
|